colorcet: Perceptually uniform colormaps from CET

Peter Kovesi at the Center for Exploration Targeting created a very useful set of perceptually uniform colormaps, many of which can replace the highly non-uniform colormaps provided with Python plotting programs. Here we will show how to use them via the new colorcet python package, listing all the ones available and allowing you to evaluate how perceptually uniform they are for you, your particular monitor, etc. Download and installation instructions are at the github site. Note that this web page is also a notebook, if you want to recreate the results yourself.

We will plot them using matplotlib via holoviews, but identical bokeh palettes are also provided, and both Bokeh palettes and Matplotlib colormaps are usable in datashader. Thus these colormaps can be used in any of those Python packages, as well as any other package that accepts a Python list of normalized RGB tuples or hex colors. See Peter's site to download versions for other non-Python packages.

We first make some utilities for sorting and visualizing colormaps:

In [1]:
import numpy as np
import holoviews as hv
import colorcet as cc

hv.notebook_extension()

import param
with param.logging_level('CRITICAL'):
    hv.plotting.mpl.LayoutPlot.v17_layout_format = True
HoloViewsJS successfully loaded in this cell.
In [2]:
%opts Image [xaxis='bare' yaxis='bare' sublabel_format=None aspect=3 fontsize=13]

# Collapse all colormap aliases into a single string, and then sort by it
inverse = {}
for k,v in cc.cm.items():
    if not k[-2:] == "_r":
        inverse[v] = inverse.get(v, [])
        inverse[v].insert(0,k)
all_cms = {',  '.join(reversed(v)):k for (k,v) in inverse.items()}.items()
all_cms.sort()

aliased_cms=[(k,v) for (k,v) in all_cms if "," in k]

def lay_out_in_columns(_list, cols, spacer=None):
    """Given a list, reorder it into the number of columns specified, filling missing items with the given spacer."""
    rows = int(np.ceil(len(_list)*1.0/cols))
    padded = _list + [spacer] * (rows*cols-len(_list))
    return list(np.array(padded).reshape(cols,rows).T.flat)

xs,ys = np.meshgrid(np.linspace(0,1,80), np.linspace(0,1,10))

def colormaps(named_cms,cols=3,array=xs):
    images = [hv.Image(array, group=name)(style=dict(cmap=cm)) 
              for name,cm in named_cms]
    return hv.Layout(lay_out_in_columns(images,cols,hv.Empty())).display('all').cols(cols)

Accessing the colormaps

After importing colorcet as cc, all the colormaps shown in this notebook will be available for use in different forms, with hopefully at least one such form convenient for any particular application. There are two different basic versions for each colormap, each of which is fundamentally a list of 256 distinct colors:

  1. A Bokeh-style palette, i.e., a Python list of RGB colors as hex strings, like ['#000000', ..., '#ffffff']
  2. If matplotlib is installed and importable, a Matplotlib LinearSegmentedColormap using normalized magnitudes, like LinearSegmentedColormap.from_list("fire",[ [0.0,0.0,0.0], ..., [1.0,1.0,1.0] ], 256)

The Bokeh-compatible palettes are provided as attributes in the colorcet namespace, with long names prefixed with b_ or a short name (if any) with no prefix. E.g. linear_kryw_0_100_c71 can be accessed as cc.b_linear_kryw_0_100_c71 or by the short name cc.fire, which both return the same Bokeh palette. These names should tab complete once cc has been imported. Because Bokeh palettes are just Python lists, you can always reverse them using normal Python syntax, e.g. list(reversed(cc.fire)). If you want to access the palettes by string name, they are also collected into a dictionary named palette, so you can use cc.palette["linear_kryw_0_100_c71"] or cc.palette["fire"] instead if that is more convenient.

The Matplotlib colormaps are also provided as tab-completable attributes, but consistently with a prefix m_, e.g. cc.m_linear_kryw_0_100_c71 or cc.m_fire. Already reversed versions are also available, as cc.m_linear_kryw_0_100_c71_r or cc.m_fire_r. The same colormaps are also registered with matplotlib's string-based dictionary with the prefix cet_, making them available by name within various matplotlib functions (e.g. cet_linear_kryw_0_100_c71, cet_linear_kryw_0_100_c71_r, cet_fire, or cet_fire_r). Finally, if you want to access the colormaps by string name without using Matplotlib's registry, they are also stored in the cc.cm dictionary, e.g. cc.cm["linear_kryw_0_100_c71"], cc.cm["linear_kryw_0_100_c71_r"], cc.cm["fire"], or cc.cm["fire_r"].

