How to change ColorFunction after plotting

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.

Bad News

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]])]

enter image description here

It works nicely. Now with Manipulate:

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

enter image description here

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...


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)]

enter image description here enter image description here


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]

hue plot

Now I replace the Hue function:

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

Temperature

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]

Temperature

Another example with the same default plot:

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

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.