Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork7.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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
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 useif np.ma.isMA(X):
orif np.ma.isMaskedArray(X):
. Both just callisinstance(...)
. If you want to use the explicitisinstance
, 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 thatset_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 theset_paths()
here to make sure that we are updating the proper facecolors to correspond to the mesh.
Currently, we return aPolyCollection
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.
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 classQuadCoordinatesMixindef__init__(self,coordinates):self._coordinates=coordinatesdefset_array(self,A):# current quadmesh array validation logic for the shapesdefget_coordinates(self):returnself._coordinates# ... other (static)methods like get/set_paths()?classQuadMesh(QuadCoordinatesMixin,Collection):# override draw/path methods from CollectionclassPolyQuadMesh(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... |
rcomer commentedApr 29, 2023 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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 importmatplotlib.pyplotaspltimportnumpyasnparr=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. |
rcomer commentedMay 1, 2023 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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
tof182fc7
Comparesuper().__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 theantialiased
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
to890afaf
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 inf588d2b
# 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 whatused 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 returnedPolyCollection
, and tried to indicate that we are subclassingPolyCollection
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 thinkhttps://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 ofa PolyCollection and QuadMesh, storing all of the coordinate dataand arrays through QuadMesh, but drawing each quad individuallythrough the PolyCollection.draw() method.The common mesh attributes and methods are stored in a privatemixin 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 thecompressed values. This required keeping track of the mask by the user.For consistency with QuadMesh, we should only accept full 2D arraysthat include the mask if a user desires. This also allows for themask 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.
deftest_quadmesh_set_array_validation(pcfunc): | |
@pytest.mark.parametrize(pcfunc, ["pcolormesh","pcolor"]) | |
deftest_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
scottshambaugh commentedJul 6, 2023 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@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 |
scottshambaugh commentedJul 6, 2023 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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 infff2a79
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. |
Uh oh!
There was an error while loading.Please reload this page.
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