54
$\begingroup$

I have been using the ubiquitous Jet color palette for 2D plots for some time now, and don't really care for it. Then I came across the series of blog posts, The rainbow is dead...long live the rainbow, and really like the color palettes he uses and how they make it more intuitive to see changes in the data on a 2D plot. Is there some way to incorporate these in Mathematica? I'd like to be able to use them without calling in a special package or defining them every time I invoke the kernel.

$\endgroup$
50
$\begingroup$

I've taken the liberty of uploading the RGB values for MyCarta's color schemes to pastebin. Mr. Niccoli provides these in CSV downloadable from his website, but I found that I had to change their format if I want Mathematica to read them during initialization.

(* Read in the numerical data *)
Get["https://pastebin.com/raw/gN4wGqxe"]
ParulaCM = With[{colorlist = RGBColor @@@ parulaColors},
   Blend[ colorlist, #]&
   ];
Cube1CM = With[{colorlist = RGBColor @@@ cube1Colors},
   Blend[ colorlist, #]&
   ];
CubeYFCM = With[{colorlist = RGBColor @@@ cubeYFColors},
   Blend[ colorlist, #]&
   ];
LinearLCM = With[{colorlist = RGBColor @@@ cube1Colors},
   Blend[ colorlist, #]&
   ];
JetCM = With[{colorlist = RGBColor @@@ jetColors},
   Blend[ colorlist, #]&
   ];

If you want to have these functions available without defining them every time you open Mathematica, then put the above text in your init.m file.

You can see the colorschemes via

BarLegend[{#,{0,1}}]&/@{JetCM,ParulaCM,Cube1CM,CubeYFCM,LinearLCM}

enter image description here

and in a simple 2D plot via

DensityPlot[Cos[x] Sin[y], {x, -10, 10}, {y, -10, 10}, 
 PlotRange -> All, ColorFunction -> #, 
 PlotPoints -> 75] & /@ {JetCM, ParulaCM, Cube1CM, CubeYFCM,LinearLCM}

enter image description here

Definitely read the MyCarta blog posts for more information about these color palettes, and why you might want to use them. Also see Matteo's answer below for more info

$\endgroup$
  • 3
    $\begingroup$ This is great, +1. Nicoli works in geospatial visualization, so his work is primarily dealing with strictly positive numbers (elevation profiles), rather than data which has a range symmetric about zero, so for his purposes a linear from 0 to 1 profile makes the most sense. Do you happen to know if there are any "zero is special" perceptual color schemes, in the sense that the legend axis goes from -1 to 1 (instead of 0 to 1), and the apparent luminance conveys the absolute value of the data? I sometimes use data which has both negative and positive numbers, and such a scheme would be great. $\endgroup$ – DumpsterDoofus Oct 31 '14 at 14:40
  • 1
    $\begingroup$ No, but I also plot a lot of data where the numbers range from -1 to 1, where it will be zero most everywhere and some small features and some large features. This was my first gripe with Jet, since it made for a bright green background and you could barely see the small features that were positive or negative but near zero. $\endgroup$ – Jason B. Oct 31 '14 at 14:47
  • 2
    $\begingroup$ @MyCarta Hi ! Although it is really informative and easier to find the information this way, i.e. adding it below the existing answer I don't think that is really the way to handle it. Why don't you sum it up and an answer of your own ? $\endgroup$ – Sektor Nov 1 '14 at 20:08
  • 1
    $\begingroup$ @DumpsterDoofus, Jason: You could also look at Paul Tol's technote, where he gives several nice colour schemes along with their corresponding formulas. In particular, Fig. 8 shows a diverging colour scheme which is what you're looking for. $\endgroup$ – Rahul Nov 2 '14 at 4:46
  • $\begingroup$ I think the Evaluate is misplaced. It has no effect as shown. The color function is more efficient if the value of the colorlist is injected. It might be done with Evaluate[Blend[..]] & but you get a warning message. (Use Quiet I guess.) $\endgroup$ – Michael E2 Dec 12 '17 at 16:17
21
$\begingroup$

Here's my attempt at recreating the LinearLCM palette. It turned out to be very tricky, because it's not a linear problem. In addition the blog article and the paper cited are confusing the terms "luminance" and "lightness".

I started the process with trying to recreate the uniform-luminance rainbow palette on the left: http://www.cs.utah.edu/~gk/papers/vis02/talk/jpg/Image09.jpg

In the paper this was achieved by visually matching the luminance of six different colors equally spaced on the color wheel. I don't know why they used the HLS color space for this because in this color space the "luminance" information is not saved in a single channel; saturation also controls the luminance.

In Mathematica I found two ways to normalize the luminance of those colors. Because of the way Blend/Darker/Lighter functions work I couldn't find a way to fully automatize them. These two approaches produce different results so I will present each one separately.

First approach

For each of the six colors {Red, Yellow, Green, Cyan, Blue, Magenta} I converted them to the LAB color space where luminance is truly controlled with a single channel and calculated the adjustment needed to be made with Lighter (for Blue) or Darker (other colors). I could probably write a more complex recursion function to do this but I thought it would be quicker if I just plot the functions and determine the intersections manually with adjusting the intervals. Here's an example:

ListPlot[Table[{x, 
   ColorConvert[Darker[Magenta, x], "LAB"][[
     1]] - .5}, {x, .15, .18, .001}]]

luminance_adjusting_1

This way I obtained the adjustment values for each colors. Negative numbers mean that the color appears too light, and vice versa.

adj = {{Red, -.080},
   {Yellow, -.520},
   {Green, -.456},
   {Cyan, -.476},
   {Blue, +.398},
   {Magenta, -.173}};

I then interpolated between these reference points in the HSB color space, to create an adjustment function for the whole color wheel [1]:

adjif = Interpolation[{ColorConvert[#[[1]], "HSB"][[1]], #[[2]]} & /@ 
   adj, InterpolationOrder -> 1];

adjustment_interpolation_1

I could now create the luminance-neutral palette. I started with the un-adjusted hue palette and then applied the interpolated adjustments.

rainbow = 
 ColorConvert[
  Rasterize[
   DensityPlot[x, {x, 0, 1}, {y, 0, 1}, 
    ColorFunction -> (Blend[{Red, Yellow, Green, Cyan, Blue, 
         Magenta}, #] &), ColorFunctionScaling -> True, 
    ImageSize -> {1000, 50}, AspectRatio -> 1/Divide @@ {1000, 50}, 
    PlotRangePadding -> 0, Frame -> False]], "HSB"]
rainbowd = ImageData[rainbow][[1]]

rainbowad = 
 ImageResize[
  ImageAssemble[{Map[
     Image[With[{a = adjif[#[[1]]]}, 
        If[a < 0, Darker[Hue @@ #, -a], Lighter[Hue @@ #, a]]]] &, 
     rainbowd]}], ImageDimensions[rainbow]]

hue_palette

adjusted_hue_palette_1

The palette looks quite neutral in luminance, but there are some artifacts: the dark bands in yellow and cyan regions and a light band in the blue region. The luminance level can be also checked with converting the palette to LAB color space:

ListLinePlot[ImageData[ColorConvert[rainbowad, "LAB"]][[1, All, 1]], 
 PlotRange -> {0, 1}]

adjusted_hue_palette_luminance_1

This plot confirms the artifacts, but they are minor except at the right end of the palette. Either way it does not look too bad.

The final step is to apply a linear luminance gradient over the neutral-luminance palette, using the Lighter or Darker functions on each half of the palette.

grad = ImageResize[
  ColorConvert[
   ImageAssemble[{Map[
      Image[With[{a = adjif[#[[1]]]}, 
         With[{b = 
            If[a < 0, Darker[Hue @@ #, -a], Lighter[Hue @@ #, a]]}, 
          If[#[[1]] < 5/12, Lighter[b, 1 - 12/5 #[[1]]], 
           Darker[b, 12/5 #[[1]] - 1]]]]] &, rainbowd]}], "RGB"], 
  ImageDimensions[rainbow]]

luminance_linear_gradient_1

This palette looks quite similar to the one in the paper. The yellow artifact is not noticeable anymore, and the other two have shifted a bit further apart toward green and purple. The luminance plot again confirms this.

ListLinePlot[ImageData[ColorConvert[grad, "LAB"]][[1, All, 1]], 
 PlotRange -> {0, 1}]

linear_gradient_palette_luminance_1

Second approach

Another way to obtain the interpolated adjustment function is to use GrayLevel instead of Darker and Lighter. Since Darker[color, amount] is the same as Blend[{color, Black}, amount], using GrayLevel we now have two variables: Blend[{color, GrayLevel[level]}, amount]. For calibration I decided to fix the amount (variable f in the code) and calculate the level (variable x in the code) values. This time I was able to calculate the adjustments automatically.

f = 2/3;
ctab = Table[
  dx = .1;
  x = 0;
  cx = ColorConvert[Blend[{c, GrayLevel[x]}, f], "LAB"][[1]];
  While[Abs[cx - .5] > 10^-10,
   While[cx - .5 < 0, x += dx; 
    cx = ColorConvert[Blend[{c, GrayLevel[x]}, f], "LAB"][[1]];];
   x -= dx; dx *= .1; 
   cx = ColorConvert[Blend[{c, GrayLevel[x]}, f], "LAB"][[1]];
   ];
  x, {c, {Red, Yellow, Green, Cyan, Blue, Magenta}}]
{0.547099, 0.218085, 0.296868, 0.273634, 0.655471, 0.510001}

With this I again created an interpolation function and applied the adjustments over the initial rainbow palette.

adjif2 = Interpolation[{ColorConvert[#[[1]], "HSB"][[1]], #[[2]]} & /@
    Transpose[{{Red, Yellow, Green, Cyan, Blue, Magenta}, ctab}], 
  InterpolationOrder -> 1];

adjustment_interpolation_2

rainbowad2 = 
 ImageResize[
  ColorConvert[
   ImageAssemble[{Map[
      Image[With[{a = adjif2[#[[1]]]}, 
         Blend[{Hue @@ #, GrayLevel[a]}, f]]] &, rainbowd]}], "RGB"], 
  ImageDimensions[rainbow]]

adjusted_hue_palette_2

Now the adjustment function is noticeably different, with some peaks more pronounced, and other less pronounced. The palette obtained this way looks much more luminance-neutral than the one from the first approach. Artifacts are much less pronounced. The downside is a greater loss of saturation.

ListLinePlot[ImageData[ColorConvert[rainbowad2, "LAB"]][[1, All, 1]], 
 PlotRange -> {0, 1}]

adjusted_hue_palette_luminance_2

Again, I can now apply a linear luminance gradient over the luminance-neutral palette.

grad2 = ImageResize[
  ColorConvert[
   ImageAssemble[{Map[
      Image[With[{a = adjif2[#[[1]]]}, 
         With[{b = Blend[{Hue @@ #, GrayLevel[a]}, f]}, 
          If[#[[1]] < 5/12, Lighter[b, 1 - 12/5 #[[1]]], 
           Darker[b, 12/5 #[[1]] - 1]]]]] &, rainbowd]}], "RGB"], 
  ImageDimensions[rainbow]]

luminance_linear_gradient_2

The gradient is almost perfectly linear, but again at the loss of saturation.

ListLinePlot[ImageData[ColorConvert[grad2, "LAB"]][[1, All, 1]], 
 PlotRange -> {0, 1}]

linear_gradient_palette_luminance_2

Conclusion

As you can see, the problem is not linear. There saturation and apparent brightness of a color are linked together, and with changing one of them the other also changes. So it's difficult to accurately reproduce the exact palette from the article.


[1] Actually the palette does not cover the whole color wheel; the hue ranges from 0 (Red) to 0.8333 (Magenta).

$\endgroup$
  • 3
    $\begingroup$ My take on perceptionally continuous, constant-luminosity, RGB-friendly and luminance/chrominance balanced "rainbow" gradient: Graphics@Raster@Transpose@Table[List @@ ColorConvert[LCHColor[0.728, 0.415, c], "RGB"], {c, 0, 1, 1/1000}, {50}] $\endgroup$ – kirma Nov 1 '14 at 12:42
  • $\begingroup$ @kirma Looks good, like a brighter version of my rainbowad2. How did you decide for these specific values for luminance and chroma? $\endgroup$ – shrx Nov 1 '14 at 12:52
  • 1
    $\begingroup$ I used a rather convoluted, ad-hoc iterative algorithm to search a sort of "mid-point" between values that maximized chromacity (which has low luminance) and essentially white (maximum luminance, but zero chromacity). The search looked for feasibility in RGB space, and after a dozen iterations settled on those values which fit inside RGB color space. Very small further corrections to evenness of perceptible of color gradient can be made with aid of CIE2000 ColorDistance, but those are barely visible even with two gradients next to each other. $\endgroup$ – kirma Nov 1 '14 at 14:17
  • 2
    $\begingroup$ @kirma: I think $l = 0.7206, c = 0.4286$ is the maximum chrominance at which all hues fit in RGB space. $\endgroup$ – Rahul Nov 1 '14 at 21:36
  • 1
    $\begingroup$ @kirma: Not sure what you mean; $l=0.295,c=0.476$ definitely leaves the RGB colour space. i.stack.imgur.com/jhuA8.png $\endgroup$ – Rahul Nov 2 '14 at 4:38
16
$\begingroup$

As suggested by Sektor I am adding this as a separate answer.

DumpsterDoofus' asked in a comment about perceptual, divergent colour schemes (where "zero is special").

I am definitely not a supporter of the common red-white-blue divergent scheme (or any scheme with white in the middle). I’ve often noticed when switching between grayscale and red-white-blue, for example (or red-white-black), that with the latter the white in the middle obfuscates subtle data features maps, and dramatically hinders the perception of discontinuities, for example faults in geological or geophysical maps.

In a paper I read recently, available on his website, Moreland argues that the white in this colour scheme is a Mach band. Mach bands, discovered by the physicist Ernst Mach, are optical illusions where the contrast at the edges between adjoined areas of different lightness appears enhanced by a natural edge detection system in the human vision. In the red-white-blue colormap the Mach band is present because a region of high lightness is surrounded on either side by a ramp of decreasing lightness.

Moreland’s solution to eliminate the Mach band effect is to interpolate between red and blue using Msh, a newly defined, polar coordinate form of CIELab color space. His resulting color map is a very good one, and is available on his website (link 1) along with a few other ones that are worth looking at.

Peter Kovesi also has a great set of divergent color maps.

For my part, I’m working on an interpolation between yellow and blue in HSV color space that goes through achromatic gray. My idea is to keep Hue constant for half of the colormap, then switch Hue at the zero, and simultaneously vary Value between, say 70 and 0, then back to 70, using an ad-hoc nonlinear function. Saturation can be kept constant or varied as well. This colormap, along with a new grayscale that is symmetric on either ends of the intermediate gray, will be available on my Github page (github dot com slash mycarta) by the end of the year, and also added as ASCII csv files on my blog color palette page (mycarta dot wordpress dot com slash color-palettes).

I would also like to add a clarification on something that shrx writes in his answer. He mentions that both in the reference paper and in my bog post there's confusion between luminance and lightness. I clearly state that the original paper makes that confusion and that's why my solution in the post is to fix the palette by adjusting the lightness profile using LAB color space, the same that shrx used. I used L* for lightness in the figures where I use my adjusted colormap, and left luminance in the figures where I used the original colormap for consistency with the author's.

$\endgroup$
  • 2
    $\begingroup$ +1 ! Hope you stick around and share your knowledge :) $\endgroup$ – Sektor Nov 2 '14 at 9:30
  • $\begingroup$ Thanks for the tips Sektor. I will. :) $\endgroup$ – MyCarta Nov 8 '14 at 4:13
13
$\begingroup$

I often use Dave Green's family of color schemes called cube helix. (https://www.mrao.cam.ac.uk/~dag/CUBEHELIX/) It gets the luminance stuff right, and always converts to gray scale correctly, but has a few tweakable parameters that I can use in different contexts. I've implemented it (inefficiently) as:

cubeHelix[z_, rot_: 1.0, start_: 0.5, γ_: 1.0, hue_: 1.0] := 
     Module[{m, fract, amp, angle},
         fract = z;
         angle = 2 π (start/3.0 + 1.0 + rot fract);
         fract = fract^γ;
         amp = hue fract (1 - fract) / 2.0;
         m = {{-0.1486, 1.78277}, {-0.29227, -0.90649}, {1.97294, 0.0}};
         m = fract + amp m.{{Cos[angle]}, {Sin[angle]}};
         m = Clip[m, {0, 1}];
         RGBColor @@ Flatten@m
     ]

For instance, I might use ColorFunction -> (cubeHelix[#, 0, 2.8]&) to get

enter image description here

or ColorFunction -> (cubeHelix[#, 0, 0.8]&) to get

enter image description here

This maintains the perceptual property of the plots, but allows me to differentiate between plots when I'm giving a presentation, or making a display for the control room.

The default ColorFunction -> cubeHelix looks a lot like some of the other rainbow options:

enter image description here

Which I think also suffers from the green-as-middle problem when you're trying to plot things that go from (-1,1).

$\endgroup$
  • $\begingroup$ That's great; +1 $\endgroup$ – Sektor Jun 23 '15 at 13:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.