38
$\begingroup$

I was recently faced with the task of creating a DensityPlot with a logarithmic colour scale, and with providing it with an appropriate legend. Since I could not find any resources to this effect on this site, I'd like to document my solution here.

For definiteness, suppose that I want a plot of the function $$ f(x,y)=\mathrm{sinc}^2(x)\mathrm{sinc}^2(y)=\frac{\sin^2(x)\sin^2(y)}{x^2y^2}, $$ which is the diffraction pattern of a square aperture, over a box of side 20. The problem with such a function, and the reason a logarithmic scale is necessary, is that the function has lots of detail over a wide range of orders of magnitude of $f$. Thus, doing a naive DensityPlot of it will produce either whited-out parts with a sharp boundary with the region where the contrast is acceptable, or one bright spot and lots of detail completely lost:

enter image description here

(Images produced by

DensityPlot[Sinc[x]^2 Sinc[y]^2, {x, -20, 20}, {y, -20, 20},
ColorFunction -> ColorData["DeepSeaColors"], PlotPoints -> 100, PlotRange -> u]

for u set to Automatic and Full respectively.)

Because of this, and particularly for plotting a function that has a zero, any solution to this problem must take as arguments the minimum and the maximum values of the range of interest. That range of interest is intrinsically hard-to-impossible for an automated range finder to obtain, so I'm OK with having to supply those values by hand.


To give a more concrete example of what the goal is, let me nick the final image from my answer:

$\endgroup$
44
$\begingroup$

Giving the density plot a logarithmic scale must always involve - unless some future version of Mathematica includes it by default - overriding the ColorFunctionScaling of the original plotting command and supplying a custom scaling function. The simplest logarithmic scaling is of the form $$ \mathrm{scaling}(x)=\frac{\log(x/\mathrm{min})}{\log(\mathrm{max}/ \mathrm{min})}, $$ which is given the parameters $\rm min$ and $\rm max$, and maps them respectively to 0 and 1, which are the limits of the standard input range of any colour function. This scaling function is implemented as

LogarithmicScaling[x_, min_, max_] := Log[x/min]/Log[max/min]

To include this in the plotting command, one needs to set ColorFunctionScaling to False, and supply LogarithmicScaling to some appropriate ColorFunction, which looks like

