Limiting Colormapping

Current technical definitions

  1. Colormaps define a mapping from the range [0…1] to color values
  2. Norms define a mapping from data coordinates to [0…1]
  3. Colorbars draw the whole colormap (i.e. the colors mapped from [0…1]).

Task from https://github.com/matplotlib/matplotlib/issues/13948:

Keep the colormapping to data space as is but limit the colorbar to the range blue-orange.

Current solution: Create a new norm and colormap

Pain points:

A. Complicated to create.
B. For diverging colormaps the color at the center of the cmap range (=0.5) is a special value. This feature gets lost in the rescaling. It replaced by a fine-tuned balance between the Norm and the Colorma (in the example norm: 0 -> 0.66, and cmap(0.66) is the sepecial value). It’s not easily possible anymore later on to exchange the colormap without regenerating the norm.

Option 1: Improved factory API

Come up with some Norm and Colormap factory API that creates these. Solves A, but not B.

Option 2: Support setting cbar limits

This gives up 3) Colorbars draw the whole colormap.

What colors should values beyond the cbar limits have?

  • Their cmap values? - I find this problematic because the image would contain values that are not in the colorbar, e.g. for the above example and limiting the cbar to orange, how should a user interpret red?
  • The over/under values? - Might be difficult to implement because the color calculation of the image is done in the cmap, but the clipping is done on the Axes. The axes would have to notify the colormap on the restriction. But what if two axes use the same colormap for two images but clip at different values?

Option 3: Add over_lim and under_lim attributes to the cmap

a value of over_lim=0.75 would map values >0.75 to the over color.

We would define:
3) Colorbars draw the (possibly clipped) colormap (i.e. the colors mapped from [under_lim…overlim]).

1 Like

If you want to truncate the colormap, its basically a two-liner:

viridisBig = cm.get_cmap('viridis', 512)
newcmp = ListedColormap(viridisBig(np.linspace(0.25, 0.75, 256)))

@jklymak I’m not quite sure what your comment is relating to. By it’s own, this does not solve the above request.

Why not? You want to retain a certain fraction of the colormap, and the two-liner does that.

I in particular also want to retain a certian mapping from data coordinates to colors, e.g. 0.5=orange.

To get that in your example I would additionally have to create a new norm. Essentially something like (untested):

def clipped_cmap(cmap_name, vmin, vmax, vmin_clip=None, vmax_clip=None):
    """
    Return a clipped but color-mapping preserving Norm and Colormap.

    The returned Norm and Colormap map data values to the same colors as
    would  `Normalize(vmin, vmax)`  with *cmap_name*, but values below
    *vmin_clip* and above *vmax_clip* are mapped to under and over values
    instead.
    """
    if vmin_clip is None:
        vmin_clip = vmin
    if vmax_clip is None:
        vmax_clip = vmax
    
    assert vmin <= vmin_clip < vmax_clip <= vmax
    cmin = (vmin_clip - vmin) / (vmax - vmin)
    cmax = (vmax_clip - vmin) / (vmax - vmin)

    big_cmap = cm.get_cmap(cmap_name, 512)
    new_cmap = ListedColormap(big_cmap(np.linspace(cmin, cmax, 256)))
    new_norm = Normalize(vmin_clip, vmax_clip)
    return new_norm, new_cmap

In the above example that would be called:

clipped_cmap(vmin=-1, vmax=1, vmax_clip=0.5)

Sure? I don’t see the need for the new norm. In practice if you want white centred at zero, you’d just do:

vmin = -0.3
vmax = 2
cmap = clipped_cmap('RdBu_r', -2, 2, -0.3, 2)
imshow(data, vmin=-0.3, vmax=2, cmap=cmap)

I’d be against something that had vmin and vmax not equal to the limits the user actually wants.

both of the above seem to be the same. Wether you return a norm from the factory and supply it to imshow, or whether you type vmin and vmax manually into imshow is the same.


Since the problem here is pretty similar to what https://github.com/matplotlib/matplotlib/pull/15333 wants to solve, I would also suggest here to solve it via a Norm alone (no new colormap needed). The difference to #15333 is that here you have no center and autoscaled limits but specify clip limits yourself. The norm could hence look like this:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize

class BikeShedNorm(Normalize):
    def __init__(self, vmin, vmax, clip_min, clip_max):
        super().__init__(vmin=clip_min, vmax=clip_max)
        self.outernorm = Normalize(vmin, vmax)
        
    def __call__(self, value, clip=None):
        result, is_scalar = self.process_value(value)
        self.autoscale_None(result)  # sets self.vmin, self.vmax if None
        under = result < self.vmin
        over = result > self.vmax
        result = self.outernorm(result)
        result = np.ma.masked_array(result, mask=np.ma.getmask(result))
        result[under] = -1.
        result[over] = 2.
        if is_scalar:
            result = np.atleast_1d(result)[0]
        return result
        


np.random.seed(19680801)
data = (np.random.rand(4, 11)-0.5)*8+2

cmap=plt.get_cmap("rainbow")
cmap.set_under("white")
cmap.set_over("black")

fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(7, 2))

norm1 = Normalize(vmin=-2, vmax=6)
im = ax1.imshow(data, cmap=cmap, norm=norm1)
cbar = fig.colorbar(im, ax=ax1, orientation="horizontal", aspect=15, extend='both')

norm2 = BikeShedNorm(vmin=-2, vmax=6, clip_min=0, clip_max=3)
im = ax2.imshow(data, cmap=cmap, norm=norm2)
cbar = fig.colorbar(im, ax=ax2, orientation="horizontal", aspect=15, extend='both')

ax1.set_title("Total")
ax2.set_title("Zoom-in")
plt.show()

image

Agreed, you can do it either with a norm or by trimming the colormap, but I don’t think you need to do both. I am a little uncertain what happens if you change vmin or vmax after the fact if you use a norm. I guess it gets passed down to the norm?

Yes you don’t need the norm if you manipulated the colormap accordingly, because it will be set according to the input data; but that would implicitely assume that vmin/vmax are the min/max of the data. So that is to say: because, while creating the colormap, you already know the norm that gets to be chosen automatically afterwards you will not need to specifically create it.

Not sure what is meant by “change vmin or vmax after the fact”, so I cannot answer that.

Does actually work. It’s not quite nice that you have to specify vmin, vmax twice. For that reason I’d still slightly prefer

norm, cmap = clipped_cmap('RdBu_r', -2, 2, -0.3, 2)
imshow(data, norm=norm, cmap=cmap)

over

cmap = clipped_cmap('RdBu_r', -2, 2, -0.3, 2)
imshow(data, vmin=-0.3, vmax=2, cmap=cmap)

In the latter, it’s more easy to accidentially break the desired vlim relation.

I would also suggest here to solve it via a Norm alone (no new colormap needed).

I wasn’t aware that is possible (and/because I don’t really understand why the norm can clip the colorbar). Is this a bug or a feature?