Skip to content

Mplot3d masked surface #18114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
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
16 changes: 13 additions & 3 deletions lib/mpl_toolkits/mplot3d/axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -1624,8 +1624,8 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None,
"Z contains NaN values. This may result in rendering "
"artifacts.")

# TODO: Support masked arrays
X, Y, Z = np.broadcast_arrays(X, Y, Z)
mask = np.ma.getmaskarray(Z)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if X or Y have a mask? My guess is we should take the union of all the masks to compute an 'overall' mask here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I see that pcolormesh doesn't accept masked X and Y values (but it looks like pcolor might). I've not tested contour but it looks like masks are ignored on X and Y values?

X, Y, Z, mask = np.broadcast_arrays(X, Y, Z, mask)
rows, cols = Z.shape

has_stride = 'rstride' in kwargs or 'cstride' in kwargs
Expand Down Expand Up @@ -1680,6 +1680,10 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None,
[cbook._array_patch_perimeters(a, rstride, cstride)
for a in (X, Y, Z)],
axis=-1)
masked = np.any(
cbook._array_patch_perimeters(mask, rstride, cstride), axis=1
)
polys = polys[~masked]
else:
# evenly spaced, and including both endpoints
row_inds = list(range(0, rows-1, rstride)) + [rows-1]
Expand All @@ -1693,6 +1697,12 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None,
cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1])
for a in (X, Y, Z)
]
# If any of the perimeters are masked, then skip the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you test this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added another test for this based on the example above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this behavior desirable? If everything but the perimeter of the polygon is masked, do we really want to draw the polygon? Some other choices would be:

  • Draw the polygon only if none of the contained elements are masked
  • Draw the polygon if less than half (or a user-configurable parameter of the contained elements are masked
  • Set the opacity of the polygon to be proportional to the maskedness
  • Attempt to precisely outline the masked regions

Whatever you decide on, I think it's important to describe the choice in the docstring.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a good question and I'm happy to have more input on this topic.

My line of thought is that as long as the perimeters of the polygons don't contain masked data, then it is ok to draw the polygon. If some data inside of the polygon is masked but is strided over, then it is not used to make the plot anyway so we aren't "revealing" masked data in the plot. I understand this might cause a bit of confusion due to the nature of the xstride/xcount settings.

Draw the polygon if less than half (or a user-configurable parameter of the contained elements are masked

and

Set the opacity of the polygon to be proportional to the maskedness

These seem like "advanced" use cases to me but happy to hear if others think differently. Though I still don't think anyone would want the perimeter of a polygon to contain masked data. As far as I can tell, no one has requested this kind of feature before.

Attempt to precisely outline the masked regions

Probably the ideal solution, if any one has suggestions how the can be accomplished then I'm happy to hear them!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I want input from @ianthomas23 about how he handled masked data for the contouring functions. I think it would make sense to be consistent with those rules.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, the concepts don't perfectly overlap because the contouring algorithms don't have a concept of striding, but I am still interested in his input.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe striding does anything other than a simple decimation at the moment? If so I don't see why it would do something fancy for a mask. You could easily have the same complaint about a sharp spike that gets strided over. If folks want to properly decimate their data they can do so before passing to mpl.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe striding does anything other than a simple decimation at the moment?

It doesn't decimate the space fully, as the perimeter is still drawn at full precision. But looking closely, the colormap color is computed based only on the perimeter, so I guess the implemented behavior is consistent.

# polygon
if np.any(cbook._array_perimeter(
mask[rs:rs_next+1, cs:cs_next+1]
)):
continue
# ps = np.stack(ps, axis=-1)
ps = np.array(ps).T
polys.append(ps)
Expand Down Expand Up @@ -1730,7 +1740,7 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None,
polyc.set_facecolors(colset)

self.add_collection(polyc)
self.auto_scale_xyz(X, Y, Z, had_data)
self.auto_scale_xyz(X, Y, Z[~mask], had_data)

return polyc

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.
26 changes: 26 additions & 0 deletions lib/mpl_toolkits/tests/test_mplot3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,32 @@ def test_surface3d_shaded():
ax.set_zlim(-1.01, 1.01)


@mpl3d_image_comparison(['surface3d_masked.png'])
def test_surface3d_masked():
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
X = np.arange(1, 10, 1)
Y = np.arange(1, 10, 1)
X, Y = np.meshgrid(X, Y)
Z = X**3 + Y**3 - 500
Z = np.ma.masked_array(Z, Z < 0)
ax.plot_surface(X, Y, Z, lw=0, antialiased=False)
ax.set_zlim(-1, 1000)


@mpl3d_image_comparison(['surface3d_masked_strides.png'])
def test_surface3d_masked_strides():
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
X = np.linspace(-1, 1)
Y = np.linspace(-1, 1)
X, Y = np.meshgrid(X, Y)
Z = X**2 + Y**2
Z[30:40, 10:20] = 0.0
Z = np.ma.masked_equal(Z, 0.0)
ax.plot_surface(X, Y, Z, lw=0, antialiased=False, rstride=3, cstride=3)


@mpl3d_image_comparison(['text3d.png'], remove_text=False)
def test_text3d():
fig = plt.figure()
Expand Down