plotter[min_, max_] := DensityPlot[Sinc[x]^2 Sinc[y]^2, {x, -20, 20}, {y, -20, 20}
  , PlotPoints -> 100, PlotRange -> Full
  , ColorFunctionScaling -> False
  , ColorFunction -> (ColorData["DeepSeaColors"][LogarithmicScaling[#, min, max]] &)
]
plotter[0.00003, 1]

and produces

enter image description here


Getting the legend to work, on the other hand, is more tricky. The standard way to include a legend for a DensityPlot is to set some nontrivial option for PlotLegends; as the examples on the DensityPlot documentation show, the Automatic setting usually does a good job. For a logarithmic scale, on the other hand, the resulting BarLegend needs to be modified.

More specifically, the colour function provided to BarLegend must vary linearly with the input it is given (which goes linearly from bottom to top of the scale), and it is the ticks themselves that must be rescaled. This requires the inverse of the $\rm scaling$ function, which is given by $$ x=\mathrm{min} \left({\mathrm{max}}\over{ \mathrm{min}}\right)^{\mathrm{scaling}(x)}. $$ The strategy is to find those values of $x$ for which $\rm{scaling}(x)$ is 0, 1, and a given number of values evenly spread between the two. Thus:

  • We set PlotLegends to BarLegend,
  • we give BarLegend the unadulterated colour function we gave to the DensityPlot,
  • we tell BarLegend to take 0 and 1 as the minimum and maximum values to be fed to the colour function, to generate the full colour spectrum linearly,
  • we generate the list of positions of the ticks using min (max/min)^(Range[0, 1, 1/NumberOfTicks]),
  • we generate the colour-function-input they correspond to, and their labels, by mapping {LogarithmicScaling[#, min, max], ScientificForm[#, 2]} & over that list,
  • and we feed that to the Ticks option of BarLegend.

The resulting code looks like

plotter[min_, max_, NumberOfTicks_] :=  DensityPlot[Sinc[x]^2 Sinc[y]^2
  , {x, -20, 20}, {y, -20, 20}
  , PlotPoints -> 100, PlotRange -> Full
  , ColorFunctionScaling -> False
  , ColorFunction -> (ColorData["DeepSeaColors"][LogarithmicScaling[#, min, max]] &)
  , PlotLegends -> BarLegend[{ColorData["DeepSeaColors"], {0, 1}}, LegendMarkerSize -> 370 
             , Ticks -> ({LogarithmicScaling[#, min, max], ScientificForm[#, 2]} & /@ (
                                          min (max/min)^Range[0, 1, 1/NumberOfTicks]))]
]
plotter[0.00003, 1, 5]

and produces output like

enter image description here

The highlighter colours the Ticks option inside BarLegend in red, but it works just fine as far as I can tell. The error class this is assigned to (visible e.g. by changing the colour setting in Edit > Preferences > Appearance > Syntax Coloring > Errors and Warnings) is "Unrecognized option names". I don't think this is particularly bad, but rather reflects the fact that the highlighter is not perfect, and should not really be expected to be.


Addendum: minor ticks.

While the above is perfectly fine, the ticks do not make it immediately clear that the scale is logarithmic in the way that appropriately placed minor ticks will do. To implement these, the best option is to take advantage of the built-in capability to make nice ticks in log scale plots.

The essential part of this is to extract, using AbsoluteOptions, the Ticks of an appropriate LogPlot. Unfortunately, the linearized coordinates of the ticks are rather inconveniently placed, and have an arbitrary linear scale of their own. The code below is therefore rather long, but I've made it verbose so that hopefully it's clear what's going on.

LogScaleLegend[min_, max_, colorfunction_, height_: 400] := Module[
  {bareTicksList, numberedTicks, m, M, ml, Ml, minInArbitraryScale, 
   maxInArbitraryScale, linearScaling},
  bareTicksList = 
   First[Ticks /. AbsoluteOptions[LogLogPlot[x, {x, min, max}]]];
  numberedTicks = (
     Select[
      bareTicksList /. {Superscript -> Power},
      NumberQ[#[[2]]] &
      ]
     )[[All, {1, 2}]];
  m = Min[numberedTicks[[All, 2]]];
  M = Max[numberedTicks[[All, 2]]];
  ml = Min[numberedTicks[[All, 1]]];
  Ml = Max[numberedTicks[[All, 1]]];
  {minInArbitraryScale, maxInArbitraryScale} = 
   ml + (Ml - ml) Log[{min, max}/m]/Log[M/m];
  linearScaling[x_] := (x - minInArbitraryScale)/(
   maxInArbitraryScale - minInArbitraryScale);

  DensityPlot[y
   , {x, 0, 0.04}, {y, 0, 1}
   , AspectRatio -> Automatic, PlotRangePadding -> 0
   , ImageSize -> {Automatic, height}
   , ColorFunction -> colorfunction
   , FrameTicks -> {{None,
      Select[
       Table[{
         linearScaling[r[[1]]],
         r[[2]] /. {Superscript[10., n_] -> Superscript[10, n]},
         {0, If[r[[2]] === "", 0.15, 0.3]},
         {If[r[[2]] === "", Thickness[0.03], Thickness[0.06]]}
         }, {r, bareTicksList}]
       , (#[[1]] (1 - #[[1]]) >= 0 &)]
      }, {None, None}}
   ]
  ]

With this function, then, the code

plotter[min_, max_, NumberOfTicks_] := DensityPlot[
  Sinc[x]^2 Sinc[y]^2
  , {x, -20, 20}, {y, -20, 20}
  , PlotPoints -> 100
  , PlotRange -> Full
  , ColorFunctionScaling -> False
  , ColorFunction -> (ColorData["DeepSeaColors"][
      LogarithmicScaling[#, min, max]] &)
  , PlotLegends -> 
   LogScaleLegend[min, max, ColorData["DeepSeaColors"], 350]
  ]
plotter[0.00003, 1, 5]

produces the output

enter image description here

| improve this answer | |
$\endgroup$
  • $\begingroup$ This kind of workaround still seems to be necessary to obtain a logarithmic ListDensityPlot in Mathematica 11. You just saved me a couple of days' work! $\endgroup$ – Mark Mitchison Dec 16 '16 at 8:55
  • $\begingroup$ Glad it's useful ;-). Ulm keeping you busy, then? $\endgroup$ – Emilio Pisanty Dec 16 '16 at 10:58
  • 1
    $\begingroup$ Actually this was for my PhD corrections. But yeah I'm definitely not struggling to find work here... too many projects! $\endgroup$ – Mark Mitchison Dec 16 '16 at 11:00
  • $\begingroup$ This is awesome! Quick question: how come it doesn't change the legend ticks when I change 'NumberOfTicks' from 5 to 10 in the log one? $\endgroup$ – Elsa Jun 5 '19 at 17:55
9
$\begingroup$

With version 11, you can use the ScalingFunctions in both DensityPlot and BarLegend

sf = Log[#/0.00003]/Log[1/0.00003] &;
isf = InverseFunction[sf];
DensityPlot[Sinc[x]^2 Sinc[y]^2, {x, -20, 20}, {y, -20, 20},
 PlotRange -> All, PlotPoints -> 100,
 ScalingFunctions -> {sf, isf},
 ColorFunction -> "DeepSeaColors",
 PlotRange -> {0.00003, 1},
 ColorFunctionScaling -> False,
 PlotLegends -> BarLegend[{"DeepSeaColors", {0.00003, 1}},
   ScalingFunctions -> {sf, isf}]]

Mathematica graphics

| improve this answer | |
$\endgroup$
  • $\begingroup$ Huh, figures. Nice catch. $\endgroup$ – Emilio Pisanty Aug 8 '16 at 18:29
  • $\begingroup$ Yeah, I have many examples where I put in tons of time to write a plotting function or implement an algorithm, only for someone to point out the built-in that does it 10 times faster (not the case here). Takes some of the fun out of it..... $\endgroup$ – Jason B. Aug 8 '16 at 18:37
  • 2
    $\begingroup$ Yeah. This comes to mind, so don't feel guilty for undercutting my answer ;-), built-ins are usually better anyway. I'll leave the other one accepted for a while until it's a safe assumption that a lot of people have v11 (including me, at least), and then switch. $\endgroup$ – Emilio Pisanty Aug 8 '16 at 19:13
  • $\begingroup$ thanks, It seems to work for me with version 11, although Mathematica marks ScalingFunctions in red and says it is an unknown option $\endgroup$ – Wouter Nov 28 '16 at 10:44
  • 1
    $\begingroup$ Do you have solution that would allow for using the $10^{-1}, 10^0, 10^1, 10^2$ format, and would also include minor tick marks which are unlabelled and shorter than the major ones? $\endgroup$ – Szabolcs Oct 30 '19 at 12:06
0
$\begingroup$

This is not meant to be an answer but a comment to Emilio's post. I do not have enough reputation to comment on the post.

I had a few issues with the LogScaleLegend function. ( I am using Mathematica v10.1) Specifically, the list "numberedTicks" contained zeroes and didn't contain all the tick values properly. The following dirty fix works for me: Modify the definition of numeredTicks:

numberedTicks = 
 Select[{bareTicksList[[All, 
     1]], (# /. {List -> Times, Row -> myF} &) /@ (bareTicksList[[All,
         2]] /. {Superscript -> Power, 
        Spacer -> Round})}\[Transpose], (#[[2]] > 0) && 
    NumberQ[#[[2]]] &]

and use the helper function:

myF[x_, y_] := x;

I hope this can be of help!

| improve this answer | |
$\endgroup$
  • $\begingroup$ Have you got a MWE that shows exactly how the examples in my answer are unsatisfactory? $\endgroup$ – Emilio Pisanty Dec 5 '19 at 8:12
  • $\begingroup$ Hi Emilio, Don't get me wrong, your function has been great to use! I think that depending on the version of mathematica, the ticks that you grab from the LogLogPlot are formated differently, and this makes the behavior less reliable. For example, when I use your code on min=0.001 and max=0.1: bareTicksList = First[Ticks /. AbsoluteOptions[LogLogPlot[x, {x, 0.01, 0.1}]]]; numberedTicks = (Select[bareTicksList /. {Superscript -> Power}, NumberQ[#[[2]]] &])[[All, {1, 2}]] It then returns only one tick: ` {{-6.90776, 0.001}}` $\endgroup$ – faelx Dec 6 '19 at 2:00
  • $\begingroup$ I had a closer look at what min-max values make a problem. It seems to be a very specific issue. for {min, max} = {10^-7, 2 10^-4} , your code returns {} whereas I manage to grab 5 ticks. for {min, max} = {10^-7, 1 10^-4} both codes work. In the working example,FullForm@(bareTicksList[[All, {1, 2}]] /. {Superscript -> Power}), returns a list containing List[-16.1181, 1.*10^-7] whereas in the non working example the list contains elements of the form: List[-16.1181, Row[List[1., 1.*10^-7], "\[Times]"]]. $\endgroup$ – faelx Dec 6 '19 at 2:40
  • $\begingroup$ So to elaborate on my previous comment, it seems that (in my version of Mathematica at least), LogLogPlot formats the tick labels differently depending on the range, which makes it difficult to grab the values for every single case. $\endgroup$ – faelx Dec 6 '19 at 2:44

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.