GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Multivar colorbar by trygvrad · Pull Request #31214 · matplotlib/matplotlib Conversation
Contributor
This PR relates to Bivariate and Multivariate Colormapping, specifically #30527 Bivariate and multivariate colorbar. This draft includes only import matplotlib.pyplot as plt import numpy as np fig, axes = plt.subplots(2, 2, figsize = (10, 6)) axes = axes.ravel() locs = ['left', 'right', 'top', 'bottom'] im = np.random.random((2, 3, 4)) for i, loc in enumerate(locs): cim = axes[i].imshow(im, cmap='BiOrangeBlue') fig.bivar_colorbar(cim, location=loc, use_gridspec=False, fraction=0.3)
Note that the first axis is the y-axis and the second is the x-axis when the bivariate colormap is shown, following the implementation in #28454 @story645, @timhoffm, @ksunden
I would really appreciate some feedback on 3. before I start making tests and typing :) The placement of the bivariate colorbar uses nearly identical code to the normal colorbar. I assume this is what we want. [we will need more complicated placement for If you want to look at the code, I advise you to look at the commit 3cf2df9, rather than the PR, as this is built on top of #30597 – Multivar imshow.
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page. All reactions
Member
All reactions
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page.
|
There was an error while loading. Please reload this page.
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Also apparently I didn't read 3 correctly 🤦♀️ but I agree w/ @timhoffm's proposal d) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
3cf2df9 to
701c4b2
Compare
|
This is progressing well, thank you for all the feedback so far. In the draft, I am using self._image = self.ax.imshow( self.colorizer.cmap.lut, origin='lower', extent=(0, 1, 0, 1), transform=self.ax.transAxes, interpolation='nearest', alpha=self.alpha, ) to display the bivariate colorbar. The This can for example be illustrated by: norm = mpl.colors.MultiNorm(['log', 'log']) ca = mpl.colorizer.Colorizer('BiOrangeBlue', norm) fig, ax = plt.subplots() cbar = fig.bivar_colorbar(ca, cax = ax, ticklocations=['left', 'bottom']) norm.norms[1].vmin = 0.1 norm.norms[1].vmax = 10 cbar.xaxis.set_ticks([1, 10, 100]) print(ca.norm.vmin, ca.norm.vmax, cbar.ax.get_xlim() ) # (None, 0.1), (None, 10.0), (0.1, 100.0) Where the xlim is changed but vmax on the norm is not. My thinking is that any action taken by the user which would change the limits on the axis by operations on the axis, and not by setting the vmin or vmax, should raise an error of the form: raise ValueError("Changing the limits of this axis must be done " "using the norm attached to the bivariate colorbar.") I can do this by connecting to Does this seem like a reasonable solution to you? When |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
701c4b2 to
f19b42c
Compare
|
Wouldn't it make sense to use pcolormesh instead of image so users can change the scale if they want to? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Thank you for the feedback @jklymak, but I think we can work with imshow in this case. The user should be able to change the scale on the norm, which will change the axis, but the bivariate colormap should render the same. Where the only change is that a LogNorm is used to when visualizing the GDP per capita in the lower plot. I do not think there is a need to set a scale different from that of the norm. (additionally: I would like to avoid pcolormesh if possible, because if saved as an SVG, each tile in the mesh gets stored as a polygon, which makes it almost impossible to edit the svg later. I personally find the pipeline matplotlib→inkscape to be extremely powerful, and would like that to be possible here.) The plots are based on https://trygvrad.github.io//mpl_docs/Bivariate%20colormaps.html |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
People do ask to set the scale differently from the norm and they will be able to, but will get incorrect scaling of the colors. Pcolormesh can be rasterized, so you can avoid the polygon problem in svg/pdf. Note normal colorbar uses pcolormesh. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@jklymak thank you for the specification, I will see if I can make pcolormesh behave :) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
f19b42c to
22e122d
Compare
|
@jklymak I am looking at what we need for layouts for multivar colorbar. import matplotlib.pyplot as plt import numpy as np im_A = np.arange(200)[np.newaxis, :]*np.ones((200, 200)) im_B = np.arange(200)[:, np.newaxis]*np.ones((200, 200)) im_C = 0.9*im_A + 0.9*im_B im_A = np.sin(im_A**0.5)**2 im_B = np.sin(im_B**0.5)**2 im_C = np.sin(im_C**0.5)**2 fig, axes = plt.subplots(2, 2, figsize = (10, 6)) axes = axes.ravel() locs = ['left', 'right', 'top', 'bottom'] im = (im_A, im_B, im_C) for i, loc in enumerate(locs): cim = axes[i].imshow(im, cmap='3VarAddA') cb = fig.multivar_colorbar(cim, location=loc, use_gridspec=False, fraction=0.3)
Where In terms of layout, I want the user to have the option to organize the colorbars in a different way: cb = fig.multivar_colorbar(..., n_major=2)
or cb = fig.multivar_colorbar(..., n_major=1)
For the prototype I am basing this on bbox.splitx() and bbox.splity() [gridspec=False]. In the existing code there is make_axes make_bivar_axes make_multivar_axes make_axes_gridspec make_bivar_axes_gridspec make_multivar_axes_gridspec [not yet implemented] Because there is a lot of overlap, I have created a number of private helper functions, which (hopefully) avoids duplicate code. @jklymak does this make sense to you in terms of the layout? I have not yet started looking at |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I'll need to look at what you are proposing. Ideally I think your colorbars would be encapsulated inside something constrained layout thinks is a "colorbar" that is should put into the colorbar slot in the layout manager. After that the "colorbar" can look as you like. If you want the colorbars to be placed inside of the allotted area by the layout manager I think that will be more difficult Overall I would not suggest making a gridspec version of the colorbars. I suppose those allow a non optimal but still legible layout. I can try and look closer this weekend. If you had some example scripts for me to look at that'd make things easier. Apologies if they are already in this PR |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
which looks fixable? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
This might be the silliest thing but can we maybe flip the names to colorbar_{bi, multi}? I think would be especially easier for auto complete. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
:D
@jklymak This is a good idea, and I will try to make it happen, but I am not super familiar with this part of the code. What kind of object do you think that something should be, because I don't think it should be an axis in an of itself, but it should probably show up in def get_children(self): """Get a list of artists contained in the figure.""" return [self.patch, *self.artists, *self._localaxes, *self.lines, *self.patches, *self.texts, *self.images, *self.legends, *self.subfigs]
Thanks for checking this, I will fix this and add some tests that cover these cases.
@story645 I think there is significant merit to this argument :) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@jklymak @timhoffm @story645 I think this PR is ready for review. I will start making PRs for #30529 and #30528 . These should be easier as they don't involve new API |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
449bd74 to
65db055
Compare
|
Now that #30597 is merged, I have rebased this onto main, so that it can be reviewed. @jklymak do you have time to review this, both in terms of functionality and the code? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I hope its ok if I ask you to take a look at this @rcomer , I was told by @story645 that you have done some PRs that interact with the layout engine. This PR is the last code-heavy PR required to get multivariate and bivariate color mapping to work, a project that has been ongoing since 2024 Bivariate and Multivariate Colormapping. We are on the last mile, but I need someone to review this PR, preferably someone with experience with the layout engine. In short, this PR allows you to make the 2d colorbar for the following plot, using the new If you would be willing to review this PR that would be very much appreciated 🥇 The first question is:
Other than that I think it is mostly a question of if there are any more things/edge cases we should test. Let me know if you have any questions :) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Hi @trygvrad, I'm sorry but I do not have the capacity to be able to take this on, at least not for the next couple of months. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Thank you for the quick reply @rcomer @story645 I wanted to go through this in the meeting today, but I don't have the mental capacity to attend today. Could you put it on the agenda for next week? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
No problem, will do! |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
multivar_colorbar: updates after rebase multivar_colorbar: fix for docs
2e57ffa to
a78ac46
Compare
a78ac46 to
c353303
Compare
|
This is a summary of what this PR does, so that it is easier to review: New top level functionality
New classes
Axes creation helpers and organization
Minimal example import matplotlib.pyplot as plt import numpy as np im_A = np.arange(12).reshape((3, 4)) % 4 im_B = np.arange(12).reshape((3, 4)) % 3 im_C = np.arange(12).reshape((3, 4)) % 5 fig, axes = plt.subplots(1, 2, figsize=(12, 4)) # ------- colorbar_multivar ------------ mim = axes[0].imshow((im_A, im_B, im_C), cmap='3VarAddA') cbs = fig.colorbar_multivar(mim) cbs[0].set_label('A') mim.norm.vmin = (-1, -1, -1) # ------- colorbar_bivar ------------ mim = axes[1].imshow((im_A, im_B), cmap='BiOrangeBlue') cbs = fig.colorbar_bivar(mim) cbs.set_ylabel('A') mim.norm.vmin = (-1, -1)
Positioning example colorbar_multivar() im_A = np.arange(200)[np.newaxis, :]*np.ones((200, 200)) im_B = np.arange(200)[:, np.newaxis]*np.ones((200, 200)) im_C = 0.9*im_A + 0.9*im_B im_A = np.sin(im_A**0.5)**2 im_B = np.sin(im_B**0.5)**2 im_C = np.sin(im_C**0.5)**2 fig, axes = plt.subplots(2, 2, figsize = (10, 6), dpi = 100, layout="constrained") axes = axes.ravel() locs = ['right', 'left', 'top', 'bottom'] im = (im_A, im_B, im_C) for i, loc in enumerate(locs): cim = axes[i].pcolormesh(im, cmap='3VarAddA', vmax=(1, 1, 1)) cb = fig.colorbar_multivar(cim, location=loc, n_major=2) cb[0].set_label('A') cb[1].set_label('B') cb[2].set_label('C') fig.show()
Positioning example colorbar_bivar() im_A = np.arange(200)[np.newaxis, :]*np.ones((200, 200)) im_B = np.arange(200)[:, np.newaxis]*np.ones((200, 200)) im_A = np.sin(im_A**0.5)**2 im_B = np.sin(im_B**0.5)**2 fig, axes = plt.subplots(2, 2, figsize = (10, 6), dpi = 100, layout="constrained") axes = axes.ravel() locs = ['right', 'left', 'top', 'bottom'] im = (im_A, im_B) for i, loc in enumerate(locs): cim = axes[i].pcolormesh(im, cmap='BiOrangeBlue', vmax=(1, 1)) cb = fig.colorbar_bivar(cim, location=loc, fraction = 0.3) cb.set_ylabel('A') cb.set_xlabel('B') fig.show()
The last two examples use |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I'll do incremental reviews, because it's too large to do it in one go.
@trygvrad You may wait for the full review or already react to the comments. If you want to add changes, for now please only add new commits (no rebase or squash) so that the state of today can still be created by selecting the commits up to today. Otherwise it's hard for me to track what I have reviewed and which changes are implemented since then.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| def __init__( | ||
| self, ax, mappable, | ||
| *, | ||
| alpha=None, |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I know we have this in Colorbar. But do you understand what this is used for? It seems like a semi-transparent colorbar is quite unhelpful because it distorts the colors of the colormap. If we don't see a use case, I'd rather not include this to keep things simple.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
That's a good point, I agree 👍
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| Unlike `Colorbar`, `BivarColorbar` does not support | ||
| customizing the ticks, and ticks must be customized on the axes instead. |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
A bit more specific:
BivarColorbar uses a regular Axes as underlying Artist.
UnlikeColorbar,BivarColorbardoes not support
customizing the ticks via a high-level API. Instead, customize ticks
directly on the underlying AxesBivarColorbar.ax.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| self.ax = ax | ||
| self.location = location | ||
| if isinstance(mappable, mpl.colorizer.Colorizer): | ||
| mappable = mcolorizer.ColorizingArtist(mappable) | ||
|
|
||
| self.mappable = mappable | ||
| self.aspect = aspect | ||
| self.colorizer = mappable.colorizer |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Should they all public? Which of these are safe to modify?
If no read access is needed, we could make them private. If reading is needed but writing should not be allowed, we should make the variable private and expose it though a public property.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This is a good point, I think we can make them private for now, until we find a use case to make them public
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| ticklocations : tuple describing the ticklocation of the y and x axis | ||
| The first element must be {'auto', 'left', 'right'} | ||
| The second element must be {'auto', 'top', 'bottom''}. | ||
| If 'auto', the ticklocations are determined by the *location*. |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Minor, but reads more specific without having to explain details 😄
| If 'auto', the ticklocations are determined by the *location*. | |
| If 'auto', a reasonable default is chosen based on *location*. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| location_ticklocs = _get_bivar_ticklocations_from_location(location) | ||
| for i in range(2): | ||
| if ticklocations[i] == 'auto': | ||
| ticklocations[i] = location_ticklocs[i] | ||
| self.ticklocations = ticklocations |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I would give the helper function a little more responsibility, so that we can here write:
self.ticklocations = _resolve_bivar_ticklocations(ticklocations, location)
read: the self.ticklocations are determined by resolving the parameters ticklocations and location.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| """ | ||
| def _y_limits_changed(self, ax=None): | ||
| if not self._allow_change_limits: | ||
| ylim = self.ax.get_ylim() | ||
| if self.colorizer.norm.norms[0].vmin != ylim[0]: | ||
| self.colorizer.norm.norms[0].vmin = ylim[0] | ||
| if self.colorizer.norm.norms[0].vmax != ylim[1]: | ||
| self.colorizer.norm.norms[0].vmax = ylim[1] | ||
|
|
||
| def _x_limits_changed(self, ax=None): | ||
| if not self._allow_change_limits: | ||
| xlim = self.ax.get_xlim() | ||
| if self.colorizer.norm.norms[1].vmin != xlim[0]: | ||
| self.colorizer.norm.norms[1].vmin = xlim[0] | ||
| if self.colorizer.norm.norms[1].vmax != xlim[1]: | ||
| self.colorizer.norm.norms[1].vmax = xlim[1] | ||
| """ | ||
| """ | ||
| def _update_limits(self, ax=None): | ||
| if not self._allow_change_limits: | ||
| ylim = self.ax.get_ylim() | ||
| xlim = self.ax.get_xlim() | ||
| if (self.colorizer.norm.norms[0].vmin != ylim[0] or | ||
| self.colorizer.norm.norms[0].vmax != ylim[1] or | ||
| self.colorizer.norm.norms[1].vmin != xlim[0] or | ||
| self.colorizer.norm.norms[1].vmax != xlim[1]): | ||
| raise ValueError("Changing the limits of this axis must be done " | ||
| "using the norm attached to the bivariate colorbar.") | ||
| """ |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Comment our by string quoting ?!?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This is somewhat embarrassing 😅
This should be outdated, are you reviewing the commits, or the PR as a whole. I think this needs to be squashed. Let me know if you want me to squash it now.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
|
||
| def update_normals(self, mappable=None): | ||
| self.set_alpha(self.mappable.get_alpha()) | ||
| #self._draw_all() |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Please check commented out code. Either uncomment or delete.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| # self.ax.callbacks.disconnect(self._colorbar_xax_cid) | ||
| # self.ax.callbacks.disconnect(self._colorbar_yax_cid) |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Please check commented out code. Either uncomment or delete.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| self._interactive_funcs = ["_get_view", "_set_view", | ||
| "_set_view_from_bbox", "drag_pan"] | ||
| for x in self._interactive_funcs: | ||
| setattr(self.ax, x, getattr(self, x)) |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Oh boy, we are picking up all the design sins of Colorbar :). Let's at least make this a concept and define
def _patch_ax_methods(self):
"""Override Axes for colorbar-specific behavior."""
self.ax.cla = self._cbar_cla
# interactive methods
self.ax._get_view = self._get_view
self.ax._set_view = self._set_view
self.ax._set_view_from_bbox = self._set_view_from_bbox
self.ax.drag_pan = self._drag_pan
def _unpatch_ax_methods(self):
"""Undo override Axes for colorbar-specific behavior."""
# Note: del works because we added these methods as
# instance attributes, hiding the class-level methods.
# Deleting the attributes uncovers the methods.
del self.ax.cla
# interactive methods
del self.ax._get_view
del self.ax._set_view
del self.ax._set_view_from_bbox
del self.ax.drag_pan
That's far more readable. Note also that we are now able to keep self._drag_pan private even though we must override a public method ax.drag_pan.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| @aspect.setter | ||
| def aspect(self, aspect): | ||
| aspect = float(aspect) | ||
| self._aspect = aspect |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Do we need to store this, or could we reconstruct it from ax.get_box_aspect() when needed?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
We can remove this, I did some interactive testing with an assertion that these are equal, and it seems to work in all cases, and no tests fail.
Good catch!
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
@timhoffm Do you want to look more at this, or should I make a commit with fixes? I'll try to show up for next weeks meeting |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@trygvrad Sorry, this is really hard to review. There's too much going on, and it's very hard to keep an overview. I tried to narrow the scope by looking at individual commits, but that doesn't work either because the commits revise each other. Would it be possible to split this into multiple subsequent PRs? E.g.
Or whatever helps to make smaller chunks. AI should be able to sort this if you give reasonalbe direction. If that sounds like a reasonable approach, please apply my comments so far, and then let's split the result. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I also question if it is necessary to touch For what it is worth, I think your three colorbars should just be three colorbars, perhaps wrapped in a helper. The bi-variate colorbar should just be an axes, and probably should just be treated like a normal axes rather than a colorbar. It really takes up too much room to be a bare decoration. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Not sure. Do we have reasonable capability to layout multiple colorbars next to an Axes? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I don't think we have a turnkey way to make three colorbars and lay them out, so a helper is great. For bivariate, a colorbar axes does three things.
1 and 2 seems like they are easily separated. Without looking too hard I think this PR does that, or if it doesn't should try and do so as much as possible. 3 is obviously a bit convoluted, and maybe requires the interaction between the new and traditional colorbars. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Thank you for the feedback @timhoffm @jklymak :)
I will try to do this, asking AI is probably a good idea.
Might as well also see if I can make some new files instead of touching colorbar.py, that might make it easier to review as well. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
At least 1 approving review is required to merge this pull request.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.