Skip to content
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

ENH: enable passing RGB(A) to polormesh #2166

Merged
merged 5 commits into from
Jul 14, 2023

Conversation

rcomer
Copy link
Member

@rcomer rcomer commented Apr 6, 2023

Rationale

Since Matplotlib v3.7.0, pcolormesh accepts RGBA arrays
https://matplotlib.org/stable/users/prev_whats_new/whats_new_3.7.0.html#pcolormesh-accepts-rgb-a-colors

Cartopy just needs a small tweak to be able to pass these down, and we can adapt this StackOverflow question to give

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(100)

x = np.arange(10, 20)
y = np.arange(0, 10)
x, y = np.meshgrid(x, y)

img = np.random.randint(low=0, high=255, size=(10, 10, 4)) / 255

projection = ccrs.Mollweide()
transform = ccrs.PlateCarree()
ax = plt.axes(projection=projection)
ax.set_global()
ax.coastlines()

ax.pcolormesh(np.linspace(0, 120, 11), np.linspace(0, 80, 11), img, transform=transform)

plt.show()

test

Closes #2156 and I think also resolves #2045 and resolves #2171, as the request for set_array to handle None seems to be a workaround that is not needed since mpl 3.7. I also note that the None handling is not actually documented in QuadMesh.set_array.

This also adds test coverage for and fixes #2208.

I'm not sure of the best way to test this. Does it just need a smoke test of a pcolormesh plot with suitable data?

Implications

@rcomer rcomer marked this pull request as draft April 6, 2023 15:24
@rcomer rcomer marked this pull request as ready for review April 6, 2023 15:37
@greglucas
Copy link
Contributor

Does this also need an update of the GeoQuadMesh collection to handle the set_array() case with wrapping?

One thought for testing this would be to duplicate one of the current tests but use RGBA values from cmap(norm(arr)) values, and then pass that into the pcolormesh(rgba_arr) call and verify it is the same as pcolormesh(arr, cmap=cmap, norm=norm). It would be good to do this for a case with wrapping as well to make sure we are handling that broadcasting properly too.

@rcomer
Copy link
Member Author

rcomer commented Apr 12, 2023

Thanks @greglucas, I took a look at the wrapping code and this is clearly more complicated than I first thought! I think I can see how to proceed but not sure how soon I will get to it, so I’ll put this in draft for now.

@rcomer rcomer marked this pull request as draft April 12, 2023 18:11
@rcomer
Copy link
Member Author

rcomer commented Apr 13, 2023

Ah, I had not twigged that this uses pcolor underneath.

pcolor_data = np.ma.array(np.zeros(C.shape),
mask=~mask)
pcolor_col = self.pcolor(coords[..., 0], coords[..., 1],
pcolor_data, zorder=zorder,
**kwargs)
# Now add back in the masked data if there was any
full_mask = ~mask if C_mask is None else ~mask | C_mask
pcolor_data = np.ma.array(C, mask=full_mask)
# The pcolor_col is now possibly shorter than the
# actual collection, so grab the masked cells
pcolor_col.set_array(pcolor_data[mask].ravel())

Does this mean that this basically can't work without changes to pcolor in Matplotlib?

@greglucas
Copy link
Contributor

Ahh, you might be right that we can't quite handle RGBA in the general case yet. I do have an open PR over in MPL to make pcolor more mesh-like, which may help with the pcolor RGBA support as well if you want to try that combination out... matplotlib/matplotlib#25027

@rcomer
Copy link
Member Author

rcomer commented Apr 22, 2023

OK, I see that matplotlib/matplotlib#25027 stops pcolor from ravelling arrays and compressing masked arrays, which definitely helps. I think we will then need a further change in mpl to get pcolor to handle RGB(A). Part of me is tempted to branch off your branch to see if I can get it working locally, but I think it might be more sensible to wait till matplotlib/matplotlib#25027 is merged.

@greglucas
Copy link
Contributor

Part of me is tempted to branch off your branch to see if I can get it working locally, but I think it might be more sensible to wait till matplotlib/matplotlib#25027 is merged.

Yeah, good call, I wouldn't put too much effort into this yet until that has some consensus on whether the PR is desired or if it goes in a different direction. Feel free to leave comments/review over there as well if you have thoughts for how to improve it!

