-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
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
Fix BoundaryNorm for multiple colors and one region #17830
Conversation
I think @efiring is the local expert on The logic for this is:
The reason this code path is important is so that we don't make user down-sample the default color maps for use with boundary norm, the re scaling implicitly down samples (as some bins will just never be hit). The opposite case (where the user gives us more bins that colors) we raise because implicitly merging the users bins is something we should not do. |
Thanks for the clear explanation! |
@dstansby I took the liberty of pushing that last GH post as in-line comments and did a bit more internal re-naming to make it clearer. |
lib/matplotlib/colors.py
Outdated
scalefac = (self.Ncmap - 1) / (self._N - 1) | ||
# if we have more colors than regions, stretch the region index | ||
# computed above to full range of the color bins. This will | ||
# make use skip some of the colors in the middle of the |
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.
Maybe change "This will...full range" to "The first region is mapped to the first color in the colormap, and the last region to the last color." Or leave the sentence out entirely.
lib/matplotlib/colors.py
Outdated
# special case the 1 region case. What we should do here | ||
# is a bit undefined (as _any_ color in the colormap is | ||
# justifiable) we go with not adjusting the value (which will | ||
# either be 0 or 1 depending on if we are extending down) |
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 this comment is also confusing. The divisor will be 0 only if there are no extensions and 2 boundaries, or one extension and only 1 boundary. So we have to pick one color out of the map. BoundaryNorm is supposed to work like contourf, so we should do what contourf does, which is pick the middle of the colormap. But we can't do that with scalefac, because iret is filled with zeros. It has to be directly filled with (self.Ncmap -1) // 2.
Actually, filled contours require a minimum of 2 boundaries, and I think that BoundaryNorm should do likewise. |
I pushed the changes to the docstrings, but the change in behavior to go to the middle rather than the bottom for the 1 region case seems like something we should think about a bit more? |
@tacaswell, thinking more is fine, but at the moment I don't see any reasonable arguments for anything other than the middle color in the one-region case without extensions, hence matching contourf. Before it raised an exception, so we are free to choose anything. In the one-region case with one boundary and one extension, we could
I haven't checked to see whether the colorbar would even work with nothing but an extension. I expect it wouldn't. I favor option 1; I don't see that a BoundaryNorm with only one boundary makes sense. It's marginal even with 2 boundaries. |
🤦 I had missed that detail and was focused on preserving the API from @dstansby 's first commit. I don't think we should allow the 1 boundary but with extensions case so option 1 here (already enforced), but we will still need to adjust the return value for "in bin". |
I think this will do it: if self.Ncmap > self._n_regions:
if self._n_regions == 1:
iret = np.full(iret.shape, (self.Ncmap - 1) // 2, dtype=np.int16)
else:
scalefac = (self.Ncmap - 1) / (self._n_regions - 1)
iret = (iret * scalefac).astype(np.int16) |
Sorry I hijacked your relatively simple PR @dstansby . |
This never worked before so is not an API change.
One concern I have about this is that if you add an extension just up or down you move the color drastically:
but the behavior of the |
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.
Slight docstring enhancement suggestion, but also fine as-is.
lib/matplotlib/colors.py
Outdated
@@ -1429,9 +1429,9 @@ def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'): | |||
Parameters | |||
---------- | |||
boundaries : array-like | |||
Monotonically increasing sequence of boundaries | |||
Monotonically increasing sequence of boundaries. |
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.
Might be worth saying "...sequence of at least 2 boundaries."
@tacaswell, the single-region case is not very useful, and there are many ways it could be handled, with or without adding extensions, so I think the present version is fine. Anyone who wants fine control over a small number of colors should be using their own ListedColormap so they can exactly match colors to regions. The stretching logic is there to make things easy, so people can use the standard 256-entry colormaps, not to match whatever any particular person might actually think they want--which would be impossible, of course. |
I now have a bunch of commits in this branch and should not review it.
I am still 👍 on these changes, but someone who is not me should do a final review and merge (I am 👍 if @dstansby wants to review my changes and merge the wholething) |
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.
Can't approve through the UI since this is technically my PR, but I approve 👍
Fixes #17579. I don't understand why the scaling is
scalefac = (self.Ncmap - 1) / (self._n_regions - 1)
, and the logic when the number of colours is > the number of regions is not well defined in the dosctring so I don't know what it should be. This prevents the case with 1 region and > 1 colours from crashing though, and gives a sensible result.I also changed a variable name from
_N
to make the code more readable.