-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
DOC: draft of a full explanation of norm + colormap interactions #18487
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
base: main
Are you sure you want to change the base?
Changes from all commits
1a30f6a
0aa8586
e5a255d
83209c8
0123002
6af9106
d6867f6
2d72105
e5bf870
c49b646
32a2a70
67ed971
ced2552
c8fa541
317a6af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -14,42 +14,307 @@ | |||||||||
:no-members: | ||||||||||
:no-inherited-members: | ||||||||||
|
||||||||||
Classes | ||||||||||
------- | ||||||||||
Color Conversion tools | ||||||||||
---------------------- | ||||||||||
|
||||||||||
.. autosummary:: | ||||||||||
:toctree: _as_gen/ | ||||||||||
:template: autosummary.rst | ||||||||||
|
||||||||||
is_color_like | ||||||||||
same_color | ||||||||||
|
||||||||||
hsv_to_rgb | ||||||||||
rgb_to_hsv | ||||||||||
|
||||||||||
to_hex | ||||||||||
to_rgb | ||||||||||
to_rgba | ||||||||||
to_rgba_array | ||||||||||
|
||||||||||
get_named_colors_mapping | ||||||||||
|
||||||||||
|
||||||||||
.. _norms_and_colormaps: | ||||||||||
|
||||||||||
Normalization and Colormapping of Continuous Data | ||||||||||
------------------------------------------------- | ||||||||||
|
||||||||||
Some `~.artist.Artist` classes can map an array of input data to RGBA | ||||||||||
values, (ex `~.axes.Axes.scatter` or `~.axes.Axes.imshow`). The | ||||||||||
machinery for this is implemented via the `~.cm.ScalarMappable` base | ||||||||||
class in `~.cm` and the `~.Normalize` and `~.Colormap` classes in | ||||||||||
`~.colors` (this module). | ||||||||||
|
||||||||||
At the core, colormapping is going from a scalar value to a RGB tuple (formally | ||||||||||
:math:`f(x) : ℝ^1 \rightarrow ℝ^3`). To effectively communicate through the | ||||||||||
Comment on lines
+49
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
color we want pick a :ref:`colormap suited to the data | ||||||||||
<sphx_glr_tutorials_colors_colormaps.py>`. For continuous data types [#f1]_ a | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The outline here is a little non-linear, and seems to take a while before it settles into an order.
I think this could all be three paragraphs, or maybe 5 if you want to be expansive:
If you plan to talk about the "Curve through space" I wonder if a diagram would make this much clearer. Expressing this with math formalisms without diagrams is going to lose your audience. |
||||||||||
"good" colormap smoothly and continuously change their RGB values as a function | ||||||||||
of the input that trace out a 1-dimensional path through the 3-dimensional RGB | ||||||||||
space [#f2]_. We can restrict the domain of :math:`f` to $[0, 1]$ which we | ||||||||||
interpret as the normalized distance along the curve. This allows us to | ||||||||||
cleanly separate the mapping process from the continuous input data to RGB into | ||||||||||
two steps: | ||||||||||
|
||||||||||
1. the mapping between the user's data to distance along the curve | ||||||||||
2. the parameterized path through color space. | ||||||||||
|
||||||||||
The first step is expressed through the `.Normalize` family of classes and the | ||||||||||
second is expressed in Matplotlib via the `.Colormap` family of classes. This | ||||||||||
allows us to fully independently pick the functional transform (e.g., linear vs | ||||||||||
log) from data space to distance along the curve space, what (user) data range | ||||||||||
to show (via the ``vmin`` and ``vmax`` attributes on `.Normalize`, or via the | ||||||||||
`.cm.ScalarMappable.set_clim` method), and what colors to use (by selecting the | ||||||||||
`.Colormap`). Both `.Colormap` and `.Normalize` are implemented as `callable | ||||||||||
classes <https://docs.python.org/3/reference/datamodel.html#object.__call__>`__ | ||||||||||
which allows use to bind some (mutable) state to a function call. The complete | ||||||||||
functionality is exposed in the `.ScalarMappable` family of artists which have | ||||||||||
a `.Colormap` and `.Normalize` instances and are responsible for invoking them | ||||||||||
at draw time. | ||||||||||
|
||||||||||
The `.Normalize` family has 3 common attributes: *vmin*, *vmax*, and *clip* | ||||||||||
which control the data limits. The `.Normalize.__call__` signature is :: | ||||||||||
|
||||||||||
def __call__(value: RawData, clip:Optional[Bool] =None) -> NormedData: | ||||||||||
... | ||||||||||
|
||||||||||
It takes in data in the user's data space and converts it to *NormedData* with | ||||||||||
the range: | ||||||||||
|
||||||||||
.. math:: | ||||||||||
|
||||||||||
\begin{cases} | ||||||||||
\mathrm{under} & d < vmin \\ | ||||||||||
[0, 1] & vmin \leq d \leq vmax \\ | ||||||||||
\mathrm{over} & vmax < d \\ | ||||||||||
\mathrm{bad} & !\mathrm{np.finite(d)} | ||||||||||
\end{cases} | ||||||||||
|
||||||||||
|
||||||||||
|
||||||||||
The `.Colormap.__call__` signature when passed *NormedData* (floats) [#f3]_ is | ||||||||||
:: | ||||||||||
|
||||||||||
def __call__(self, X: NormedData, | ||||||||||
alpha:Optional[float] =None, bytes:Bool=False) -> RGBA: | ||||||||||
... | ||||||||||
|
||||||||||
In addition to parameterized path through RGB (which handles values in $[0, | ||||||||||
1]$, `.Colormap` objects carry three additional colors: | ||||||||||
|
||||||||||
- *over* (`.Colormap.set_over` / `.Colormap.get_over`) | ||||||||||
- *under* (`.Colormap.set_under` / `.Colormap.get_under`) | ||||||||||
- *bad* (`.Colormap.set_bad` / `.Colormap.get_bad`) | ||||||||||
|
||||||||||
which control the color for the corresponding values in *NormedData*. | ||||||||||
By default the over and under colors are the top and bottom colors of | ||||||||||
the colormap respectively and bad is transparent. | ||||||||||
|
||||||||||
.. warning:: | ||||||||||
|
||||||||||
Using `.cm.get_cmap` may return to you a reference to a globally | ||||||||||
visible instance of the colormap (rather than a new instance). If | ||||||||||
you plan to set the over/under/bad values we recommend you first | ||||||||||
make a copy :: | ||||||||||
|
||||||||||
from copy import copy | ||||||||||
import matplotlib.cm as mcm | ||||||||||
|
||||||||||
my_cmap = copy(mcm.get_cmap('viridis')) | ||||||||||
|
||||||||||
.. rubric:: Footnotes | ||||||||||
|
||||||||||
.. [#f1] Discrete data types, such as Categorical and Ordinal, have different | ||||||||||
considerations. | ||||||||||
.. [#f2] Notable, the cubehelix colormap is named because it traces a helix | ||||||||||
through the RGB color cube from black to white. | ||||||||||
.. [#f3] Discrete data, as handled by `.NoNorm` and `.BoundaryNorm` are passed | ||||||||||
as integers and act as direct Look Up Table (LUT) indexes into the | ||||||||||
colormap. | ||||||||||
|
||||||||||
In practice | ||||||||||
~~~~~~~~~~~ | ||||||||||
|
||||||||||
To make the above section concrete, lets first consider the linear `.Normalize` | ||||||||||
|
||||||||||
.. ipython :: | ||||||||||
|
||||||||||
In [104]: import matplotlib.colors as mcolors | ||||||||||
|
||||||||||
In [105]: norm = mcolors.Normalize(vmin=100, vmax=300) | ||||||||||
|
||||||||||
In [106]: norm | ||||||||||
Out[106]: <matplotlib.colors.Normalize at 0x7f9bf441aeb0> | ||||||||||
|
||||||||||
If we now pass in values in the range of :math:`[vmin, vmax]` | ||||||||||
|
||||||||||
.. ipython :: | ||||||||||
|
||||||||||
|
||||||||||
In [130]: norm([100, 200, 300]) | ||||||||||
Out[130]: | ||||||||||
masked_array(data=[ 0. , 0.5, 1.], | ||||||||||
mask=False, | ||||||||||
fill_value=1e+20) | ||||||||||
|
||||||||||
We see that they are scaled as expected. If we also pass in some over | ||||||||||
/ under / bad values | ||||||||||
|
||||||||||
.. ipython :: | ||||||||||
|
||||||||||
In [131]: norm([0, 100, 200, 300, 400, np.nan]) | ||||||||||
Out[131]: | ||||||||||
masked_array(data=[-0.5, 0. , 0.5, 1. , 1.5, nan], | ||||||||||
mask=False, | ||||||||||
fill_value=1e+20) | ||||||||||
|
||||||||||
we see that they are also scaled and produce values outside of the | ||||||||||
range :math:`[0, 1]`. If you need the values to be clipped, you can do | ||||||||||
have the norm do that for you via the *clip* kwarg | ||||||||||
|
||||||||||
.. ipython :: | ||||||||||
|
||||||||||
In [130]: norm([0, 100, 200, 300, 400, np.nan], clip=True) | ||||||||||
Out[130]: | ||||||||||
masked_array(data=[0. , 0. , 0.5, 1. , 1. , nan], | ||||||||||
mask=False, | ||||||||||
fill_value=1e+20) | ||||||||||
|
||||||||||
The default value of *clip* can be set when instantiating the | ||||||||||
`.Normalize` instance. | ||||||||||
|
||||||||||
We can also use a non-linear norm | ||||||||||
|
||||||||||
.. ipython :: | ||||||||||
|
||||||||||
In [136]: log_norm = mcolors.LogNorm(10, 1000) | ||||||||||
|
||||||||||
In [137]: log_norm([10, 100, 1000]) | ||||||||||
Out[137]: | ||||||||||
masked_array(data=[0.0, 0.5, 1.0], | ||||||||||
mask=[False, False, False], | ||||||||||
fill_value=1e+20) | ||||||||||
|
||||||||||
if the data has a large dynamic range. | ||||||||||
|
||||||||||
Once we have normalized our data we can pass it to the colormap | ||||||||||
|
||||||||||
.. ipython :: | ||||||||||
|
||||||||||
In [102]: import copy | ||||||||||
|
||||||||||
In [103]: import matplotlib.cm as mcm | ||||||||||
|
||||||||||
In [141]: viridis = copy.copy(mcm.get_cmap('viridis')) | ||||||||||
|
||||||||||
.. ipython :: | ||||||||||
|
||||||||||
In [143]: viridis([0, .5, 1]) | ||||||||||
Out[143]: | ||||||||||
array([[0.267004, 0.004874, 0.329415, 1. ], | ||||||||||
[0.127568, 0.566949, 0.550556, 1. ], | ||||||||||
[0.993248, 0.906157, 0.143936, 1. ]]) | ||||||||||
|
||||||||||
Which pulls out the bottom, middle, and top color of the *viridis* . | ||||||||||
If we set the over/under/bad colors and pass out-of-range values we can also | ||||||||||
see them pulled out: | ||||||||||
|
||||||||||
.. ipython :: | ||||||||||
|
||||||||||
In [144]: viridis.set_over('w') | ||||||||||
|
||||||||||
In [147]: viridis.set_under('k') | ||||||||||
|
||||||||||
In [148]: viridis.set_bad('r') | ||||||||||
|
||||||||||
In [149]: viridis([1.5, -0.5, np.nan]) | ||||||||||
Out[149]: | ||||||||||
array([[1., 1., 1., 1.], | ||||||||||
[0., 0., 0., 1.], | ||||||||||
[1., 0., 0., 1.]]) | ||||||||||
|
||||||||||
|
||||||||||
|
||||||||||
Directly using a `.Colormap` outside of a `.ScalarMappable` can be useful | ||||||||||
to generate a family of coherent colors for plotting | ||||||||||
|
||||||||||
.. plot:: | ||||||||||
:include-source: | ||||||||||
|
||||||||||
import matplotlib.cm as mcm | ||||||||||
import numpy as np | ||||||||||
|
||||||||||
cmap = mcm.get_cmap('viridis') | ||||||||||
array_of_colors = cmap(np.linspace(0, 1, 5)) | ||||||||||
|
||||||||||
x = np.linspace(0, 1, 25) | ||||||||||
fig, ax = plt.subplots(constrained_layout=True) | ||||||||||
for j, color in enumerate(array_of_colors): | ||||||||||
ax.plot(x, x**j, color=color, label=f'$x^{j}$') | ||||||||||
ax.legend() | ||||||||||
|
||||||||||
API | ||||||||||
~~~ | ||||||||||
|
||||||||||
Colormap Classes | ||||||||||
++++++++++++++++ | ||||||||||
|
||||||||||
.. autosummary:: | ||||||||||
:toctree: _as_gen/ | ||||||||||
:template: autosummary.rst | ||||||||||
|
||||||||||
BoundaryNorm | ||||||||||
Colormap | ||||||||||
CenteredNorm | ||||||||||
LightSource | ||||||||||
LinearSegmentedColormap | ||||||||||
ListedColormap | ||||||||||
LogNorm | ||||||||||
NoNorm | ||||||||||
|
||||||||||
|
||||||||||
.. inheritance-diagram:: matplotlib.colors.Colormap matplotlib.colors.LinearSegmentedColormap matplotlib.colors.ListedColormap | ||||||||||
:parts: 1 | ||||||||||
:private-bases: | ||||||||||
|
||||||||||
|
||||||||||
Norm Classes | ||||||||||
++++++++++++ | ||||||||||
|
||||||||||
.. autosummary:: | ||||||||||
:toctree: _as_gen/ | ||||||||||
:template: autosummary.rst | ||||||||||
|
||||||||||
Normalize | ||||||||||
LogNorm | ||||||||||
CenteredNorm | ||||||||||
TwoSlopeNorm | ||||||||||
PowerNorm | ||||||||||
SymLogNorm | ||||||||||
TwoSlopeNorm | ||||||||||
FuncNorm | ||||||||||
BoundaryNorm | ||||||||||
NoNorm | ||||||||||
|
||||||||||
|
||||||||||
.. inheritance-diagram:: matplotlib.colors.Normalize matplotlib.colors.LogNorm matplotlib.colors.PowerNorm matplotlib.colors.NoNorm matplotlib.colors.TwoSlopeNorm matplotlib.colors.SymLogNorm matplotlib.colors.BoundaryNorm matplotlib.colors.FuncNorm matplotlib.colors.CenteredNorm | ||||||||||
:parts: 1 | ||||||||||
:private-bases: | ||||||||||
|
||||||||||
|
||||||||||
Factory Functions & Decorators | ||||||||||
++++++++++++++++++++++++++++++ | ||||||||||
|
||||||||||
Functions | ||||||||||
--------- | ||||||||||
|
||||||||||
.. autosummary:: | ||||||||||
:toctree: _as_gen/ | ||||||||||
:template: autosummary.rst | ||||||||||
|
||||||||||
from_levels_and_colors | ||||||||||
hsv_to_rgb | ||||||||||
rgb_to_hsv | ||||||||||
to_hex | ||||||||||
to_rgb | ||||||||||
to_rgba | ||||||||||
to_rgba_array | ||||||||||
is_color_like | ||||||||||
same_color | ||||||||||
get_named_colors_mapping | ||||||||||
make_norm_from_scale | ||||||||||
|
||||||||||
|
||||||||||
|
||||||||||
Hill Shading | ||||||||||
------------ | ||||||||||
|
||||||||||
.. autosummary:: | ||||||||||
:toctree: _as_gen/ | ||||||||||
:template: autosummary.rst | ||||||||||
|
||||||||||
LightSource |
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 is a TOC aspect to this file, and then you've added an explanation. a) not 100% sure the explanation belongs here, versus an intermediate/advanced tutorial. Most long-form explanations are tutorials currently (i.e. transform stack). I'm not adamantly advocating for that, but it would be more consistent with current practice. It would also make your examples easier to write.
Regardless, this large narrative in the middle of the TOC makes the TOC very hard to parse. At the very least, this should go at the end?