In each case, the names used are the shortest ones that are expected to be unique in that context, and in practice you are only likely to need one of these forms for any particular application.

Named colormaps

The full list of colormaps included will be shown below, but a useful subset of these maps that cover the various types have been given more convenient names, and we will focus on those first.

Linear (sequential) colormaps, for plotting magnitudes:

In [3]:
colormaps([i for i in aliased_cms if "linear" in i[0] and "diverging" not in i[0]])
Out[3]:

Diverging colormaps, for plotting magnitudes increasing or decreasing from a central point:

In [4]:
colormaps([i for i in aliased_cms if "diverging" in i[0]])
Out[4]:

Misc colormaps:

  • cyclic: for cyclic quantities like orientation or phase (where the highest and lowest values need the same color)
  • isoluminant: to highlight low spatial-frequency information
  • rainbow: to highlight local differences in sequential data
In [5]:
colormaps([i for i in aliased_cms if "cyclic" in i[0] or "rainbow" in i[0] or "isoluminant" in i[0]])
Out[5]:

Full list of available colormaps

This list includes all those above, plus other variants and alternatives. Each colormap has a name in the form:

category_huesequence_lightnessrange_cmeanchroma[_scolorshift_[rifreversed]]

along with any shorter alias that may be defined above.

In [6]:
colormaps(all_cms, cols=4)
Out[6]:

Testing perceptual uniformity

Peter Kovesi created a test image with a sine grating modulation of intensity, where modulation gain decreases from top to bottom, which helps evaluate perceptual uniformity of a colormap at a glance. The matplotlib maintainers use different definitions of perceptually uniform (uniformity in a different color space), but the new matplotlib perceptually uniform colormaps do well at Peter's test image:

In [7]:
%output size=200
%opts Image [xaxis='bare' yaxis='bare' sublabel_format=None aspect=4 fontsize=10]
sine=np.load("colourmaptest.npy")
In [8]:
colormaps([["viridis"]*2,["inferno"]*2],cols=1,array=sine)
Out[8]:

Here the sine grating for a uniform colormap should be visible as a fine-toothed comb with teeth that gradually become less visible from top to bottom. The further down the comb these teeth are visible, the higher the discriminability of magnitudes at that location in the colormap. Thus a perceptually uniform colormap, like the two above, should have teeth that visible at the same length for all colors.

You can also use these images to evaluate the overall level of discriminability provided by a given colormap -- the longer the visible area of teeth, the better this colormap allows you to discriminate fine differences in magnitude. Here the inferno map seems to have better discriminability than the viridis map, despite both being perceptually uniform.

The default colormaps that have traditionally been used with Matlab, Matplotlib, and HoloViews are clearly not perceptually uniform -- all the green and yellow areas are nearly indistinguishable:

In [9]:
colormaps([["hot"]*2,["jet"]*2],cols=1,array=sine)
Out[9]:

Thus those colormaps should be avoided if at all possible, to avoid generating misleading visualizations. Compare these two to the perceptually uniform versions provided by this package:

In [10]:
colormaps([("fire",cc.m_fire),("rainbow",cc.m_rainbow)],cols=1,array=sine)
Out[10]:

We can see the results for all the colorcet colormaps below, which can be summarized as:

  • "linear" colormaps all work well by this criterion
  • "diverging" colormaps typically have discontinuities in perceptual discriminability around the central value
  • "cyclic" colormaps with repeating colors tend to have discontinuities at 1/4 and 3/4 of the way through the cycle, or at other locations if shifted.
  • "isoluminant" colormaps typically show no visible modulation, because without luminance cues humans can only discriminate low spatial-frequency modulations (i.e., much wider teeth would be needed for evaluating such colormaps)
  • Some of the "rainbow" colormaps seem to have a perceptual discontinuity around the color red, the reasons for which are not yet clear.
In [11]:
colormaps(all_cms,cols=1,array=sine)
Out[11]: