Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions lib/mpl_toolkits/mplot3d/art3d.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# art3d.py, original mplot3d version by John Porter
# Parts rewritten by Reinier Heeres <reinier@heeres.eu>
# Minor additions by Ben Axelrod <baxelrod@coroware.com>
Expand Down Expand Up @@ -1206,22 +1206,26 @@
and _edgecolors properties.
"""
if shade:
normals = _generate_normals(verts)
self.shaded = True
self.normals = _generate_normals(verts)
self.lightsource = lightsource
facecolors = kwargs.get('facecolors', None)
if facecolors is not None:
kwargs['facecolors'] = _shade_colors(
facecolors, normals, lightsource
facecolors, self.normals, self.lightsource
)

edgecolors = kwargs.get('edgecolors', None)
if edgecolors is not None:
kwargs['edgecolors'] = _shade_colors(
edgecolors, normals, lightsource
edgecolors, self.normals, self.lightsource
)
if facecolors is None and edgecolors is None:
raise ValueError(
"You must provide facecolors, edgecolors, or both for "
"shade to work.")
else:
self.shaded = False
super().__init__(verts, *args, **kwargs)
if isinstance(verts, np.ndarray):
if verts.ndim != 3:
Expand All @@ -1233,6 +1237,37 @@
self._codes3d = None
self._axlim_clip = axlim_clip

def update_scalarmappable(self):
"""
Update colors from the scalar mappable array, if any.