@rcomer rcomer force-pushed the pcolormesh-rgba branch 3 times, most recently from 5c17100 to 7483a21 Compare May 2, 2023 09:13
@rcomer rcomer changed the title FIX: enable passing RGB(A) to polormesh ENH: enable passing RGB(A) to polormesh May 2, 2023
@rcomer
Copy link
Member Author

rcomer commented May 2, 2023

OK this now works with your branch for the cases I have tested. There are some loose ends to tidy up:

  • Currently this works with RGBA but not RGB, but I don't think it will take much to address that.
  • GeoQuadMesh.set_array still only works for 1D arrays.
  • Need to look at the get_array method.
  • Possibly adapt some more tests for better coverage.

But I'm happy I have a working POC, and it probably doesn't make sense to polish this too much until matplotlib/matplotlib#25027 makes it into a release candidate.

There were a couple of new test failures when I picked up the latest commit to your branch. I've not looked into those yet, but they were both failing on the assertion of expected output which doesn't always mean something has gone wrong!

@rcomer
Copy link
Member Author

rcomer commented Jun 5, 2023

Thanks to @kylehofmann for the latest commit, which handles the RGB case that I had previously neglected.

@rcomer rcomer force-pushed the pcolormesh-rgba branch 3 times, most recently from 24ed4a8 to 1031649 Compare June 5, 2023 12:42
@rcomer
Copy link
Member Author

rcomer commented Jun 7, 2023

I think all the functionality is there now. A few things are not yet tested:

  • GeoQuadMesh.get_array for RGB(A)
  • [maybe not] GeoQuadMesh.set_array when the input is different to before, e.g. if the original pcolormesh call used a 2D array but now we're passing RGBA. This works in Matplotlib and, from a quick check, seems to work here too. Edit: actually it doesn't work in matplotlib if you initially call pcolormesh with RGBA and then try to set a 2D array - it updates but you do not get the same plot that you would have if you passed the 2D array to begin with. I don't even know what the use-case would be for this so maybe we should leave it until and unless someone asks for it.
  • GeoQuadMesh.set_array when there is no wrapping. I guess this didn't need to be tested before as previously it just passed straight to QuadMesh.set_array for this case.

@kylehofmann I'm sorry but I changed my mind about supporting the masked case for RGB(A). As far as I can tell Matplotlib's pcolormesh just ignores masks on RGB(A) arrays, and I think it makes sense to be consistent with that. Dropping support for that case also made the code simpler.

@greglucas
Copy link
Contributor

greglucas commented Jun 7, 2023

As far as I can tell Matplotlib's pcolormesh just ignores masks on RGB(A) arrays, and I think it makes sense to be consistent with that

I agree, the masked RGBA case should follow Matplotlib. It explicitly states the mask gets ignored in the docstring of to_rgba:
https://matplotlib.org/stable/api/cm_api.html#matplotlib.cm.ScalarMappable.to_rgba

With that being said, I think it would be fairly straight forward to implement handling of the masked arrays if we wanted to make them fully transparent? Something like the following patch.

index daefbe08e9..0c4a99496a 100644
--- a/lib/matplotlib/cm.py
+++ b/lib/matplotlib/cm.py
@@ -493,6 +493,9 @@ class ScalarMappable:
                 else:
                     raise ValueError("Image RGB array must be uint8 or "
                                      "floating point; found %s" % xx.dtype)
+                # Account for any masked entries in the original array
+                if np.ma.is_masked(x):
+                    xx[np.any(np.ma.getmaskarray(x), axis=2), 4] = 0
                 return xx
         except AttributeError:
             # e.g., x is not an ndarray; so try mapping it

@rcomer
Copy link
Member Author

rcomer commented Jun 7, 2023

@greglucas thanks, I was just wondering about that. With your mpl branch, pcolor does respect the mask for the RGBA cases I tried. So if pcolormesh did as well then we could probably simplify the logic here and just use masks for all cases rather than messing with the alpha channel.

@kylehofmann
Copy link
Contributor

@rcomer: No offense taken! Consistency with matplotlib is important, and as you say, the code is simpler. (A big plus!) My only suggestion is to change RBG to RGB.

