Skip to content

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

Merged
merged 1 commit into from
Jul 20, 2022
Merged

Set norms using scale names. #20752

merged 1 commit into from
Jul 20, 2022

Conversation

anntzer
Copy link
Contributor

@anntzer anntzer commented Jul 27, 2021

PR Summary

Implements #20746.

Proof of concept implementation for #20746.
Still needs a lot of docs, but otherwise mostly works.

Remaining issues:

PR Checklist

  • Has pytest style unit tests (and pytest passes).
  • Is Flake 8 compliant (run flake8 on changed files to check).
  • New features are documented, with examples if plot related.
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • Conforms to Matplotlib style conventions (install flake8-docstrings and run flake8 --docstring-convention=all).
  • New features have an entry in doc/users/next_whats_new/ (follow instructions in README.rst there).
  • API changes documented in doc/api/next_api_changes/ (follow instructions in README.rst there).

colors.Normalize)()
norm_cls = type(norm)
norm_cls.__name__ = f"{scale_cls.__name__}Norm"
return norm_cls
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#20755 fixes that part.

@anntzer
Copy link
Contributor Author

anntzer commented Dec 21, 2021

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.

@anntzer anntzer force-pushed the strnorms branch 2 times, most recently from 9715b10 to ae7c8e8 Compare December 21, 2021 20:27
@jklymak jklymak requested review from timhoffm and jklymak January 20, 2022 20:42
Copy link
Member

@timhoffm timhoffm left a 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.

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.

👍 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.

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
Copy link
Contributor

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.

if norm is None:
norm = colors.Normalize()
elif isinstance(norm, str):
# case-insensitive, consistently with scale_factory.
scale_cls = scale._scale_mapping[norm.lower()]
Copy link
Contributor

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.

@anntzer
Copy link
Contributor Author

anntzer commented Jan 21, 2022

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):
Copy link
Member

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.

Copy link
Contributor Author

@anntzer anntzer Jan 21, 2022

Choose a reason for hiding this comment

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

  1. Some scales (most prominently, logit) don't have a norm associated;
  2. because everything is wrapped under lru_caches, _auto_norm_from_scale(LogScale) is going to be exactly the same object as LogNorm (multiple calls to make_norm_from_scale or _auto_norm_from_scale always return the same norm class). (I added a test for that.)

Copy link
Member

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?

Copy link
Contributor Author

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).

Copy link
Member

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)

Copy link
Contributor Author

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 :)

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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.

@anntzer anntzer force-pushed the strnorms branch 5 times, most recently from e0de415 to d38dd06 Compare January 21, 2022 14:44
Copy link
Member

@jklymak jklymak left a 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.

Comment on lines 5324 to 5288
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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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.

Copy link
Contributor Author

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.)

Copy link
Member

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.

Comment on lines 5917 to 5891
cmap, norm, vmin, vmax
Data normalization and colormapping parameters for *C*. See
`~.Axes.imshow` for a detailed description.
Copy link
Member

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.

@anntzer
Copy link
Contributor Author

anntzer commented Jun 23, 2022

Rebased.

@anntzer anntzer marked this pull request as ready for review June 23, 2022 10:13
@timhoffm
Copy link
Member

timhoffm commented Jun 23, 2022

@anntzer It seems that my proposal to interpolate every parameter individually got lost:

marker : ...
    ...

%(param_cmap)s

%(param_norm)s

%(param_vmin_vmax)s

    more text on vmin/vmax.

alpha : float
    ...

IMHO that would solve the major issues with the present interpolation

  • Naming %(cmap_norm_vmin_vmax_doc)s is quite longish and still not very clear.
  • There's one place that extends the vmin/vmax section, but it relies implicitly on vmin/vmax being the last parameter in the interpolation block.

What do you think?

@anntzer
Copy link
Contributor Author

anntzer commented Jun 23, 2022

Sure, done. (I named them cmap_doc, norm_doc, vmin_vmax_doc as that's more consistent with other _doc entries already present in interpd, but don't feel strongly about that either.)

if norm is None:
norm = colors.Normalize()
elif isinstance(norm, str):
try:
scale_cls = scale._scale_mapping[norm]
Copy link
Contributor Author

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.

@timhoffm
Copy link
Member

timhoffm commented Jun 25, 2022

Flake8:

./lib/matplotlib/contour.py:1787:1: E124 closing bracket does not match visual indentation
./lib/matplotlib/tri/tricontour.py:210:1: E124 closing bracket does not match visual indentation

The other CI failures can probably be fixed by a rebase.

Comment on lines 647 to 648
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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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.

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.""",
Copy link
Member

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).

Copy link
Contributor Author

@anntzer anntzer Jun 25, 2022

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.

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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?

Copy link
Contributor Author

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.

@jklymak
Copy link
Member

jklymak commented Jun 30, 2022

Moving to draft until flake8 looked at....

@jklymak jklymak marked this pull request as draft June 30, 2022 08:59
@QuLogic QuLogic modified the milestones: v3.6.0, v3.7.0 Jul 5, 2022
@anntzer anntzer marked this pull request as ready for review July 14, 2022 10:14
@anntzer
Copy link
Contributor Author

anntzer commented Jul 19, 2022

@jklymak Thanks for the ping, now fixed.

Copy link
Member

@jklymak jklymak left a 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!

@jklymak
Copy link
Member

jklymak commented Jul 20, 2022

Did we want the three commits?

@anntzer
Copy link
Contributor Author

anntzer commented Jul 20, 2022

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?

@jklymak
Copy link
Member

jklymak commented Jul 20, 2022

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

@jklymak jklymak modified the milestones: v3.7.0, v3.6.0 Jul 20, 2022
@jklymak jklymak merged commit 3992fb0 into matplotlib:main Jul 20, 2022
@anntzer anntzer deleted the strnorms branch July 20, 2022 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants