Skip to content
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

[MNT]: Colormaps odd behavior (1 != 1.0) #28198

Open
aaronshenhao opened this issue May 9, 2024 · 13 comments
Open

[MNT]: Colormaps odd behavior (1 != 1.0) #28198

aaronshenhao opened this issue May 9, 2024 · 13 comments
Labels

Comments

@aaronshenhao
Copy link

aaronshenhao commented May 9, 2024

Summary

I encountered some unexpected behavior while usingn matplotlib.colormaps. Apparently, a_colormap(1) outputs the wrong result. The color it produces is indistinguishable from a_colormap(0), although it is slightly different. a_colormap(1.0) produces the correct result, but a_colormap(1.0) != a_colormap(1).

What is going on? It would be useful to clarify this in the docs.

Example

from matplotlib import colors, colormaps
import matplotlib.pyplot as plt

print(colors.to_hex(colormaps["binary"](0)), colors.to_hex(colormaps["binary"](1.0)))
print(colors.to_hex(colormaps["binary"](0)), colors.to_hex(colormaps["binary"](1)))

Output

#ffffff #000000
#ffffff #fefefe

Proposed fix

Improved documentation, or added consistency to make integer values work for the upper endpoint.

@aaronshenhao
Copy link
Author

The relevant line is here:

def __call__(self, X, alpha=None, bytes=False):

The Python docstring appears to comment on the difference:

    X : float or int, `~numpy.ndarray` or scalar
        The data value(s) to convert to RGBA.
        For floats, *X* should be in the interval ``[0.0, 1.0]`` to
        return the RGBA values ``X*100`` percent along the Colormap line.
        For integers, *X* should be in the interval ``[0, Colormap.N)`` to
        return RGBA values *indexed* from the Colormap with index ``X``.

Colormap.N is 256 by default. It's mentioned in the API reference as well. However, it may still be useful to mention this behavior in the more accessible docs for colormap (link).

@story645 story645 added topic: color/color & colormaps Documentation: user guide files in galleries/users_explain or doc/users labels May 9, 2024
@story645
Copy link
Member

story645 commented May 9, 2024

It would be useful to clarify this in the docs.

Totally agree, though maybe as two subsections in colormap manipulaton?

  • access by index
  • access by percentage

@Kaustbh
Copy link

Kaustbh commented May 9, 2024

Can I work on it , or is it in a decision making phase ?

@story645
Copy link
Member

story645 commented May 9, 2024

You can add the documentation as described.

@timhoffm
Copy link
Member

timhoffm commented May 10, 2024

From the API design perspective: First and foremost color maps are a mapping from [0, 1] to colors. The whole quantization concept should be an implementation detail and not mixed into the primary API, i.e. I consider it a design mistake that calling with integers returns the quantized values.

I’ve not completely thought this through yet and there are quirks like the qualitative colormaps (which IMHO should better be a different type of object not a color map) that have a strong dependency on quantization. But I propose to deprecate calling colormaps with ints and instead move this to a new syntax, possibly cmap[i] or cmap.colors[i].

@story645
Copy link
Member

I’ve not completely thought this through yet and there are quirks like the qualitative colormaps (which IMHO should better be a different type of object not a color map)

I think it's a weird quirk/implementation artifact of ListedColormap that it allows resampling since technically there's no guarantee that the colors are arranged in a space where resampling makes any sense. On the different type of object, I think a DiscreteNorm that legends can read maybe makes more sense for how nominal colormaps are used in practice.

that have a strong dependency on quantization. But I propose to deprecate calling colormaps with ints and instead move this to a new syntax, possibly cmap[i] or cmap.colors[i].

Big fan of this proposal since I think it makes it clearer that that object is indexable.

@rcomer
Copy link
Member

rcomer commented May 10, 2024

I propose to deprecate calling colormaps with ints and instead move this to a new syntax, possibly cmap[i] or cmap.colors[i].

I think cmap.colors[i] is the better choice because

  • It already works 😀
  • It is more visually distinct from cmap(x) and so less likely to be confusing.

@timhoffm
Copy link
Member

I agree that cmap.colors[i] is better. But it still needs some work:

  • It's only available in ListedColoramp not in LinearSegmentedColormap.
  • There, it's a simple attribute containing the raw data with which the colormap was created. This should (1) be normalized and (2) be read-only - overwriting or modifying colors does not change the underlying lookup table; anyway we're trying to move more towards immutability of colormaps.

makes more sense for how nominal colormaps are used in practice.

How/and what for are they used in practice? The only example I've found in our codebase seems to be https://matplotlib.org/stable/gallery/pie_and_polar_charts/nested_pie.html and I have to confess, that that usage is not quite understandable for someone not well versed in Colormaps

