Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit afbf6e4

Browse files
committed
DEV: add Bar3DCollection for 3d bar graphs
1 parent bcaffa1 commit afbf6e4

File tree

1 file changed

+284
-2
lines changed

1 file changed

+284
-2
lines changed

lib/mpl_toolkits/mplot3d/art3d.py

Lines changed: 284 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,72 @@
1818
path as mpath)
1919
from matplotlib.collections import (
2020
Collection, LineCollection, PolyCollection, PatchCollection, PathCollection)
21-
from matplotlib.colors import Normalize
21+
from matplotlib.colors import Normalize, LightSource
2222
from matplotlib.patches import Patch
2323
from . import proj3d
2424

2525

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+
2687
def _norm_angle(a):
2788
"""Return the given angle normalized to -180 < *a* <= 180 degrees."""
2889
a = (a + 360) % 360
@@ -1096,7 +1157,7 @@ def set_alpha(self, alpha):
10961157
pass
10971158
try:
10981159
self._edgecolors = mcolors.to_rgba_array(
1099-
self._edgecolor3d, self._alpha)
1160+
self._edgecolor3d, self._alpha)
11001161
except (AttributeError, TypeError, IndexError):
11011162
pass
11021163
self.stale = True
@@ -1118,6 +1179,195 @@ def get_edgecolor(self):
11181179
return np.asarray(self._edgecolors2d)
11191180

11201181

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+
11211371
def poly_collection_2d_to_3d(col, zs=0, zdir='z'):
11221372
"""
11231373
Convert a `.PolyCollection` into a `.Poly3DCollection` object.
@@ -1264,3 +1514,35 @@ def norm(x):
12641514
colors = np.asanyarray(color).copy()
12651515

12661516
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

Comments
 (0)