Designing multivariate colormaps

Multivariate colormaps are seeing use in a number of domains that rely upon python ecosystem. I work with dark-field X-ray microscopy, which is one such domain, but there are numerous others, such as astronomy, polarization microscopy etc. While each domain may have dedicated tooling, darfix, astropy, xrayutilities etc, this tooling is built upon the scientific python stack, including numpy, scipy, pandas, matplotlib, plotly, etc. Since these domains share common challenges regarding multivariate colormaps, it would be ideal to handle these challenges upstream (i.e. in matplotlib, plotly, etc.) rather than in the domain-specific packages. In addition to the technical implementation, the design criteria for multivariate colormaps must be explored.

For the design of colormaps, Peter Kovesis paper (2015) covers the topic with great diligence, as is Nathaniel Smith and Stéfan van der Walts talk (2015) explaining the origin of the now-ubiquitous (at least in academia) ‘viridis’ colormap. They outline an approach centered on human perception, where colormaps are designed to be perceptually uniform while maximizing the number of levels that can be distinguished. For multivariate colormaps, this approach has not previously been implemented.

2D colormaps
Just as any continuous 1D colormap is a curve in colorspace, any continuous 2D colormap is a surface. Peter Kovesis, Nathaniel Smith and Stéfan van der Walts made the point that one should use a perceptually uniform colormap, such as CIELAB when designing 1D colormaps, and this remains true for 2D colormaps. The sRBG color gamut is illustrated below in the colorspace CAM02-LCD (Large Color Difference) Luo et al. (2006).

CAM02-LCD (and CIELAB) describes colorspace using three orthogonal axes: L*: lightness, a*: green-red, and b*: blue-yellow. The colorspace is designed to be perceptually uniform, i.e. the distance you need to move along an axis before you notice a change, should be the same along L*, a*, and b*.

Colormap design is further determined by the kind of data the colormap is designed to represent. Matplotlib groups colormaps as sequential, diverging, and cyclic (in addition to qualitative), designed to represent data that fit into those categories. For 2D colormaps, the two axis can represent categorically different data, making 6 different categories: sequential×sequential, sequential×diverging, sequential×cyclic, diverging×diverging, diverging×cyclic, cyclic×cyclic. One can compare the different categories to the axis in CIELAB space, where L* is sequential, a*/b* is diverging, and the hue (azimuthal around a*=b*=0) is cyclic.

These most relevant categories, are shown as 2D planes in 3D L*a*b* space above.

Using the shapes above to slice the sRGB color gamut will produce 2D colormaps.

Note that defining colormaps in L*a*b*-space limits how much of the color gamut we can utilize. Colormaps defined in sRGB space provide a better utilization of the color gamut. Consider the following blue-orange bi-sequential colormaps:

The colormap defined in sRGB space has increased saturation because it is not limited by perceptual uniformity (i.e. the maximum orange has higher L* than the maximum blue). For many applications, the loss of perceptual uniformity is an acceptable trade for increased saturation (making the colormap ‘pop’ more).

Additional colormaps can be generated using the same design principles:

The circular colormaps are useful for applications where the data has rotational symmetry.

3D colormaps – alpha, a* and b*

One can design a 3D colormap based on the three axes L*, a* and b*. On a black background L* can be replaced by an alpha channel, if the colormap is constant in L*. The colormaps ‘flat’ and ‘disk’ (above) are designed to be used in this way.

Adapting for colorblindness

It is not possible to fully accommodate the various kinds of colorblindness in multi-dimensional colormaps. However, it is worth noting all the common forms of color-blindness affect primarily the a* (red-green) axis in L*a*b* colorspace. Thus, when we design colormaps by slicing L*a*b* space, we should preferentially slice along the b* (blue-yellow) axis, as is done above for the colormaps ‘orangeBlue’ and ‘cut’. However, when making use of a the hue in a cyclic colormap, this is not possible.

However, the hue can be cycled freely, and for the relevant colormaps the axes can be chosen such that the main features of the data are accessible also to those with common forms of colorblindness. The following figure shows the same data as seen by someone with normal color vision, and someone whom is red-green colorblind. For both cases, the data is shown with two different rotations of the colormap. With normal color vision, it is easily discernible that the two circled regions have different hue, but for someone who is red-green colorblind, this is only apparent if the colormap is rotated so that they are differentiated along the blue-yellow axis.

The rotation of the colormap is therefore of critical importance for accommodating those with common forms of colorblindness.

For reference, the above colormaps are shown below as they appear for someone with deuteranopia (unable to perceive ‘green’ light – the most common form of color blindness).

Tools used:

colorspacious to convert between colorspaces and simulated colorblindness
plotly to make 3d figures
matplotlib to make 2d figures
The colormaps were originally made for colorstamps

Other resources:

Bernard, Jürgen, et al. “A survey and task-based quality assessment of static 2D colormaps.” Visualization and Data Analysis 2015. Vol. 9397. SPIE, 2015.

Strode, Georgianna, et al. “Operationalizing Trumbo’s principles of bivariate choropleth map design.” Cartographic perspectives 94 (2019): 5-24.

Kruse, Andrew W., et al. “User study comparing linearity and orthogonalization for polarimetric visualizations.” IEEE Access 10 (2022): 28308-28321.

Leave a Reply

Your email address will not be published. Required fields are marked *

Proudly powered by WordPress | Theme: Baskerville 2 by Anders Noren.

Up ↑