@story645
Copy link
Member

story645 commented May 10, 2024

How/and what for are they used in practice?

Mostly choropleths (mapped heatmaps), but also, going back to my GSOC 😓, they're useful for showing categories on rasters:
stas

xref: #21786 and #27721

ETA: Also I have totally exploited interpolation on ListedColormap to make arbitrarily colored diverging color maps.

@timhoffm
Copy link
Member

Ok, deprecating int mapping is not so trivial, because BoundaryNorm returns ints. Before we can deprecate, we need an alternative story for qualitative maps, likely

  • we can immediately deprecate the int mapping on LinearSegmentedColormaps. IMO they are not reasonably usable for int mapping at all.
  • we should have a DiscreteColormap (or QualitativeColormap) class that only maps ints to colors. The existing qualitative colormaps should use this instead of ListedColormap.
  • After that, we can also deprecate int mapping on ListedColormaps.
  • Optional: warn if BoundaryNorm and DiscreteColormap are not used together. Since colormaps and norms don't know about each other, this check would likely have to be done during initialization of ScalarMappable. - This is not perfect, but can detect many cases of I'll-configured mappings.

@story645
Copy link
Member

story645 commented May 10, 2024

DiscreteColormap (or QualitativeColormap)

NominalColormap? since it's used for Nominal measurements and the scale is what imposes the constraints on behavior (Stevens 1946)

warn if BoundaryNorm and DiscreteColormap are not used together. Since colormaps and norms don't know about each other, this check would likely have to be done during initialization of ScalarMappable. - This is not perfect, but can detect many cases of I'll-configured mappings.

BoundaryNorm is used a ton for (ETA and far as I can tell, designed for) ordinal data, and in that case it's perfectly appropriate to use most of the sequential and many of the diverging maps & to essentially let the norm cut up the color. That's what geopandas is also doing frequently.

@timhoffm
Copy link
Member

timhoffm commented May 11, 2024

Ah, eventually, I understand BondaryNorm. It needs a ncolors parameter specifying the number of colors in the colormap to be able to map to integers in the whole cmap range. We should investigate whether we can achieve the same by mapping to [0, 1] instead (theoretically no problem, but there may be numeric edge effects. Probably also needs digging into the design decision why it maps to ints in the first place). This would remove the need to accept ints on colorbars for this context. It’s also simpler because the user does not have to do a manual matching of the ncolors parameter.

@anntzer
Copy link
Contributor

anntzer commented May 12, 2024

The only example I've found in our codebase seems to be https://matplotlib.org/stable/gallery/pie_and_polar_charts/nested_pie.html and I have to confess, that that usage is not quite understandable for someone not well versed in Colormaps

A bit of a tangent but I agree the color handling mechanism in that example is indeed rather impossible to understand. I would suggest rewriting as something like

diff --git i/galleries/examples/pie_and_polar_charts/nested_pie.py w/galleries/examples/pie_and_polar_charts/nested_pie.py
index c83b4f6f84..92d77e1975 100644
--- i/galleries/examples/pie_and_polar_charts/nested_pie.py
+++ w/galleries/examples/pie_and_polar_charts/nested_pie.py
@@ -9,6 +9,7 @@ in Matplotlib. Such charts are often referred to as donut charts.
 See also the :doc:`/gallery/specialty_plots/leftventricle_bullseye` example.
 """
 
+import matplotlib as mpl
 import matplotlib.pyplot as plt
 import numpy as np
 
@@ -31,9 +32,11 @@ fig, ax = plt.subplots()
 size = 0.3
 vals = np.array([[60., 32.], [37., 40.], [29., 10.]])
 
-cmap = plt.colormaps["tab20c"]
-outer_colors = cmap(np.arange(3)*4)
-inner_colors = cmap([1, 2, 5, 6, 9, 10])
+outer_colors = mpl.colors.to_rgba_array(["C0", "C1", "C2"])[:, :3]
+desaturation = [.6, .3]
+inner_hsv = np.repeat(mpl.colors.rgb_to_hsv(outer_colors), len(desaturation), axis=0)
+inner_hsv[:, 1] *= np.tile(desaturation, len(outer_colors))
+inner_colors = mpl.colors.hsv_to_rgb(inner_hsv)
 
 ax.pie(vals.sum(axis=1), radius=1, colors=outer_colors,
        wedgeprops=dict(width=size, edgecolor='w'))

although perhaps complicated color-generation code here just distracts from the example (but again, perhaps realistic use cases of this kind of nested pies would be happy to have the code to generate suitable "sub-colors" for the inner ring). Alternatively at least cmap(np.arange(3) * 4) could be rewritten as cmap([0, 4, 8]) with the comment "select some suitable colors".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants