-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Set norms using scale names. #20752
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
Set norms using scale names. #20752
Conversation
lib/matplotlib/cm.py
Outdated
colors.Normalize)() | ||
norm_cls = type(norm) | ||
norm_cls.__name__ = f"{scale_cls.__name__}Norm" | ||
return norm_cls |
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 guess this seems overkill to me, and fragile as shown by the pickling issue? Why don't we just use scale._scale_mapping
or make one, i.e. colors._norm_mapping
? Do we really need the extra flexibility given by allowing dynamic creation?
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 really want to create a separate mapping as people can register new scales (e.g. mpl-probscale does it), but they wouldn't show up in the norm mapping if these are separate.
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.
Back when I was a whippersnapper, we only had the linear norm and we liked it....
I'm not sure that making this super-flexible is worth the complication. The strings can be for a few common norms. How many specialized scales truly need a norm as well, and how much of a problem is it to ask users to just pass the norm instead to the string? Its not like probscale is going to be a super-useful color norm. If you wanted to add a register_norm
, fine, but making it all dynamic just seems to be asking for trouble.
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.
#20755 fixes that part.
This should now be ready for review. Note that the docs are currently split in a separate commit and most methods now just point to imshow() to reduce the duplication, but once we agree on a wording I can also copy paste the same doc everywhere if that's what we decide to do at the end. |
9715b10
to
ae7c8e8
Compare
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.
At some later point, we may want to move the explanation of the colormapping mechanism to some Explanations section in the docs and link everything there instead of to imshow()
. But linking imshow()
for a start is ok.
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 like the concept of using strings to make it easier to get a norm than having to import matplotlib.colors
and create an object. I think this could lead to some mismatch/confusion about which scale/norm mappings are available, so perhaps some better string validation/error messages would be helpful when using this.
lib/matplotlib/axes/_axes.py
Outdated
The `.Normalize` instance used to scale scalar data to the [0, 1] | ||
range before mapping to colors using *cmap*. By default, a linear | ||
range before mapping to colors using *cmap*. By default, a linear |
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.
There should only be one space after a period for most (all now?) style guides, so this shouldn't be updated. Same for the other docstrings in this PR.
lib/matplotlib/cm.py
Outdated
if norm is None: | ||
norm = colors.Normalize() | ||
elif isinstance(norm, str): | ||
# case-insensitive, consistently with scale_factory. | ||
scale_cls = scale._scale_mapping[norm.lower()] |
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 wondering if it would be helpful to wrap this in a try-except and re-raise with a list of options available for users?
I can see users trying: imshow(..., norm="Boundary")
and wondering what option would work instead.
Thanks, I should have addressed all the comments. |
@@ -331,6 +332,35 @@ def unregister_cmap(name): | |||
return _cmap_registry.pop(name) | |||
|
|||
|
|||
@functools.lru_cache(None) | |||
def _auto_norm_from_scale(scale_cls): |
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.
Overall I think this is a great idea. However, all the scales with strings currently have Norm classes, don't they? If so, why are you creating a new class here versus using the existing classes? It seems like it will just make things needlessly confusing.
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.
- Some scales (most prominently, logit) don't have a norm associated;
- because everything is wrapped under lru_caches,
_auto_norm_from_scale(LogScale)
is going to be exactly the same object asLogNorm
(multiple calls tomake_norm_from_scale
or_auto_norm_from_scale
always return the same norm class). (I added a test for that.)
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 you document that here? Because I couldn't parse that from the code, but maybe my ignorance of how the caches get shared across submodules.
However, I'm still not understanding what happens if the user puts 'logit' in here. Are you saying a norm will still be created?
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 documentation is the third bullet point below ("The returned norm class is memoized and reused for later calls." But feel free to suggest something clearer). Yes, a norm class will be dynamically created if the user puts in "logit" (and later calls will reuse the same dynamically created norm class).
This will also work with scales provided by third-parties, e.g. mpl-probscale (which is why I did not go for just an explicit and exhaustive mapping of names to norm classes -- this would not work with third-party scales).
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 would not work with third-party scale
Fair enough if we are supporting those as norms, though that sounds risky to me...
The returned norm class is memoized and reused for later calls.
Maybe just a bit more verbose "the returned norm class is the same as for make_norm_from_scale (so norm='log' will return a LogNorm instance); these are are also memoized..." (though is there a reason to not use "cached"? "memoized" is pretty jargon, but maybe my ignorance)
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.
Re: 3rd-parties: well, that was also one of the points of the PR (and again, think of this as "supporting putting a third-party scale on a colorbar axis").
Re: docs: sure, reworded, sorry for the jargon :)
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.
Edit: actually I had to fix the caching, but it should be good 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.
I had a bit of confusion initially figuring out exactly what is being cached. The cached "thing" is the class definition. It is not the returned Norm object. So, these two calls will have different LogNormalize objects, which is consistent with how it works currently when no norm is given (a new Normalize object is created each time).
im1 = ax.imshow([[0, 1]], norm="log")
im2 = ax.imshow([[0, 1]], norm="log")
assert im1.norm is not im2.norm
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.
Good point, I further reworded the docs to clarify that.
e0de415
to
d38dd06
Compare
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 looks ready to go to me, but we probably need to copy the documentation between the methods. Its not very nice to be asked to look at another doscstring for basic kwargs if we can help it.
lib/matplotlib/axes/_axes.py
Outdated
The normalization can also be given as a str, which should be a | ||
scale name (as in `~.Axes.set_xscale`, i.e. one of "linear", "log", | ||
"symlog", "logit", etc.). In that case, a normalization class is | ||
dynamically generated from the corresponding scale, as if using | ||
that scale for the image's colorbar. |
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 normalization can also be given as a str, which should be a | |
scale name (as in `~.Axes.set_xscale`, i.e. one of "linear", "log", | |
"symlog", "logit", etc.). In that case, a normalization class is | |
dynamically generated from the corresponding scale, as if using | |
that scale for the image's colorbar. | |
If a str, *norm* must be a scale name (i.e. "linear", "log", | |
"symlog", "logit", as per `~.Axes.set_xscale`, or call | |
`~.scale.get_scale_names`). By default, this scale will be | |
also used for a colorbar for the image. |
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 not really sure about this wording? (Why "by default"? 1) I guess you could change that, but having a linear-scale colorbar for a lognorm seems rare; 2) more than anything, this kwarg sets the norm on the mappable; setting the colorbar scale is more a side effect of that. Also, even set_xscale doesn't mention get_scale_names
. Perhaps it should, but if it doesn't, I don't think imshow() needs to do that either.)
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.
"one of "linear", "log", "symlog", "logit", etc." is vague, so was trying to be more precise. "...as if using the scale" was also theoretical sounding, whereas we do use the scale for the colorbar, so why not be explicit? As for "by default" - the user can change the colorbar scale. But I don't feel strongly about my wording - feel free to iterate as you see fit.
lib/matplotlib/axes/_axes.py
Outdated
cmap, norm, vmin, vmax | ||
Data normalization and colormapping parameters for *C*. See | ||
`~.Axes.imshow` for a detailed description. |
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 guess this needs to all be copied over.
Rebased. |
@anntzer It seems that my proposal to interpolate every parameter individually got lost:
IMHO that would solve the major issues with the present interpolation
What do you think? |
Sure, done. (I named them |
if norm is None: | ||
norm = colors.Normalize() | ||
elif isinstance(norm, str): | ||
try: | ||
scale_cls = scale._scale_mapping[norm] |
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.
Given #20753, I made this case-sensitive from the beginning.
Flake8:
The other CI failures can probably be fixed by a rebase. |
lib/matplotlib/cm.py
Outdated
before mapping to colors using *cmap*. By default, a linear scaling mapping | ||
the lowest value to 0 and the highest to 1 is used. This parameter is |
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.
before mapping to colors using *cmap*. By default, a linear scaling mapping | |
the lowest value to 0 and the highest to 1 is used. This parameter is | |
before mapping to colors using *cmap*. By default, a linear scaling is used, | |
mapping the lowest value to 0 and the highest to 1. This parameter is |
IMHO it's a bit easier to read this way.
lib/matplotlib/cm.py
Outdated
cmap_doc="""\ | ||
cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` | ||
The Colormap instance or registered colormap name used to map scalar data | ||
to colors. This parameter is ignored for RGB(A) data.""", |
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 not all functions using cmap support RGB(A) data. Now that we have entries per parameter it is possible to do move this out of the general description and do
%(cmap_doc)s
This parameter is ignored for RGB(A) data.
only when needed (or make a cmap_rgba_doc
if there are many cases).
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.
Actually, none of cmap, vmin/vmax, and norm apply for RGBA data, so all of them should get this comment.
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.
Um, that's not what I meant. We have functions like scatter that allow either colormapped or RGBA data. Here, the sentence makes sense. But we also have functions like hexbin, that only operate with colormapping. Mentioning that a parameter is ignored for RGBA data when RGBA data is not supported by the function is somewhat confusing.
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.
Sorry I was unclear, I was meaning "all of cmap, norm, and vmin/vmax should get this comment". From a quick check, the only functions that do not take color input are hexbin and hist2d; do you agree?
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.
From a quick search, I think the following functions support color input:
- scatter
- hexbin
- imshow
- pcolor
- pcolormesh
- pcolorfast
- hist2d
- specgram
- contour
- tripcolor
Feel free to update the list.
Even if it's only hexbin and hist2d, how do we handle them?
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, this should be handled now.
Moving to draft until flake8 looked at.... |
@jklymak Thanks for the ping, now fixed. |
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.
Looks good to me!
Did we want the three commits? |
This was mostly to make it easier to iterate on the individual parts separately, now squashed. I don't know what's the status of 3.6 right now; @QuLogic can I remilestone this to 3.6? |
I don't think there is a 3.6.x branch, and I've not heard that the release has been started, so lets merge as 3.6 |
PR Summary
Implements #20746.
Proof of concept implementation for #20746.Still needs a lot of docs, but otherwise mostly works.Remaining issues:autoscale() will run into issues for scales with limited domains, e.g. we should/need to be able to auto-infer that LogNorm only normalizes based on the positive values (cf. overrides ofautoscale()
in LogNorm). Closed by Autoinfer norm bounds. #21989.The autogenerated norm classes are not picklable, which can be considered a slightly obscure limitation, but can be solved using semi-standard boilerplate as in Templatize class factories. #19033. This is tracked by [Bug]: make_norm_from_scale should create picklable classes even when used in-line. #20755.PR Checklist
pytest
passes).flake8
on changed files to check).flake8-docstrings
and runflake8 --docstring-convention=all
).doc/users/next_whats_new/
(follow instructions in README.rst there).doc/api/next_api_changes/
(follow instructions in README.rst there).