This overrides `Collection.update_scalarmappable()`.
This function differs in the following way:
1. This function only sets facecolors, never edgecolors
2. This function applies shading.
3. self._A is assumed to have the correct shape
"""
if not self._set_mappable_flags():
return
# Allow possibility to call 'self.set_array(None)'.
if self._A is not None:
if np.iterable(self._alpha):
if self._alpha.size != self._A.size:
raise ValueError(
f'Data array shape, {self._A.shape} '
'is incompatible with alpha array shape, '
f'{self._alpha.shape}. '
)
self._alpha = self._alpha.reshape(self._A.shape)
self._mapped_colors = self.to_rgba(self._A, self._alpha)
if self.shaded:
self._mapped_colors = _shade_colors(
self._mapped_colors, self.normals, self.lightsource
)

self._facecolors = self._mapped_colors
self.stale = True

_zsort_functions = {
'average': np.average,
'min': np.min,
Expand Down
102 changes: 69 additions & 33 deletions lib/mpl_toolkits/mplot3d/axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -3380,7 +3380,8 @@ def calc_arrows(UVW):
quiver3D = quiver

def voxels(self, *args, facecolors=None, edgecolors=None, shade=True,
lightsource=None, axlim_clip=False, **kwargs):
lightsource=None, axlim_clip=False, colorizer=None,
norm=None, cmap=None, vmin=None, vmax=None, **kwargs):
"""
ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \
**kwargs)
Expand All @@ -3393,9 +3394,10 @@ def voxels(self, *args, facecolors=None, edgecolors=None, shade=True,

Parameters
----------
filled : 3D np.array of bool
A 3D array of values, with truthy values indicating which voxels
to fill
filled : 3D np.array of bool or float
If bool, with truthy values indicate which voxels to fill.
If float, voxels with finite walues are shown with color
mapped via *cmap* and *norm*, while voxels with nan are ignored.

x, y, z : 3D np.array, optional
The coordinates of the corners of the voxels. This should broadcast
Expand Down Expand Up @@ -3432,6 +3434,19 @@ def voxels(self, *args, facecolors=None, edgecolors=None, shade=True,

.. versionadded:: 3.10

cmap : Colormap, optional
Colormap to use if facecolor is scalar.

norm : `~matplotlib.colors.Normalize`, optional
Normalization for the colormap.

vmin, vmax : float, optional
Bounds for the normalization.

colorizer : `~matplotlib.colorizer.Colorizer` or None, default: None
The Colorizer object used to map color to data. If None, a Colorizer
object is created from a *norm* and *cmap*.

**kwargs
Additional keyword arguments to pass onto
`~mpl_toolkits.mplot3d.art3d.Poly3DCollection`.
Expand All @@ -3452,6 +3467,10 @@ def voxels(self, *args, facecolors=None, edgecolors=None, shade=True,
.. plot:: gallery/mplot3d/voxels_torus.py
.. plot:: gallery/mplot3d/voxels_numpy_logo.py
"""
mpl.colorizer.ColorizingArtist._check_exclusionary_keywords(
colorizer, cmap=cmap, norm=norm, vmin=vmin, vmax=vmax)
if colorizer is None:
colorizer = mpl.colorizer.Colorizer(cmap, norm)

# work out which signature we should be using, and use it to parse
# the arguments. Name must be voxels for the correct error message
Expand All @@ -3465,6 +3484,12 @@ def voxels(filled, **kwargs):

xyz, filled, kwargs = voxels(*args, **kwargs)

if filled.dtype == np.float64:
scalars = filled
filled = np.isfinite(filled)
else:
scalars = None

# check dimensions
if filled.ndim != 3:
raise ValueError("Argument filled must be 3-dimensional")
Expand Down Expand Up @@ -3497,7 +3522,8 @@ def _broadcast_color_arg(color, name):
facecolors = _broadcast_color_arg(facecolors, 'facecolors')

# broadcast but no default on edgecolors
edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors')
if edgecolors is not None:
edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors')

# scale to the full array, even if the data is only in the center
self.auto_scale_xyz(x, y, z)
Expand Down Expand Up @@ -3564,35 +3590,45 @@ def permutation_matrices(n):
if filled[ik]:
voxel_faces[ik].append(pk2 + square_rot_pos)

# iterate over the faces, and generate a Poly3DCollection for each
# voxel
polygons = {}
# iterate over the faces, and collect input for a Poly3DCollection
coords = []
faces_indexes = []
for coord, faces_inds in voxel_faces.items():
# convert indices into 3D positions
if xyz is None:
faces = faces_inds
else:
faces = []
for face_inds in faces_inds:
ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2]
face = np.empty(face_inds.shape)
face[:, 0] = x[ind]
face[:, 1] = y[ind]
face[:, 2] = z[ind]
faces.append(face)

# shade the faces
facecolor = facecolors[coord]
edgecolor = edgecolors[coord]

poly = art3d.Poly3DCollection(
faces, facecolors=facecolor, edgecolors=edgecolor,
shade=shade, lightsource=lightsource, axlim_clip=axlim_clip,
**kwargs)
self.add_collection3d(poly)
polygons[coord] = poly

return polygons
coords.append([coord] * len(faces_inds))
faces_indexes.append(faces_inds)
coords = np.concatenate(coords, axis=0).T
faces_indexes = np.concatenate(faces_indexes, axis=0)

if xyz is None:
faces = faces_indexes
else:
faces = []
for face_inds in faces_indexes:
ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2]
face = np.empty(face_inds.shape)
face[:, 0] = x[ind]
face[:, 1] = y[ind]
face[:, 2] = z[ind]
faces.append(face)

# shade the faces
facecolor = facecolors[*coords]
if edgecolors is not None:
edgecolor = edgecolors[*coords]
else:
edgecolor = None

poly = art3d.Poly3DCollection(
faces, facecolors=facecolor, edgecolors=edgecolor,
shade=shade, lightsource=lightsource, axlim_clip=axlim_clip,
colorizer=colorizer,
**kwargs)

if scalars is not None:
poly.set_array(scalars[*coords])

self.add_collection3d(poly)
return poly

@_preprocess_data(replace_names=["x", "y", "z", "xerr", "yerr", "zerr"])
def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 33 additions & 5 deletions lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -1450,7 +1450,7 @@ def test_edge_style(self):
v = ax.voxels(voxels, linewidths=3, edgecolor='C1')

# change the edge color of one voxel
v[max(v.keys())].set_edgecolor('C2')
v.set_edgecolor('C2')

@mpl3d_image_comparison(['voxels-named-colors.png'], style='mpl20')
def test_named_colors(self):
Expand Down Expand Up @@ -1491,10 +1491,7 @@ def test_alpha(self):
colors[v1] = [0, 1, 0, 0.5]
v = ax.voxels(voxels, facecolors=colors)

assert type(v) is dict
for coord, poly in v.items():
assert voxels[coord], "faces returned for absent voxel"
assert isinstance(poly, art3d.Poly3DCollection)
assert type(v) is art3d.Poly3DCollection

@mpl3d_image_comparison(['voxels-xyz.png'],
tol=0.01, remove_text=False, style='mpl20')
Expand Down Expand Up @@ -1555,6 +1552,37 @@ def test_calling_conventions(self):
ax.voxels(filled=filled, x=x, y=y, z=z)
assert exec_info.value.name == 'x'

@mpl3d_image_comparison(['voxels-scalar.png'],
tol=0.01, remove_text=False, style='mpl20')
def test_scalar_input(self):
Hist = np.zeros((9, 9, 9))
Hist[2, 5, 5], Hist[5, 5, 3], Hist[5, 7, 5], = 1, 55, 4
Hist[5, 0, 5], Hist[5, 1, 5] = 39, 39
Hist[Hist == 0] = np.nan

# plot
fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
res = ax.voxels(Hist, cmap='rainbow', norm='log', alpha=0.5)
fig.colorbar(res)
res.colorizer.norm.vmin = 3

@mpl3d_image_comparison(['voxels-scalar-xyz.png'],
tol=0.01, remove_text=False, style='mpl20')
def test_scalar_xyz_input(self):
X, Y, Z = np.meshgrid(np.linspace(0, 5, 10),
np.linspace(0, 5, 10),
np.linspace(0, 5, 10), indexing="ij")
Hist = np.zeros((9, 9, 9))
Hist[2, 5, 5], Hist[5, 5, 3], Hist[5, 7, 5], = 1, 55, 4
Hist[5, 0, 5], Hist[5, 1, 5] = 39, 39
Hist[Hist == 0] = np.nan

# plot
fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
res = ax.voxels(X, Y, Z, Hist, cmap='rainbow', norm='log', alpha=0.5)
fig.colorbar(res)
res.colorizer.norm.vmin = 3


def test_line3d_set_get_data_3d():
x, y, z = [0, 1], [2, 3], [4, 5]
Expand Down
Loading