18
18
path as mpath )
19
19
from matplotlib .collections import (
20
20
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
@@ -1052,7 +1113,7 @@ def set_alpha(self, alpha):
1052
1113
pass
1053
1114
try :
1054
1115
self ._edgecolors = mcolors .to_rgba_array (
1055
- self ._edgecolor3d , self ._alpha )
1116
+ self ._edgecolor3d , self ._alpha )
1056
1117
except (AttributeError , TypeError , IndexError ):
1057
1118
pass
1058
1119
self .stale = True
@@ -1074,6 +1135,195 @@ def get_edgecolor(self):
1074
1135
return np .asarray (self ._edgecolors2d )
1075
1136
1076
1137
1138
+ class Bar3DCollection (Poly3DCollection ):
1139
+ """
1140
+ Bars with constant square cross section, bases located on z-plane at *z0*,
1141
+ aranged in a regular grid at *x*, *y* locations and height *z - z0*.
1142
+ """
1143
+
1144
+ # TODO: don't need to plot occluded faces
1145
+ # ie. back panels don't need to be drawn if alpha == 1
1146
+
1147
+ def __init__ (self , x , y , z , dxy = 0.8 , z0 = 0 , shade = True , lightsource = None , ** kws ):
1148
+ #
1149
+ assert 0 < dxy <= 1
1150
+
1151
+ self .xyz = np .atleast_3d ([x , y , z ])
1152
+ self .z0 = float (z0 )
1153
+ # self.dxy = dxy = float(dxy)
1154
+
1155
+ # bar width and breadth
1156
+ self .dx , self .dy = self ._resolve_dx_dy (dxy )
1157
+
1158
+ # Shade faces by angle to light source
1159
+ self ._original_alpha = kws .pop ('alpha' , None )
1160
+ self ._shade = bool (shade )
1161
+ if lightsource is None :
1162
+ # chosen for backwards-compatibility
1163
+ lightsource = LightSource (azdeg = 225 , altdeg = 19.4712 )
1164
+ else :
1165
+ assert isinstance (lightsource , LightSource )
1166
+ self ._lightsource = lightsource
1167
+
1168
+ # rectangle polygon vertices
1169
+ verts = self ._compute_bar3d_verts ()
1170
+
1171
+ # init Poly3DCollection
1172
+ if (no_cmap := {'color' , 'facecolor' , 'facecolors' }.intersection (kws )):
1173
+ kws .pop ('cmap' , None )
1174
+
1175
+ # print(kws)
1176
+ Poly3DCollection .__init__ (self , verts , ** kws )
1177
+
1178
+ if not no_cmap :
1179
+ self .set_array (z .ravel ())
1180
+
1181
+ def _resolve_dx_dy (self , dxy ):
1182
+ d = [dxy , dxy ]
1183
+ for i , s in enumerate (self .xyz .shape [1 :]):
1184
+ # dxy * (#np.array([1]) if s == 1 else
1185
+ d [i ], = dxy * np .diff (self .xyz [(i , * (0 , np .s_ [:2 ])[::(1 , - 1 )[i ]])])
1186
+ dx , dy = d
1187
+ assert (dx != 0 ) & (dy != 0 )
1188
+ return dx , dy
1189
+
1190
+ @property
1191
+ def xy (self ):
1192
+ return self .xyz [:2 ]
1193
+
1194
+ @property
1195
+ def z (self ):
1196
+ return self .xyz [2 ]
1197
+
1198
+ def set_z0 (self , z0 ):
1199
+ self .z0 = float (z0 )
1200
+ super ().set_verts (self ._compute_verts ())
1201
+
1202
+ def set_z (self , z , clim = None ):
1203
+ self .xyz [2 ] = z
1204
+ self .set_data (self .xyz , clim )
1205
+
1206
+ def set_data (self , xyz , clim = None ):
1207
+ self .xyz = np .atleast_3d (xyz )
1208
+ super ().set_verts (self ._compute_verts ())
1209
+ self .set_array (z := self .z .ravel ())
1210
+
1211
+ if clim is None or clim is True :
1212
+ clim = (z .min (), z .max ())
1213
+ if clim is not False :
1214
+ self .set_clim (* clim )
1215
+
1216
+ if not self .axes :
1217
+ return
1218
+
1219
+ if self .axes .M is not None :
1220
+ self .do_3d_projection ()
1221
+
1222
+ def _compute_verts (self ):
1223
+ x , y , dz = self .xyz
1224
+ dx = np .full (x .shape , self .dx )
1225
+ dy = np .full (x .shape , self .dy )
1226
+ z = np .full (x .shape , self .z0 )
1227
+ return _compute_bar3d_verts (x , y , z , dx , dy , dz )
1228
+
1229
+ def do_3d_projection (self ):
1230
+ """
1231
+ Perform the 3D projection for this object.
1232
+ """
1233
+ if self ._A is not None :
1234
+ # force update of color mapping because we re-order them
1235
+ # below. If we do not do this here, the 2D draw will call
1236
+ # this, but we will never port the color mapped values back
1237
+ # to the 3D versions.
1238
+ #
1239
+ # We hold the 3D versions in a fixed order (the order the user
1240
+ # passed in) and sort the 2D version by view depth.
1241
+ self .update_scalarmappable ()
1242
+ if self ._face_is_mapped :
1243
+ self ._facecolor3d = self ._facecolors
1244
+ if self ._edge_is_mapped :
1245
+ self ._edgecolor3d = self ._edgecolors
1246
+
1247
+ txs , tys , tzs = proj3d ._proj_transform_vec (self ._vec , self .axes .M )
1248
+ xyzlist = [(txs [sl ], tys [sl ], tzs [sl ]) for sl in self ._segslices ]
1249
+
1250
+ # get panel facecolors
1251
+ cface , cedge = self ._resolve_colors (xyzlist , self ._lightsource )
1252
+
1253
+ if xyzlist :
1254
+ zorder = self ._compute_zorder ()
1255
+
1256
+ z_segments_2d = sorted (
1257
+ ((zo , np .column_stack ([xs , ys ]), fc , ec , idx )
1258
+ for idx , (zo , (xs , ys , _ ), fc , ec )
1259
+ in enumerate (zip (zorder , xyzlist , cface , cedge ))),
1260
+ key = lambda x : x [0 ], reverse = True )
1261
+
1262
+ _ , segments_2d , self ._facecolors2d , self ._edgecolors2d , idxs = \
1263
+ zip (* z_segments_2d )
1264
+ else :
1265
+ segments_2d = []
1266
+ self ._facecolors2d = np .empty ((0 , 4 ))
1267
+ self ._edgecolors2d = np .empty ((0 , 4 ))
1268
+ idxs = []
1269
+
1270
+ if self ._codes3d is None :
1271
+ PolyCollection .set_verts (self , segments_2d , self ._closed )
1272
+ else :
1273
+ codes = [self ._codes3d [idx ] for idx in idxs ]
1274
+ PolyCollection .set_verts_and_codes (self , segments_2d , codes )
1275
+
1276
+ if len (self ._edgecolor3d ) != len (cface ):
1277
+ self ._edgecolors2d = self ._edgecolor3d
1278
+
1279
+ # Return zorder value
1280
+ if self ._sort_zpos is not None :
1281
+ zvec = np .array ([[0 ], [0 ], [self ._sort_zpos ], [1 ]])
1282
+ ztrans = proj3d ._proj_transform_vec (zvec , self .axes .M )
1283
+ return ztrans [2 ][0 ]
1284
+
1285
+ if tzs .size > 0 :
1286
+ # FIXME: Some results still don't look quite right.
1287
+ # In particular, examine contourf3d_demo2.py
1288
+ # with az = -54 and elev = -45.
1289
+ return np .min (tzs )
1290
+
1291
+ return np .nan
1292
+
1293
+ def _resolve_colors (self , xyzlist , lightsource ):
1294
+ # This extra fuss is to re-order face / edge colors
1295
+ cface = self ._facecolor3d
1296
+ cedge = self ._edgecolor3d
1297
+ n , nc = len (xyzlist ), len (cface )
1298
+ if (nc == 1 ) or (nc * 6 == n ):
1299
+ cface = cface .repeat (n // nc , axis = 0 )
1300
+ if self ._shade :
1301
+ verts = self ._compute_verts ()
1302
+ ax = self .axes
1303
+ normals = _generate_normals (verts )
1304
+ cface = _shade_colors (cface , normals , lightsource )
1305
+
1306
+ if self ._original_alpha is not None :
1307
+ cface [:, - 1 ] = self ._original_alpha
1308
+
1309
+ if len (cface ) != n :
1310
+ cface = cface .repeat (n , axis = 0 )
1311
+
1312
+ if len (cedge ) != n :
1313
+ cedge = cface if len (cedge ) == 0 else cedge .repeat (n , axis = 0 )
1314
+
1315
+ return cface , cedge
1316
+
1317
+ def _compute_zorder (self ):
1318
+ # sort by depth (furthest drawn first)
1319
+ zorder = camera .distance (self .axes , * self .xy )
1320
+ zorder = (zorder - zorder .min ()) / zorder .ptp ()
1321
+ zorder = zorder .ravel () * len (zorder )
1322
+ panel_order = get_cube_face_zorder (self .axes )
1323
+ zorder = (zorder [..., None ] + panel_order / 6 ).ravel ()
1324
+ return zorder
1325
+
1326
+
1077
1327
def poly_collection_2d_to_3d (col , zs = 0 , zdir = 'z' ):
1078
1328
"""
1079
1329
Convert a `.PolyCollection` into a `.Poly3DCollection` object.
@@ -1218,3 +1468,35 @@ def norm(x):
1218
1468
colors = np .asanyarray (color ).copy ()
1219
1469
1220
1470
return colors
1471
+
1472
+
1473
+ def _compute__bar3d_verts (x , y , z , dx , dy , dz ):
1474
+ # indexed by [bar, face, vertex, coord]
1475
+
1476
+ # handle each coordinate separately
1477
+ polys = np .empty (x .shape + CUBOID .shape )
1478
+ for i , (p , dp ) in enumerate (((x , dx ), (y , dy ), (z , dz ))):
1479
+ p = p [..., np .newaxis , np .newaxis ]
1480
+ dp = dp [..., np .newaxis , np .newaxis ]
1481
+ polys [..., i ] = p + dp * CUBOID [..., i ]
1482
+
1483
+ # collapse the first two axes
1484
+ return polys .reshape ((- 1 ,) + polys .shape [- 2 :])
1485
+
1486
+
1487
+ def get_cube_face_zorder (ax ):
1488
+ # -z, +z, -y, +y, -x, +x
1489
+ # 0, 1, 2, 3, 4, 5
1490
+
1491
+ view_quadrant = int ((ax .azim % 360 ) // 90 )
1492
+ idx = CAMERA_VIEW_QUADRANT_TO_CUBE_FACE_ZORDER [view_quadrant ]
1493
+ order = np .array (idx )
1494
+
1495
+ if (ax .elev % 180 ) > 90 :
1496
+ order [:2 ] = order [1 ::- 1 ]
1497
+
1498
+ # logger.trace('Panel draw order quadrant {}:\n{}\n{}', view_quadrant, order,
1499
+ # list(np.take(['-z', '+z', '-y', '+y', '-x', '+x'],
1500
+ # order)))
1501
+
1502
+ return order
0 commit comments