57
$\begingroup$

I have written some custom functions to draw multi-panel graphs like this one: enter image description here

It's done by passing a matrix of (custom) plotting functions to a MultiPanelGraph function, which pulls apart the head, argument and options of those plots, adds a few of its own, and then puts it all together. There is also some funkiness to deal with plot labels and footnotes, but I've removed these from the code below for simplicity. The PanelHeightFactor type options are custom options to the custom functions for the individual plots. Likewise the MultiPanel option tells the custom functions for the individual plots to, for example, turn off ticks and tick marks on particular sub-plots so that they all fit together neatly as shown in the picture above, and to turn the PlotLabel into a panel label inside the plot frame using Prolog.

Attributes[MultiPanelGraph]={HoldFirst};
MultiPanelGraph[{{l_[largs__,lopts___Rule], r_[rargs__,ropts___Rule]}}, 
 mpopts:OptionsPattern[{MultiPanelGraph,myLineGraph}]] := 
Module[{ (* there was some stuff here *)},With[{
    wl = PanelWidthFactor /.{lopts}/.{PanelWidthFactor->1/2},
    wr = PanelWidthFactor /.{ropts}/.{PanelWidthFactor->1/2},
    Grid[{{l @@ Join[{largs},{lopts}, 
       {MultiPanel-> {All,Left},PanelHeightFactor->1,PanelWidthFactor->wl,
        ImageMargins->0}],
          r @@ Join[{rargs},{ropts}, 
       {MultiPanel->{All,Right},PanelHeightFactor->1,
        PanelWidthFactor->wr,ImageMargins->0}]}},
     Sequence @@ FilterRules[Join[{mpopts},{lopts},{ropts}],Grid],
    Spacings -> {0,0} , ItemSize -> {{4.8+42.*wl, 4.+42.*wr},Full}, Alignment -> Left]

It works fine (aside from some issues to do with ImageSize being measured in points and ItemSize being measured in x-heights or some silly thing - a topic for another question), but I would have to code every single case - 2 by 1, 2 by 2, 3 by 4, whatever else some manager decides he or she wants - separately. Doable, but tedious.

I can imagine capturing the dimensions of the grid to get the appropriate default PanelWidthFactor and PanelHeightFactor, but beyond that I don’t know if there is a neat way to capture all the possible cases in one definition of the function. Does anyone have any suggestions for coding this in a way that doesn’t require each separate case to be coded to match separately? It would have to handle telling the subplot which ticks to use depending on its position.

EDIT: A couple of additional business requirements that might be useful to know:

  • It's essential for the plot frame to be the same width and height, regardless of how many panels there are, unless this is explicitly overridden (in my code, by having an explicit PanelHeightFactor or PanelWidthFactor option in the code for the subgraph).
  • Panels can be of dated data (DateListPlot or a custom DateListBarChart) or undated data (ListPlot, BarChart), but never both in the same chart.
  • The subplots will have some options (namely footnotes and source notes) that need to be captured and merged to be shown down the bottom of the main plot.
$\endgroup$
1
  • 1
    $\begingroup$ Side note: nowadays PlotLayout option has been extended to support Plot, ListPlot, etc. (Can't remember when these happen, maybe v12.0?) Also, there's a ResourceFunction["PlotGrid"] in Wolfram Function Repository. Their styles are not exactly the same as requested in the question, but if you're not aware of them, check their documents and see if they're what you need! :) $\endgroup$
    – xzczd
    Jan 25, 2023 at 3:31

2 Answers 2

67
$\begingroup$

Of course I can't know all the details of what you really want, but here is a simple starting point, plotGrid, that takes care of the relative sizing and the tick label display automatically.

Instead of using panels or grids, I'm placing everything into a single Graphics by using a separate Inset for each plot in the list l. The dimensions of this list l fully determine the layout of the result, under the constraints of the total width and height of the "grid" which are specified as the second and third arguments to the function.

The plots that are passed to the function in l must have the frame ticks enabled in all directions in which they could potentially be displayed. That is, you coud for example, prepare all the plots with FrameTicks -> All, or do the analogous thing manually.

Here is the code for the function:

Options[plotGrid] = {ImagePadding -> 40};
plotGrid[l_List, w_, h_, opts : OptionsPattern[]] := Module[{nx, ny,
   sidePadding = OptionValue[plotGrid, ImagePadding],
   topPadding = 0,
   widths,
   heights,
   dimensions,
   positions,
   frameOptions = 
    FilterRules[{opts}, 
     FilterRules[Options[Graphics], 
      Except[{ImagePadding, Frame, FrameTicks}]]]},
  {ny, nx} = Dimensions[l];
  widths = (w - 2 sidePadding)/nx Table[1, {nx}];
  widths[[1]] = widths[[1]] + sidePadding;
  widths[[-1]] = widths[[-1]] + sidePadding;
  heights = (h - 2 sidePadding)/ny Table[1, {ny}];
  heights[[1]] = heights[[1]] + sidePadding;
  heights[[-1]] = heights[[-1]] + sidePadding;
  positions = 
   Transpose@
    Partition[
     Tuples[Prepend[Accumulate[Most[#]], 0] & /@ {widths, heights}], 
     ny];
  Graphics[
   Table[
    Inset[
     Show[
      l[[ny-j+1, i]],
      ImagePadding -> {
        {If[i == 1, sidePadding, 0], If[i == nx, sidePadding, 0]},
        {If[j == 1, sidePadding, 0], 
         If[j == ny, sidePadding, topPadding]}
        },
      AspectRatio -> Full
      ],
     positions[[j, i]],
     {Left, Bottom},
     {widths[[i]], heights[[j]]}],
    {i, 1, nx}, {j, 1, ny}
    ],
   PlotRange -> {{0, w}, {0, h}},
   ImageSize -> {w, h},
   Evaluate@Apply[Sequence, frameOptions]
   ]
  ]

The Inset takes care of the relative sizing and the correct placement of the individual plots. To do this most easily, I set the PlotRange of the enclosing Graphics in which the Insets live to {w, h}, identical to the desired ImageSize of the result.

To illustrate what it does, there is a bit of algebra to make the Insets containing displayed tick labels wide enough so that the plot frames appear equally sized everywhere. The showing and hiding of the frame tick labels is simply accomplished by setting ImagePadding -> 0 on the interior-facing edges of each plot.

For the exterior-facing edges, the option value of ImagePadding is used. That value has to be set large enough to show all labels without cutting them off. One could combine this with the solutions in this answer to improve this a little, but for the purposes of this answer I don't think that's the crucial point.

To show how the function works, here is a grid of example plots (I chose simple sine functions - economics data are not my thing):

pt = Table[
  Plot[Cos[2 Pi m x + Pi/4] Sin[2 Pi n x], {x, -1, 1}, 
   Frame -> True, FrameTicks -> All, PlotRangePadding -> .1, 
   PlotRange -> {-1.1, 1.1}, Background -> Hue[m n/7]], {m, 1, 3}, {n,
    1, 2}];

Here I intentionally added a Background color even though it looks weird - the goal is to show more clearly how my function works:

plotGrid[pt, 500, 300, ImagePadding -> 40]

grid

What the colors show is where the ImagePadding has been set to zero, and where it extends beyond the plot frame to reveal the labels (which are actually present around all plots).

The function takes the option ImagePadding as shown above.

Here is another example with a larger matrix of plots, using the default ImagePadding:

pt = Table[
  Plot[Cos[2 Pi m x + Pi/4] Sin[2 Pi n x], {x, -1, 1}, 
   Frame -> True, FrameTicks -> All, PlotRangePadding -> .1, 
   PlotRange -> {-1.1, 1.1}, Background -> Hue[m n/7]], {m, 1, 3}, {n,
    1, 4}];

plotGrid[pt, 500, 400]

grid 2

I've focused only on the logic to arrange the plots, and that still leaves you to figure out how to make the plots look good when they are placed in the grid. For example, one has to take care to avoid tick labels at the very extreme ends of the frame because they tend to get too close to the edge and get cut off when facing a neighboring plot.

Also, one could add If statements to suppress the FrameTicks at certain edges. That's pretty easy to add to the framework above, but I'll leave it out for now.

Also, if you don't like the labels appearing at the top of the grid, you could just set your individual plots to FrameTicksStyle -> {{Black, Black}, {Black, Transparent}}.

$\endgroup$
16
$\begingroup$

The nub of the answer turned out to rest on solutions to this question, particularly Heike's.

The trick is to Map Hold onto the subplots at the required level, and also to extract the Head of the plotting function in the right way. Heike's answer to that question gives a simple version of the solution. Below is a cutdown version of my actual code, where myLineGraph etc are custom functions that take MultiPanel, PanelHeightFactor and PanelWidthFactor options that determine the size of the graph panel relative to a standard dimension, and which sets of frame labels should show on the graph.

MultiPanelGraph[gs_?MatrixQ,mpopts:OptionsPattern[{MultiPanelGraph,myLineGraph,myBarGraph,myUndatedLineGraph}]]:=
 Module[{nr,nc, gsheld,subrules,subargs,subplots, pwfs,phfs,mph = OptionValue[Title]},
    {nr, nc} = Dimensions[gs];
    gsheld = Map[Hold, Unevaluated[gs], {2}];
    subrules = Table[Cases[gsheld[[i,j]],_Rule,{2}],{i, nr},{j, nc}];         
      (* collects all the options explicitly set for each sub-plot *)
    subargs =  Table[Cases[gsheld[[i,j]],Except[_Rule],{2}],{i, nr},{j, nc}]; 
      (* collects all the non-option arguments for each sub-plot *)
    pwfs = Flatten@Table[PanelWidthFactor /.{subrules[[1,j]]}/.{PanelWidthFactor->1/nc},{j, nc}]; 
      (* assume only options specified in first row graphs override default PWF *)
    phfs = Flatten@Table[PanelHeightFactor /.{subrules[[i,1]]}/.{PanelHeightFactor->1/nr},{i, nr}];  
     (* assume only options specified in first row graphs override default PHF *)
    subplots = Table[gsheld[[i, j, 1, 0]]@@ Join[{Flatten[subargs[[i,j]],1]},{subrules[[i,j]]}, 
       {MultiPanel-> {Switch[i,1,If[nr==1,All,Top],nr,Bottom,_,Middle],
        Switch[j,1,If[nc==1,All,Left],nc,Right,_,Center]}, 
     (* determines panel side - top, left etc. Captures case 
      where only one row or one column - NB 1,1 will not look like a one-panel graph *)
        PanelHeightFactor->phfs[[i]],PanelWidthFactor->pwfs[[j]],ImageMargins->0}],
         {i,nr},{j,nc}];  
      (* put head and args of subplots together with other arguments 
      that need to be passed to them *)
        Grid[subplots, Alignment->Left, Spacings->{0,0}, ItemSize-> {Full, Full}]
 ]
$\endgroup$
7
  • $\begingroup$ I still think this code could be a lot cleaner without all the Table stuff as I tried to show in my answer to the referenced question. $\endgroup$
    – Mr.Wizard
    Jun 30, 2012 at 6:43
  • $\begingroup$ @Mr.Wizard I agree yours was neater, but in the context of the actual application, this approach worked better with what I already had. The total package is about 120kb and there were things I didn't want to mess too much with. The real version also has footnotes, source notes, titles and subtitles, and pulls the title and subtitle for each subgraph and moves it inside the plot are as Inset text in the Prolog option. Huge amounts of complexity. I also had to allow for the fact that, if my version is ever used by others in my firm, they have to be able to follow it. $\endgroup$
    – Verbeia
    Jun 30, 2012 at 6:47
  • 1
    $\begingroup$ Okay. :-) I'm a compulsive code compactor. $\endgroup$
    – Mr.Wizard
    Jun 30, 2012 at 6:58
  • 1
    $\begingroup$ Using Table also meant I was subsequently able to extend this code to handle SpanFromLeft and SpanFromAbove cases. $\endgroup$
    – Verbeia
    Nov 21, 2013 at 21:02
  • $\begingroup$ How do I use this? Jens's answer doesn't seem to work in my use case... And in OptionsPattern, is the first element of the list there MultiPanel instead of MultiPanelGraph? $\endgroup$
    – Taiki
    Oct 6, 2015 at 17:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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