@@ -578,6 +633,8 @@ def test_pcolormesh_diagonal_wrap():
assert hasattr(mesh, "_wrapped_collection_fix")


@pytest.mark.skipif(MPL_VERSION.release[:2] >= (3, 8),
reason='redundant from mpl v3.8')
Copy link
Member Author

@rcomer rcomer Jun 13, 2023

Choose a reason for hiding this comment

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

Have I understood this right? I think this test relates to the need to initially call pcolor with an array of zeros, which will no longer be necessary with the new PolyQuadMesh array handling.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that the number of paths should still be short in the new version, so I think this should still be tested. i.e. we only want to draw the small number of cells, we don't want to draw all pcolor cells and make them invisible.

@rcomer
Copy link
Member Author

rcomer commented Jun 13, 2023

Thanks to matplotlib/matplotlib#26096, Matplotlib's pcolormesh will respect the masks on RGB(A) data from v3.8. So I have simplified the handling to always use masks and also reinstated the masked RGB tests.

@rcomer rcomer force-pushed the pcolormesh-rgba branch 2 times, most recently from 459bbb9 to afbb531 Compare July 13, 2023 10:45
@rcomer rcomer marked this pull request as ready for review July 13, 2023 10:46
@rcomer
Copy link
Member Author

rcomer commented Jul 13, 2023

I think this one is finally ready for review!

@rcomer rcomer force-pushed the pcolormesh-rgba branch 2 times, most recently from 4118598 to 7695be5 Compare July 13, 2023 13:36
Copy link
Contributor

@greglucas greglucas left a comment

Choose a reason for hiding this comment

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

This is really nice work @rcomer! Thank you for tackling this because there are a lot of paths to keep track of with the different versions hanging around everywhere. Eventually, this will slim down the code ;)

@@ -578,6 +633,8 @@ def test_pcolormesh_diagonal_wrap():
assert hasattr(mesh, "_wrapped_collection_fix")


@pytest.mark.skipif(MPL_VERSION.release[:2] >= (3, 8),
reason='redundant from mpl v3.8')
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that the number of paths should still be short in the new version, so I think this should still be tested. i.e. we only want to draw the small number of cells, we don't want to draw all pcolor cells and make them invisible.

@rcomer
Copy link
Member Author

rcomer commented Jul 14, 2023

I think that the number of paths should still be short in the new version, so I think this should still be tested. i.e. we only want to draw the small number of cells, we don't want to draw all pcolor cells and make them invisible.

That test now doesn't get any paths back, so I have updated to assert that from v3.8. But I think I am still not understanding quite right.

@greglucas
Copy link
Contributor

I think that the number of paths should still be short in the new version, so I think this should still be tested. i.e. we only want to draw the small number of cells, we don't want to draw all pcolor cells and make them invisible.

That test now doesn't get any paths back, so I have updated to assert that from v3.8. But I think I am still not understanding quite right.

I think you actually had a better grasp on this than I did! I think you can actually skip this after 3.8 like you had before because it doesn't make sense. Let me try and write down what I think is happening and see if you agree.

This test passes in one all-nan cell that spans the wrap-line. That actually becomes two wrapped cells in the pcolor-collection. Before MPL3.8 we wanted those cells to show up because we needed to be able to update them later. After MPL3.8 the PolyQuadMesh collection takes care of the shrink/expansion of the paths automatically for us. So, with nan's it is empty, but if we updated the mesh we'd expect two paths to be present (PolyQuadMesh.set_array(np.array([[1]])) should create those two paths I think). I suppose that case of setting a new data array could be added as an extra assertion as well if that makes sense...

@rcomer
Copy link
Member Author

rcomer commented Jul 14, 2023

Yes, I think we're on the same page. It makes sense to me to always make sure the wrapped cells can be updated, so I have added that extra assertion.

@greglucas greglucas merged commit de7b307 into SciTools:main Jul 14, 2023
@rcomer
Copy link
Member Author

rcomer commented Jul 14, 2023

Thanks for all your guidance and mpl updates @greglucas!

@rcomer rcomer deleted the pcolormesh-rgba branch July 14, 2023 14:11
@QuLogic QuLogic added this to the 0.22 milestone Jul 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants