# How to change ColorFunction after plotting

Say I have a plot like

plot = DensityPlot[ Exp[-((x-5)^2+(y-5)^2)/5^2],
{x, 0, 10}, {y, 0, 10}, PlotRange -> {0,1} ]


Is there a way to change the ColorFunction after the fact, as an operation on plot (say change it to ColorFunction -> "Temperature") without changing the initial plot instructions?

-
I'm assuming you want to change the colors used in an existing plot, not replot it with a different set of colors, correct? – rcollyer May 8 '12 at 3:24
@rcollyer Yeah. Basically, I generate lots of ListDensityPlots from a large dataset, then want to use a different ColorFunction for a select one or two of them. – Eli Lansey May 8 '12 at 4:37
plot is a graphics object, it does not retain information of the underlying mathematical function used to construct it (i.e. you cannot access the "z" values). You can replace the VertexColors in the graphics object, but you'd need to map rgb values between colormaps. – kadrach May 8 '12 at 4:43

One solution that works quite well and is fast (interpolation would've been slow), is to use Nearest on a fine grid. The advantage is that you don't have to re-evaluate your plot, or modify it prior to generating it like Yu-Sung's answer does. Here's an example:

(* Create a rule list of original RGB colors to value and a nearest function *)
list = List @@ ColorData["LakeColors"][#] -> # & /@ Range[0, 1, 0.001];
nf = Nearest[list];

(* function to convert original RGB triplets to those of chosen color data *)
convert[s_String][x_List] := List @@ ColorData[s][First@nf[x]]

(* Apply to plot *)
plot /. Rule[VertexColors, x_List] :> Rule[VertexColors, (convert["Temperature"] /@ x)]


-
That works (+1), but FindMinimum works too and requires no grid. – Jens May 8 '12 at 6:27
@Jens Yeah, I thought about FindMinimum, but wasn't sure about how robust it would be for arbitrary and complicated colormaps (I'm not questioning it, just am too sleepy/lazy to think through... ) – R. M. May 8 '12 at 6:34
Agreed, there are bound to be cases where FindMinimum will give a wrong local minimum. – Jens May 8 '12 at 6:44
One can make this ~30 times faster by setting nr = Nearest[list] before hand, and then doing convert[s_String][x_List] := List @@ ColorData[s][First@nr[x]]. – Chip Hurst Feb 28 '14 at 21:28
@RiemannZeta Thank you for catching that! :) I've updated the post. – R. M. Feb 28 '14 at 21:35

# Inspection

Let's take a look at what is being returned from the plot:

plot = DensityPlot[Exp[-((x-5)^2 + (y-5)^2)/5^2], {x,0,10}, {y,0,10}, PlotRange -> {0,1}]


The output mainly consists of a big GraphicsComplex:

In[2]:= Head[Plot[[1]]]
Out[2]= GraphicsComplex


with color specified by VertexColors:

In[3]:= Short[Options[plot[[1]]]]
Out[3]= {VertexColors->{<<1>>}}


So in theory, by replacing VertexColors with other color values, you can change the colors without reevaluation.

### Good News

By default, DensityPlot uses color functions on z values and it is scaled (ColorFunctionScaling->True), which makes our task a bit easier since scaled z-values between 0 and 1 will be applied to color functions.

We need z-values because we need to apply color functions on them. But, the color function is already evaluated, and only RGB values are stored in VertexColors. Thankfully, the default color function is 1-1 function, but there is no such guarantee in general. Also, the inverse function of the color functions are not trivial (not that hard, just will take some time to compute--beating the purpose of not re-evaluating).

# Solution

A bit of cheating. But let's set up the graphics a bit so that it actually contains useful information. Instead of the default color function, let's use GrayLevel.

plot = DensityPlot[Exp[-((x-5)^2 + (y-5)^2)/5^2], {x,0,10}, {y,0,10},
PlotRange -> {0,1}, ColorFunction -> GrayLevel]


If you take a look at the vertex colors again, now it is in fact the same value as the scaled z-values of the function (and nicely packed too).

In[5]:= Short[Options[plot[[1]]], 4]
Out[5]= {VertexColors->{0.,0.0475548,0.0989165,0.150418,0.197553,0.23559,<<238>>,0.0113745,0.0113745,0.0113745,0.0113745,0.0113745}}


From here, now it is just a matter of applying a color function to the list of vertex colors--which is indeed scaled z-values--and replace it with the new values.

The position of VertexColors within output can be inspected by the following:

In[6]:= Position[Plot, HoldPattern[VertexColors -> _]]
Out[6] = {{1, 3}}


Usually, the positions in plot outputs are pretty stable unless you change options that modifies geometry (such as Exclusions), and there is a slight worry that Position may cause unpacking of nicely packed plot outputs (read this), so let's stick to this value for a while (of course, actual replace should be {1, 3, 2}, the second argument of the rule). A simple test:

ReplacePart[plot, {1,3,2} -> (ColorData["Rainbow"] /@ plot[[1, 3, 2]])]


It works nicely. Now with Manipulate:

Manipulate[
ReplacePart[plot, {1,3,2} -> (ColorData[gradient] /@ plot[[1, 3, 2]])],


# Problems

If the goal is to completely avoid Kernel evaluation, there is no easy solution--if not impossible, since ColorData will rely on the Kernel evaluation anyway.

Calling ColorData (and any other color functions, such as Blend or Hue for that matter) manually like this is not efficient in two ways;

First, internal plot functions do a lot of optimization for calling color data. Essentially it is getting a treatment like CompliedFunction, but more specialized. By manually calling them, you are not getting much speed up.

Secondly, the output form is a list of RGBColor[...] which makes the whole result unpacked. Not efficient memory-wise.

There is a way to achieve maximum efficiency by turning ColorData into a linear interpolation function of triples and using CompiledFunction or InterpolatingFunction, but I frankly don't think that it is worth...

-
The most complete answer! – VividD May 27 '15 at 14:27

The basic idea is to find the inverse function of the ColorFunction that your original plot uses, to reconstruct the scaled function values. Then apply the new ColorFunction to those values.

Here is how this would look for the special case when the original plot uses Hue:

colorfunction1 = Hue;
colorfunction2 = ColorData["Temperature"];
plot =
DensityPlot[
Exp[-((x - 5)^2 + (y - 5)^2)/5^2], {x, 0, 10}, {y, 0, 10},
PlotRange -> {0, 1}, ColorFunction -> colorfunction1]


Now I replace the Hue function:

p = plot /.
r_colorfunction1 :>
colorfunction2[Quiet[InverseFunction[colorfunction1][r]]]


So in principle, this works. However, Hue is special because it is invertible easily. For the default color scheme ("LakeColors") that's not so easy. InverseFunction seems to be unavailable for the blends appearing there.

So one would have to do the inversion by hand, probably using FindMinimum, applied to the difference between RGBColors in the VertexColors list and the RGBColors returned by the original color function. It's a FindMinimum problem because numerically the inversion may not work.

Edit

What I did with the Hue example was cheating in the same way that Yu-Sung Chang's solution based on GrayLevel was cheating, because the default color functions are more complicated. So here is something that works for the blends in the ColorData list, including the default LakeColors:

invertColor[rgb_, blend_, x_?NumericQ] := (#.#) &[
Apply[List, rgb] - Apply[List, blend[x]]];
changeColorFunction[pl_, colfn1_, colfn2_] :=
pl /. HoldPattern[
VertexColors -> l_] :> (VertexColors -> Map[colfn2[
Clip[
x /. Last@
Quiet@FindMinimum[invertColor[#, colfn1, x], {x, 0}], {0,
1}]] &, l])


The arguments in changeColorFunction[pl_, colfn1_, colfn2_] are the plot that you want to change, the original ColorFunction and the new one (both are assumed to have RGBColor values).

Here I'm applying it to a plot with default colors:

colorfunction2 = ColorData["Temperature"];
colorfunction1 = ColorData["LakeColors"];
plot = DensityPlot[
Exp[-((x - 5)^2 + (y - 5)^2)/5^2], {x, 0, 10}, {y, 0, 10},
PlotRange -> {0, 1}];
changeColorFunction[plot, colorfunction1, colorfunction2]


Another example with the same default plot:

changeColorFunction[plot, colorfunction1, ColorData["FallColors"]]


Probably it's worth reiterating that the inversion of the color function is a little tricky - but it seems to work well for the default colors.

-