-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Make pcolor more mesh-like #25027
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
Make pcolor more mesh-like #25027
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm strongly in favor of this, but I have questions about one detail and one matter of strategy.
lib/matplotlib/axes/_axes.py
Outdated
X = np.hstack((X[:, [0]] - dX[:, [0]], | ||
X[:, :-1] + dX, | ||
X[:, [-1]] + dX[:, [-1]])) | ||
if isinstance(X, np.ma.core.MaskedArray): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be more compact you can use if np.ma.isMA(X):
or if np.ma.isMaskedArray(X):
. Both just call isinstance(...)
. If you want to use the explicit isinstance
, you can still make it more compact be deleting the .core
part. The most compact version would be
hstack = np.ma.hstack if np.ma.isMA(X) else np.hstack
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I also cleaned up the handling above this as well.
lib/matplotlib/collections.py
Outdated
@@ -2111,3 +2111,39 @@ def get_cursor_data(self, event): | |||
if contained and self.get_array() is not None: | |||
return self.get_array().ravel()[info["ind"]] | |||
return None | |||
|
|||
|
|||
class PolyQuadMesh(PolyCollection, QuadMesh): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make more sense to reverse the inheritance order, and use "super" below? Is the PolyQuadMesh more like a QuadMesh than a PolyCollection, falling back on the latter only for its draw method, as is already done explicitly below? Or is the current order correct with respect to all inherited methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we actually want most of the methods from PolyCollection because we are drawing Polygons and want that set_paths()
method. We really want the QuadMesh coordinates stored internally is all I think. I reworked the inheritance a bit in the most recent commit.
One thing I need to think about a bit more is how to handle the masking of paths here... len(self._paths()) != coords[:2].size
when there are masked elements. So, we may actually have to overwrite the set_paths()
here to make sure that we are updating the proper facecolors to correspond to the mesh.
Currently, we return a PolyCollection
that has a smaller number of paths, so if you want to update the array with the proper colors you need to account for that resizing yourself. (We do that in Cartopy, so perhaps just porting that upstream here is the way to go)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out my understanding of multiple inheritance was, and probably still is, incomplete. There are arguments in favor of "composition instead of inheritance", and I wonder whether this is an example where using composition--making an internal QuadMesh instance, and referring to its contents explicitly when needed--might be more readable, robust, and maintainable. It would reduce the inheritance to single--much easier to fit in one's brain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or, is there a part of QuadMesh code that should be factored out into a mixin?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a _MeshData
mixin as a separate commit. Let me know your thoughts on how that organization seems now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mixin looks fine to me.
265995a
to
a6615e1
Compare
Discussed this on the call today and there was some interest in pursuing the class mixin idea here to try and make the multiple-inheritance a little bit more manageable. We want the coordinate handling of class QuadCoordinatesMixin
def __init__(self, coordinates):
self._coordinates = coordinates
def set_array(self, A):
# current quadmesh array validation logic for the shapes
def get_coordinates(self):
return self._coordinates
# ... other (static)methods like get/set_paths()?
class QuadMesh(QuadCoordinatesMixin, Collection):
# override draw/path methods from Collection
class PolyQuadMesh(QuadCoordinatesMixin, PolyCollection):
# override draw/path methods from PolyCollection to utilize 2D coordinates from the mixin Other thoughts and comments on the design are more than welcome! |
This seems to be in draft state, but feel free to move back... |
1ed9d14
to
d017def
Compare
d017def
to
ccb3966
Compare
I tried this out with Cartopy, and it breaks the usage there because Cartopy expects a masked array to be compressed and subsequently passes an array to import matplotlib.pyplot as plt
import numpy as np
arr = np.ma.array(np.arange(12).reshape(3, 4), mask=[[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 1, 0, 1]])
fig, ax = plt.subplots()
pc = ax.pcolor(arr)
pc.set_array(np.ones(6))
plt.show() With v3.7 and With this branch we get
There is also handling in Cartopy that expects to get the compressed array out of |
I'm not sure if the "compressed" nature of the output is a feature or a bug... A similar "bug" for scatter changing the size of the array and the colors #9891. In Cartopy, we decided to work with the compression, but I really don't think that is the right way to do it because you have to keep track of everything externally and remember how you compressed your masks each time. I've just pushed up a new commit that changes the number of polys based on the masked array passed into One thing this does bring up though is that there is an inconsistency between the I checked seaborn and Astropy and didn't see any usage of |
I think the compression is a misfeature, an unfortunate historical artifact, so the question is not whether to keep it in the long run but whether to try to support it through a deprecation cycle. I haven't looked closely, but it seems like temporarily continuing to allow the compressed 1-D input (with a deprecation warning) could be done now that you are saving the mask. |
I completely agree that it's better without the compression. It's just the transition that might be a problem. This will break users of |
Thanks for both of your suggestions! I added a deprecation warning for when the compressed size of the array is used. We still defer to super's set_array() by making our own filled array, so this special casing just needs to be removed in a couple of releases. |
I'm not at all sure it would be worthwhile, but it would be possible to give the |
I agree it would be nicer for consistency and that is actually the way it is on the current main branch after #24638. But, a few downstream packages test against So the current state of this PR would have array/data as 2d arrays, but facecolors and edgecolors (and all other collection properties) are always flattened. I think this is reasonable for taking incremental steps forward here and we can approach turning more collection properties into 2d meshes in future PRs if we think it is worthwhile. |
7486412
to
f182fc7
Compare
super().__init__(coordinates=coordinates, shading=shading) | ||
Collection.__init__(self, **kwargs) | ||
|
||
self._antialiased = antialiased |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using a different approach to handling the antialiased
kwarg in #26062. See lines 1991-2007 there in the modified collections.py file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I'm happy to rebase/update if that goes in first. This is just using the same logic that was there before. I'm trying to keep this PR focused on just the substantial rewrites and leave other updates for follow-on PRs.
29709b2
to
890afaf
Compare
@jklymak I added two new notes, one for the deprecation and one trying to give a description of the |
@@ -23,11 +23,11 @@ | |||
"lib/matplotlib/colorbar.py:docstring of matplotlib.colorbar:1" | |||
], | |||
"matplotlib.axes.Axes.patch": [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this file getting hit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc build was complaining about inherited private members (if I remember correctly). This file is automatically generated, so there are also other missing references that have been updated unrelated to this PR:
Lines 184 to 185 in f588d2b
# change this to True to update the allowed failures | |
missing_references_write_json = False |
``PolyQuadMesh`` is a new class for drawing quadrilateral meshes | ||
---------------------------------------------------------------- | ||
|
||
``plt.pcolor()`` now returns a ``PolyQuadMesh`` class. It is a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
link to the class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's helpful to point out what used to be returned and maybe any major usage changes we expect the user to see (if any)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated it with references to what used to be returned PolyCollection
, and tried to indicate that we are subclassing PolyCollection
still, so things should hopefully be transparent to the end-user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.pcolormesh.html#differences-pcolor-pcolormesh needs to be modified, and that is probably a good place to explain a bit more about what is going on here.
Thanks for pointing that out, I updated that with a new sentence at the end and updated the mentions of PolyCollection to point to PolyQuadMesh now. |
@@ -0,0 +1,4 @@ | |||
``PolyQuadMesh.set_array()`` with compressed values | |||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||
... is deprecated. Instead, pass the full 2D array of values |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is note isn't useful to users as PolyQuadMesh is new. I think you need something like the object returned from pcolor wants 2D values passed to set_array. "Compressed values" isn't right either. I think you need to be more loquacious here. Maybe:
Object returned by `pcolor` has changed to `PolyQuadMesh`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The old object was a `PolyCollection` with flattened vertices and array data.
The new `set_array` method on `PolyQuadMesh` requires a 2D array rather than a flattened
array. The old flattened data is still accepted, but deprecated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I expanded upon this. But, just to note that flattened data can be input still (same as QuadMesh), the only thing deprecated is the part where a mask was used and previously compressed and lost information about which polygons were present.
lib/matplotlib/collections.py
Outdated
return ~(mask | self._original_mask) | ||
# Take account of the array data too, temporarily avoiding | ||
# the compression warning and resetting the variable after the call | ||
temp_deprecated_compression = self._deprecated_compression |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use _setattr_cm
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, much nicer, thanks! Just pushed that update. Yes, I believe this is ready to go.
I think this is ready to go? |
This adds a new collection PolyQuadMesh that is a combination of a PolyCollection and QuadMesh, storing all of the coordinate data and arrays through QuadMesh, but drawing each quad individually through the PolyCollection.draw() method. The common mesh attributes and methods are stored in a private mixin class _MeshData that is used for both QuadMesh and PolyQuadMesh. Additionally, this supports RGB(A) arrays for pcolor() calls now. Previously to update a pcolor array you would need to set only the compressed values. This required keeping track of the mask by the user. For consistency with QuadMesh, we should only accept full 2D arrays that include the mask if a user desires. This also allows for the mask to be updated when setting an array.
@@ -818,12 +823,12 @@ def test_autolim_with_zeros(transform, expected): | |||
np.testing.assert_allclose(ax.get_xlim(), expected) | |||
|
|||
|
|||
def test_quadmesh_set_array_validation(): | |||
def test_quadmesh_set_array_validation(pcfunc): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional, but I'd rather put an explicit parametrization on the handful of functions that use this. Because of the indirections, it's hard to see what's going on with the fixture.
def test_quadmesh_set_array_validation(pcfunc): | |
@pytest.mark.parametrize(pcfunc, ["pcolormesh", "pcolor"]) | |
def test_quadmesh_set_array_validation(pcfunc): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong preference either, probably slightly lean towards fixture as it currently is. I am away from a computer for a week though, so if someone wants to make updates to this feel free to push to my branch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets merge and follow up if anything is problematic
@jklymak It looks like this is failing CI on the main branch. Not immediately obvious to me what the issue is. |
Can you elaborate on what broke |
The problem is that the x- and y-values in the test have alternating columns that are all masked, so become completely masked arrays within matplotlib/lib/matplotlib/axes/_axes.py Lines 5821 to 5837 in fff2a79
I guess the question is whether the test represents a realistic use-case. |
I'm confused why this was passing CI before? |
Having slept on it, I realise it shouldn't have. I have opened #26273 to modify the test. |
PR Summary
This is a follow-on from #24638
and makes pcolor more similar to pcolormesh with 2D arrays of x/y/c and just uses a different draw implementation. This should maintain backwards compatibility as we are subclassing the PolyCollection still to use that draw method.
pcolor
: draw every quad individually as Polygonspcolormesh
: draw all quads together (depending on backend implementation)Basically, this is an attempt to allow all inputs/getters/setters to be able to use 2D meshes rather than requiring raveling arrays and needing to remember when to use 1D/2D quantities for various functions.
closes #25770
PR Checklist
Documentation and Tests
pytest
passes)Release Notes
.. versionadded::
directive in the docstring and documented indoc/users/next_whats_new/
.. versionchanged::
directive in the docstring and documented indoc/api/next_api_changes/
next_whats_new/README.rst
ornext_api_changes/README.rst