Cyclic colormap without visual distortions for use in phase angle plots?

cilix picture cilix · May 17, 2014 · Viewed 14.1k times · Source

I'm looking for a good circular/cyclic colormap to represent phase angle information (where the values are restricted to the range [0, 2π] and where 0 and 2π represent the same phase angle).

Background: I'd like to visualize normal modes by plotting both the power spectral density and the relative phase information of the oscillations across the system.

I'll admit that previously I used the 'rainbow' colormap for the power plot and the 'hsv' colormap for the phase plot (see [1]). However, the use of the rainbow colormap is extremely discouraged because of its lack of perceptual linearity and ordering [2][3]. So I switched to the 'coolwarm' colormap for the power plot which I quite like. Unfortunately, the 'hsv' colormap seems to introduce the same kind of visual distortions as the 'rainbow' map (and it also doesn't go along very well with the 'coolwarm' map since it looks kind of ugly and flashy in comparison).

Does anyone have a good recommendation for an alternative circular colormap which I could use for the phase plots?

Requirements:

  • It needs to be circular so that the values 0 and 2π are represented by the same color.

  • It should not introduce any visual distortions; in particular, it should be perceptually linear (which the 'hsv' colormap doesn't seem to be). I don't believe that perceptual ordering is such a big deal for phase information, but it would of course not do any harm.

  • It should be visually appealing when combined with the 'coolwarm' colormap. However, I'm not dead set on 'coolwarm' and am happy to consider other options if there is another nice pair of colormaps to visualize amplitude and phase information.

Bonus points if the colormap is available (or can be easily created) for use in matplotlib.

Many thanks for any suggestions!

[1] http://matplotlib.org/examples/color/colormaps_reference.html

[2] http://www.renci.org/~borland/pdfs/RainbowColorMap_VisViewpoints.pdf

[3] http://medvis.org/2012/08/21/rainbow-colormaps-what-are-they-good-for-absolutely-nothing/

Answer

poppie picture poppie · Jan 1, 2016

EDIT: Matplotlib has now nice cyclic color maps, see the answer of @andras-deak below. They use a similar approach to the color maps as in this answer, but smooth the edges in luminosity.

The issue with the hue-HUSL colormap is that it's not intuitive to read an angle from it. Therefore, I suggest to make your own colormap. Here's a few possibilities:

  • For the linear segmented colormap, we definine a few colors. The colormap is then a linear interpolation between the colors. This has visual distortions.
  • For the luminosity-HSLUV map, we use the HUSL ("HSLUV") space, however instead of just hue channel, we use two colors and the luminosity channel. This has distortions in the chroma, but has bright colors.
  • The luminosity-HPLUV map, we use the HPLUV color space (following @mwaskom's comment). This is the only way to really have no visual distortions, but the colors are not saturated This is what they look like:

colormap with simple function

We see that in our custom colormaps, white stands for 0, blue stands for 1i, etc. On the upper right, we see the hue-HUSL map for comparison. There, the color-angle assignments are random.

enter image description here

Also when plotting a more complex function, it's straightforward to read out the phase of the result when using one of our colormaps.

And here's the code for the plots:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as col
import seaborn as sns
import hsluv # install via pip
import scipy.special # just for the example function

##### generate custom colormaps
def make_segmented_cmap(): 
    white = '#ffffff'
    black = '#000000'
    red = '#ff0000'
    blue = '#0000ff'
    anglemap = col.LinearSegmentedColormap.from_list(
        'anglemap', [black, red, white, blue, black], N=256, gamma=1)
    return anglemap

def make_anglemap( N = 256, use_hpl = True ):
    h = np.ones(N) # hue
    h[:N//2] = 11.6 # red 
    h[N//2:] = 258.6 # blue
    s = 100 # saturation
    l = np.linspace(0, 100, N//2) # luminosity
    l = np.hstack( (l,l[::-1] ) )

    colorlist = np.zeros((N,3))
    for ii in range(N):
        if use_hpl:
            colorlist[ii,:] = hsluv.hpluv_to_rgb( (h[ii], s, l[ii]) )
        else:
            colorlist[ii,:] = hsluv.hsluv_to_rgb( (h[ii], s, l[ii]) )
    colorlist[colorlist > 1] = 1 # correct numeric errors
    colorlist[colorlist < 0] = 0 
    return col.ListedColormap( colorlist )

N = 256
segmented_cmap = make_segmented_cmap()
flat_huslmap = col.ListedColormap(sns.color_palette('husl',N))
hsluv_anglemap = make_anglemap( use_hpl = False )
hpluv_anglemap = make_anglemap( use_hpl = True )

##### generate data grid
x = np.linspace(-2,2,N)
y = np.linspace(-2,2,N)
z = np.zeros((len(y),len(x))) # make cartesian grid
for ii in range(len(y)): 
    z[ii] = np.arctan2(y[ii],x) # simple angular function
    z[ii] = np.angle(scipy.special.gamma(x+1j*y[ii])) # some complex function

##### plot with different colormaps
fig = plt.figure(1)
fig.clf()
colormapnames = ['segmented map', 'hue-HUSL', 'lum-HSLUV', 'lum-HPLUV']
colormaps = [segmented_cmap, flat_huslmap, hsluv_anglemap, hpluv_anglemap]
for ii, cm in enumerate(colormaps):
    ax = fig.add_subplot(2, 2, ii+1)
    pmesh = ax.pcolormesh(x, y, z/np.pi, 
        cmap = cm, vmin=-1, vmax=1)
    plt.axis([x.min(), x.max(), y.min(), y.max()])
    cbar = fig.colorbar(pmesh)
    cbar.ax.set_ylabel('Phase [pi]')
    ax.set_title( colormapnames[ii] )
plt.show()