18
18
path as mpath )
19
19
from matplotlib .collections import (
20
20
Collection , LineCollection , PolyCollection , PatchCollection , PathCollection )
21
- from matplotlib .colors import Normalize
21
+ from matplotlib .colors import Normalize , LightSource
22
22
from matplotlib .patches import Patch
23
23
from . import proj3d
24
24
25
25
26
+ # shape (6, 4, 3)
27
+ # All faces are oriented facing outwards - when viewed from the
28
+ # outside, their vertices are in a counterclockwise ordering.
29
+ CUBOID = np .array ([
30
+ # -z
31
+ (
32
+ (0 , 0 , 0 ),
33
+ (0 , 1 , 0 ),
34
+ (1 , 1 , 0 ),
35
+ (1 , 0 , 0 ),
36
+ ),
37
+ # +z
38
+ (
39
+ (0 , 0 , 1 ),
40
+ (1 , 0 , 1 ),
41
+ (1 , 1 , 1 ),
42
+ (0 , 1 , 1 ),
43
+ ),
44
+ # -y
45
+ (
46
+ (0 , 0 , 0 ),
47
+ (1 , 0 , 0 ),
48
+ (1 , 0 , 1 ),
49
+ (0 , 0 , 1 ),
50
+ ),
51
+ # +y
52
+ (
53
+ (0 , 1 , 0 ),
54
+ (0 , 1 , 1 ),
55
+ (1 , 1 , 1 ),
56
+ (1 , 1 , 0 ),
57
+ ),
58
+ # -x
59
+ (
60
+ (0 , 0 , 0 ),
61
+ (0 , 0 , 1 ),
62
+ (0 , 1 , 1 ),
63
+ (0 , 1 , 0 ),
64
+ ),
65
+ # +x
66
+ (
67
+ (1 , 0 , 0 ),
68
+ (1 , 1 , 0 ),
69
+ (1 , 1 , 1 ),
70
+ (1 , 0 , 1 ),
71
+ ),
72
+ ])
73
+
74
+ CAMERA_VIEW_QUADRANT_TO_CUBE_FACE_ZORDER = {
75
+ # -z, +z, -y, +y, -x, +x
76
+ # 0, 1, 2, 3, 4, 5
77
+
78
+ # viewing | cube face | cube face
79
+ # quadrant| indices | names order
80
+ 0 : (5 , 0 , 4 , 1 , 3 , 2 ), # ('+z', '+y', '+x', '-x', '-y', '-z')
81
+ 1 : (5 , 0 , 4 , 1 , 2 , 3 ), # ('+z', '+y', '-x', '+x', '-y', '-z')
82
+ 2 : (5 , 0 , 1 , 4 , 2 , 3 ), # ('+z', '-y', '-x', '+x', '+y', '-z')
83
+ 3 : (5 , 0 , 1 , 4 , 3 , 2 ) # ('+z', '-y', '+x', '-x', '+y', '-z')
84
+ }
85
+
86
+
26
87
def _norm_angle (a ):
27
88
"""Return the given angle normalized to -180 < *a* <= 180 degrees."""
28
89
a = (a + 360 ) % 360
@@ -1096,7 +1157,7 @@ def set_alpha(self, alpha):
1096
1157
pass
1097
1158
try :
1098
1159
self ._edgecolors = mcolors .to_rgba_array (
1099
- self ._edgecolor3d , self ._alpha )
1160
+ self ._edgecolor3d , self ._alpha )
1100
1161
except (AttributeError , TypeError , IndexError ):
1101
1162
pass
1102
1163
self .stale = True
@@ -1118,6 +1179,195 @@ def get_edgecolor(self):
1118
1179
return np .asarray (self ._edgecolors2d )
1119
1180
1120
1181
1182
+ class Bar3DCollection (Poly3DCollection ):
1183
+ """
1184
+ Bars with constant square cross section, bases located on z-plane at *z0*,
1185
+ aranged in a regular grid at *x*, *y* locations and height *z - z0*.
1186
+ """
1187
+
1188
+ # TODO: don't need to plot occluded faces
1189
+ # ie. back panels don't need to be drawn if alpha == 1
1190
+
1191
+ def __init__ (self , x , y , z , dxy = 0.8 , z0 = 0 , shade = True , lightsource = None , ** kws ):
1192
+ #
1193
+ assert 0 < dxy <= 1
1194
+
1195
+ self .xyz = np .atleast_3d ([x , y , z ])
1196
+ self .z0 = float (z0 )
1197
+ # self.dxy = dxy = float(dxy)
1198
+
1199
+ # bar width and breadth
1200
+ self .dx , self .dy = self ._resolve_dx_dy (dxy )
1201
+
1202
+ # Shade faces by angle to light source
1203
+ self ._original_alpha = kws .pop ('alpha' , None )
1204
+ self ._shade = bool (shade )
1205
+ if lightsource is None :
1206
+ # chosen for backwards-compatibility
1207
+ lightsource = LightSource (azdeg = 225 , altdeg = 19.4712 )
1208
+ else :
1209
+ assert isinstance (lightsource , LightSource )
1210
+ self ._lightsource = lightsource
1211
+
1212
+ # rectangle polygon vertices
1213
+ verts = self ._compute_bar3d_verts ()
1214
+
1215
+ # init Poly3DCollection
1216
+ if (no_cmap := {'color' , 'facecolor' , 'facecolors' }.intersection (kws )):
1217
+ kws .pop ('cmap' , None )
1218
+
1219
+ # print(kws)
1220
+ Poly3DCollection .__init__ (self , verts , ** kws )
1221
+
1222
+ if not no_cmap :
1223
+ self .set_array (z .ravel ())
1224
+
1225
+ def _resolve_dx_dy (self , dxy ):
1226
+ d = [dxy , dxy ]
1227
+ for i , s in enumerate (self .xyz .shape [1 :]):
1228
+ # dxy * (#np.array([1]) if s == 1 else
1229
+ d [i ], = dxy * np .diff (self .xyz [(i , * (0 , np .s_ [:2 ])[::(1 , - 1 )[i ]])])
1230
+ dx , dy = d
1231
+ assert (dx != 0 ) & (dy != 0 )
1232
+ return dx , dy
1233
+
1234
+ @property
1235
+ def xy (self ):
1236
+ return self .xyz [:2 ]
1237
+
1238
+ @property
1239
+ def z (self ):
1240
+ return self .xyz [2 ]
1241
+
1242
+ def set_z0 (self , z0 ):
1243
+ self .z0 = float (z0 )
1244
+ super ().set_verts (self ._compute_verts ())
1245
+
1246
+ def set_z (self , z , clim = None ):
1247
+ self .xyz [2 ] = z
1248
+ self .set_data (self .xyz , clim )
1249
+
1250
+ def set_data (self , xyz , clim = None ):
1251
+ self .xyz = np .atleast_3d (xyz )
1252
+ super ().set_verts (self ._compute_verts ())
1253
+ self .set_array (z := self .z .ravel ())
1254
+
1255
+ if clim is None or clim is True :
1256
+ clim = (z .min (), z .max ())
1257
+ if clim is not False :
1258
+ self .set_clim (* clim )
1259
+
1260
+ if not self .axes :
1261
+ return
1262
+
1263
+ if self .axes .M is not None :
1264
+ self .do_3d_projection ()
1265
+
1266
+ def _compute_verts (self ):
1267
+ x , y , dz = self .xyz
1268
+ dx = np .full (x .shape , self .dx )
1269
+ dy = np .full (x .shape , self .dy )
1270
+ z = np .full (x .shape , self .z0 )
1271
+ return _compute_bar3d_verts (x , y , z , dx , dy , dz )
1272
+
1273
+ def do_3d_projection (self ):
1274
+ """
1275
+ Perform the 3D projection for this object.
1276
+ """
1277
+ if self ._A is not None :
1278
+ # force update of color mapping because we re-order them
1279
+ # below. If we do not do this here, the 2D draw will call
1280
+ # this, but we will never port the color mapped values back
1281
+ # to the 3D versions.
1282
+ #
1283
+ # We hold the 3D versions in a fixed order (the order the user
1284
+ # passed in) and sort the 2D version by view depth.
1285
+ self .update_scalarmappable ()
1286
+ if self ._face_is_mapped :
1287
+ self ._facecolor3d = self ._facecolors
1288
+ if self ._edge_is_mapped :
1289
+ self ._edgecolor3d = self ._edgecolors
1290
+
1291
+ txs , tys , tzs = proj3d ._proj_transform_vec (self ._vec , self .axes .M )
1292
+ xyzlist = [(txs [sl ], tys [sl ], tzs [sl ]) for sl in self ._segslices ]
1293
+
1294
+ # get panel facecolors
1295
+ cface , cedge = self ._resolve_colors (xyzlist , self ._lightsource )
1296
+
1297
+ if xyzlist :
1298
+ zorder = self ._compute_zorder ()
1299
+
1300
+ z_segments_2d = sorted (
1301
+ ((zo , np .column_stack ([xs , ys ]), fc , ec , idx )
1302
+ for idx , (zo , (xs , ys , _ ), fc , ec )
1303
+ in enumerate (zip (zorder , xyzlist , cface , cedge ))),
1304
+ key = lambda x : x [0 ], reverse = True )
1305
+
1306
+ _ , segments_2d , self ._facecolors2d , self ._edgecolors2d , idxs = \
1307
+ zip (* z_segments_2d )
1308
+ else :
1309
+ segments_2d = []
1310
+ self ._facecolors2d = np .empty ((0 , 4 ))
1311
+ self ._edgecolors2d = np .empty ((0 , 4 ))
1312
+ idxs = []
1313
+
1314
+ if self ._codes3d is None :
1315
+ PolyCollection .set_verts (self , segments_2d , self ._closed )
1316
+ else :
1317
+ codes = [self ._codes3d [idx ] for idx in idxs ]
1318
+ PolyCollection .set_verts_and_codes (self , segments_2d , codes )
1319
+
1320
+ if len (self ._edgecolor3d ) != len (cface ):
1321
+ self ._edgecolors2d = self ._edgecolor3d
1322
+
1323
+ # Return zorder value
1324
+ if self ._sort_zpos is not None :
1325
+ zvec = np .array ([[0 ], [0 ], [self ._sort_zpos ], [1 ]])
1326
+ ztrans = proj3d ._proj_transform_vec (zvec , self .axes .M )
1327
+ return ztrans [2 ][0 ]
1328
+
1329
+ if tzs .size > 0 :
1330
+ # FIXME: Some results still don't look quite right.
1331
+ # In particular, examine contourf3d_demo2.py
1332
+ # with az = -54 and elev = -45.
1333
+ return np .min (tzs )
1334
+
1335
+ return np .nan
1336
+
1337
+ def _resolve_colors (self , xyzlist , lightsource ):
1338
+ # This extra fuss is to re-order face / edge colors
1339
+ cface = self ._facecolor3d
1340
+ cedge = self ._edgecolor3d
1341
+ n , nc = len (xyzlist ), len (cface )
1342
+ if (nc == 1 ) or (nc * 6 == n ):
1343
+ cface = cface .repeat (n // nc , axis = 0 )
1344
+ if self ._shade :
1345
+ verts = self ._compute_verts ()
1346
+ ax = self .axes
1347
+ normals = _generate_normals (verts )
1348
+ cface = _shade_colors (cface , normals , lightsource )
1349
+
1350
+ if self ._original_alpha is not None :
1351
+ cface [:, - 1 ] = self ._original_alpha
1352
+
1353
+ if len (cface ) != n :
1354
+ cface = cface .repeat (n , axis = 0 )
1355
+
1356
+ if len (cedge ) != n :
1357
+ cedge = cface if len (cedge ) == 0 else cedge .repeat (n , axis = 0 )
1358
+
1359
+ return cface , cedge
1360
+
1361
+ def _compute_zorder (self ):
1362
+ # sort by depth (furthest drawn first)
1363
+ zorder = camera .distance (self .axes , * self .xy )
1364
+ zorder = (zorder - zorder .min ()) / zorder .ptp ()
1365
+ zorder = zorder .ravel () * len (zorder )
1366
+ panel_order = get_cube_face_zorder (self .axes )
1367
+ zorder = (zorder [..., None ] + panel_order / 6 ).ravel ()
1368
+ return zorder
1369
+
1370
+
1121
1371
def poly_collection_2d_to_3d (col , zs = 0 , zdir = 'z' ):
1122
1372
"""
1123
1373
Convert a `.PolyCollection` into a `.Poly3DCollection` object.
@@ -1264,3 +1514,35 @@ def norm(x):
1264
1514
colors = np .asanyarray (color ).copy ()
1265
1515
1266
1516
return colors
1517
+
1518
+
1519
+ def _compute__bar3d_verts (x , y , z , dx , dy , dz ):
1520
+ # indexed by [bar, face, vertex, coord]
1521
+
1522
+ # handle each coordinate separately
1523
+ polys = np .empty (x .shape + CUBOID .shape )
1524
+ for i , (p , dp ) in enumerate (((x , dx ), (y , dy ), (z , dz ))):
1525
+ p = p [..., np .newaxis , np .newaxis ]
1526
+ dp = dp [..., np .newaxis , np .newaxis ]
1527
+ polys [..., i ] = p + dp * CUBOID [..., i ]
1528
+
1529
+ # collapse the first two axes
1530
+ return polys .reshape ((- 1 ,) + polys .shape [- 2 :])
1531
+
1532
+
1533
+ def get_cube_face_zorder (ax ):
1534
+ # -z, +z, -y, +y, -x, +x
1535
+ # 0, 1, 2, 3, 4, 5
1536
+
1537
+ view_quadrant = int ((ax .azim % 360 ) // 90 )
1538
+ idx = CAMERA_VIEW_QUADRANT_TO_CUBE_FACE_ZORDER [view_quadrant ]
1539
+ order = np .array (idx )
1540
+
1541
+ if (ax .elev % 180 ) > 90 :
1542
+ order [:2 ] = order [1 ::- 1 ]
1543
+
1544
+ # logger.trace('Panel draw order quadrant {}:\n{}\n{}', view_quadrant, order,
1545
+ # list(np.take(['-z', '+z', '-y', '+y', '-x', '+x'],
1546
+ # order)))
1547
+
1548
+ return order
0 commit comments