11
$\begingroup$

Simple Context

This is a question about understanding how Inset really works and how GraphicsColumn and the like automatically calculate pos, opos, and size (and what those really mean) in Inset[fig, pos, opos, size]. Related posts that I have perused at (variable) length include

I am still digesting the first, and the second doesn't solve the particular problem I am facing (although it is exceedingly useful).

Greater Context

I have an established workflow that allows me to generate journal-ready figures with correct widths, correct font sizes, good quality, and small-enough file size. The journals in which I publish are usually two-column format, so I often have multiple figures that include (at least) two plots in a column format. An archetypical example is shown Framed below on the right:

enter image description here

(By the way, the color function was adapted from the seaborn color schemes as worked out in the answers to this question.)

The workflow typically involves something like the following.

  • Generate the figures using Jens's amazing contourDensityPlot. (This allows Exporting to pdf that eliminates the lines of the density plot mesh.)

  • Set the correct FontSize and ImageSize so that the exported figure will match the font size and column width used in the journal, based on suggestions found in Szabolcs's $\LaTeX$ and Mathematica answer. (This is a key point: re-sizing the compound figure later can be an issue, since the fonts in the figure should match the fonts in the paper.)

  • Use a generalization of the automated ImagePadding from Szabolcs's answer here to align the figures in a GraphicsColumn. Use Epilog in the GraphicsColumn to add the annotations $(a)$ and $(b)$. (This is important: I cannot use Column because this disallows Epilog.)

  • Manually go into the automated Insets that GraphicsColumn creates and hack the size so that the compound figure width matches the widths of the component figures. This step is to get rid of the extra white space (highlighted by including the Frame) as shown in the figure on the left above. (The main question posed in this post is about automating this step. I walk through an example below.)

  • Export as pdf and include in a document type-setted in $\LaTeX$.

(Somewhat Vague) Question

Inset sizing is mysterious to me, so I use GraphicsColumn to get the sizing about right and then, by trial and error, change the size until I get what I want. Based on the example below, can someone explain why GraphicsColumn creates a Graphics object that has all of the extra white space, why my hack works, and how the fix can be automated? This question will become less vague with a specific example:

Example

We consider two plots:

p1 = Plot[Table[{x, x^2}, {x, 0, 1, 0.5}]
  , Frame -> True, FrameLabel -> {None, "y"}
  , BaseStyle -> {FontSize -> 10}
  , ImageSize -> 255
  ]
p2 = ListPlot[Table[{x, x^2}, {x, 0, 1, 0.25}]
  , Frame -> True, FrameLabel -> {"x", None}
  , BaseStyle -> {FontSize -> 10}
  , ImageSize -> 255
  ]

The framed parts of these figures have the same implicit AspectRatio, but that will not be the case in general. In addition, due to the different FrameLabels, the actual aspect ratios of the figures are necessarily different. Therefore, we need to add equal ImagePaddings to the two plots. If we were to able to use Column, we could set just the left and right padding using the trick by Szabolcs and Heike mentioned above to automatically extract the padding:

padding[g_Graphics] := With[
 {im = Image[Show[g, LabelStyle -> White, Background -> White]]}
 , BorderDimensions[im]
]
leftRightPadding[graphicsSequence__Graphics] := {
  1 + Max /@ Transpose@(First /@ #) & @ (padding /@ List@graphicsSequence)
  , {Automatic, Automatic}
}

ipLR = leftRightPadding[p1, p2]
(* {{36, 4}, {Automatic, Automatic}} *)

Then, we can make perfectly sized and spaced image using Column that can be directly exported:

Column[Show[#, ImagePadding -> ipLR] & /@ {p1, p2}]
Export["fig1.pdf", %]

resulting in (this is actually exported as a png)

enter image description here

which will be the perfect size for including in a $\LaTeX$ document. Unfortunately, I cannot use Epilog to add figure labels (a) and (b). So we use GraphicsColumn instead, but in order to line up the frame correctly, we need to have equal padding on all sides (which is something I don't understand). Therefore we use

maxPadding[graphicsSequence__Graphics] := 1 + {
  Max /@ Transpose@(First /@ #)
  , Max /@ Transpose@(Last /@ #)
 } & @ (padding /@ List @ graphicsSequence)

ip = maxPadding[p1, p2]
(* {{36, 4}, {37, 1}} *)

and generate the figure using

GraphicsColumn[Show[#, ImagePadding -> ip] & /@ {p1, p2}
 , ImageSize -> 255
 , Frame -> True]
Export["fig2.pdf", Show[%, ImageSize -> 255]]

resulting in

enter image description here

Normally I would not include the Frame -> True, but I have included it here to show the extra white space to the left and right (and usually top and bottom) of the figures.

At this point, to get rid of the extra white space, I play around with the Inset parameters until I hit upon something that works. Here, I did:

GraphicsColumn[Show[#, ImagePadding -> ip] & /@ {p1, p2}
 , ImageSize -> 255
 , Spacings -> 17
 , Frame -> True
] /. {Inset[a__, size_] :> Inset[a, {1.12, 1.33} size]}

I found the scale factors 1.12 and 1.33 that modify the quantities in size by trial and error. In addition, I need to set the Spacings by hand, which introduces another bottleneck to workflow. Finally, I will need to modify these so that there actually is a little white-space to the left for the Epilog -> {Text["(a)", posA], Text["(b)", posB]}.

Despite these annoyances, the hack works. Below, I am showing an Exported Row of the Column solution and the hacked GraphicsColumn solution side-by-side. They are almost indistinguishable.

enter image description here

So, the question is, what have I done? I have very little idea how this works, and ideally there would be a solution that allows me to automate the process. (By the way, another option is to export the final figure with an image size larger than the column width and then clip the figure when I \includegraphics in the $\LaTeX$ document; this is obviously another non-ideal trial-and-error solution.)

Some extra poking around

Just to show that something strange (to me) is going on, below are some screen shots of the combined figures with the component figures selected. First, the Column solution with the second figure selected:

enter image description here

Second, the non-hacked GraphicsColumn solution with second figure selected:

enter image description here

You can see how the border of the selection is wider and taller (barely) in the second, even though both versions have the same left-right ImagePadding. (I have played around with this more, including making the ImagePadding the same around the entire figure for the Column solution; the same sorts of thing happen.) Third, the final side-by-side figure with the second figure in the GraphicColumn solution selected:

enter image description here

You can see how the border of the selection is now right up against the left and right sides of the figure.

Finally, just for fun, note that if we give the same padding to the figures only on the right and left, then GraphicsColumn doesn't even line up the frames correctly. Calling

GraphicsColumn[Show[#, ImagePadding -> ipLR] & /@ {p1, p2},  ImageSize -> 255]

results in

enter image description here

This is the final mystery, I guess. Thank you for slogging through this long tale.

$\endgroup$
  • 1
    $\begingroup$ I have attacked this problem many times and prefer to generate my own Graphics[{Inset[...],Inset[...],...}] with appropriate options. See if this helps you: mathematica.stackexchange.com/a/79773/26956 (specifically, my function undistortedGraphicsColumn, easily generalizable to GraphicsGrid functionality). $\endgroup$ – LLlAMnYP Jul 15 '15 at 14:01
  • 2
    $\begingroup$ The source of this behavior is that GraphicsColumn and co. allocate equal space to every enclosed Graphics, whereas Column stacks them as densely as possible. Additional conflict between e.g. the aspect ratio of the plots and the aspect ratio of the entire GraphicsColumn can cause further frustration (excessive white-space) $\endgroup$ – LLlAMnYP Jul 15 '15 at 14:08
  • $\begingroup$ @LLlAMnYP. Thanks for the link. I didn't come across that one in my searches for some reason. I'll give it a look. (I figured that somehow this was a duplicate but couldn't find the specific solution.) $\endgroup$ – march Jul 15 '15 at 14:23
  • $\begingroup$ @LLlAMnYP. Your function undistortedGraphicsColumn works great, and your explanation in that post seems to identify the problem (I feel like I should've noticed that GraphicsColumn lazily chooses the Frame AspectRatio rather than the full Graphics aspect ratio, for instance). My question is definitely a duplicate, then. I'm inclined to close it, then, but link to your posted solution so that all of this will be easily searchable, since it's such a common problem. $\endgroup$ – march Jul 15 '15 at 17:10
6
$\begingroup$

I decided to modify undistortedGraphicsColumn from LLlAMnYP's post to work for my purposes and extend it to an undistortedGraphicsGrid. Because I am particularly interested in aligning the plot region frames carefully, I have changed the names to alignedGraphicsColumn and alignedGraphicsGrid

Caveats, up front:

  • alignedGraphicsColumn is a straight-forward extension of undistortedGraphicsColumn, but I feel like I've done just enough to warrant not closing the question as a duplicate and to supply my own answer.

  • I've introduced some custom Options, but my implementation of them is messy and needs some work. Nonetheless, they are somewhat robust and allow control over the image size after the original plots have been made, which I think is pretty useful. Furthermore, there is some choice as to how the alignment should be done (at least for the Column version).

  • I have implemented Spacings functionality, but there is no extensive testing of how well it works. It has worked on the simple cases that I tend to need.

  • We can pass Graphics Options to the outer Graphics but not to the sub-plots that are Inset in the outer Graphics. This may or may not be useful.

  • alignedGraphicsGrid is not very robust yet and really only works for plots whose plot regions have the same AspectRatio and whose image widths are all the same. In addition, sometimes the plots will be cut off, and I need to manually add some ImagePadding to the entire figure. I plan on extending it to include cases where each column can have a different width and each row can have a different width, but by then I'm encroaching on SciDraw territory, and I may as well just use that instead.

The code is included at the bottom of each section below.

For testing, let's use the plots

p1 = Plot[x, {x, 0, 1}
   , Frame -> True, FrameLabel -> {None, "left"}
   , BaseStyle -> {FontSize -> 10}
   , ImageSize -> 300
   ];
p2 = Plot[x^2, {x, 0, 1}
   , Frame -> True, FrameLabel -> {{None, "right"}, {"bottom", None}}
   , BaseStyle -> {FontSize -> 10}
   , ImageSize -> 255 , AspectRatio -> 1
   ];
p3 = Plot[(x - 1)^3, {x, 0, 1}
   , Frame -> True, FrameLabel -> {"bottom", None}
   , BaseStyle -> {FontSize -> 10}
   , ImageSize -> 255
   ];

alignedGraphicsColumn

Options

  • Accepts Graphics options for the whole image.

  • In the current implementation, ImageSize is broken, but it's functionality is mimicked with:

  • SubImageWidth: an option that determines the figure width.

    • Choosing a positive real number x mimics the behavior of ImageSize -> x.
    • Full scales all of the sub-plots so that they all have the width of the sub-plot with the largest width, and the ImageSize is then this width.
    • Automatic is the default: the ImageSize is the width of the sub-plot with the largest width.
  • FrameAligned determines the alignment functionality.

    • All: all plots get the same ImagePadding, and so the plots get lined up nicely, with lots of space between them, which can be fixed with Spacings.
    • LeftOnly: all plots get the same ImagePadding on the left and the right, which aligns the left sides of the frames only, unless all sub-plots have the same ImageSize.
    • None: applies no ImagePadding, so the plots do not line up.
  • Spacings: works as advertised in GraphicsGrid and related functions.

Examples

Consider the function calls below

alignedGraphicsColumn[{p1, p2, p3}
 , FrameAligned -> All , SubImageWidth -> 200
 , Spacings -> 20, Background -> LightBlue]
alignedGraphicsColumn[{p1, p2, p3}
 , FrameAligned -> LeftOnly, SubImageWidth -> Automatic
 , Spacings -> 20]
alignedGraphicsColumn[{p1, p2, p3}
 , FrameAligned -> LeftOnly, SubImageWidth -> Full]
alignedGraphicsColumn[{p1, p2, p3}, FrameAligned -> None]

that result in the sequence of column figures below, respectively. For clarity, all of the figures have been wrapped in Framed[].

  • In the first, all of the sub-figures are lined up nicely, the width of the figure is 200, I have added vertical spacings of 20 between the sub-figures, and we have added a LightBlue Background to the entire figure.

  • In the second, the figures aren't all right-aligned because the first figure has a larger horizontal size than the others, and we have FrameAligned -> LeftOnly; SubImageWidth -> Automatic means that the width of the image will be the width of the largest sub-figures, but the sub-figures don't get their widths modified.

  • In the third, SubImageWidth -> Full makes it so that all sub-figures have the same width (equal to the width of the largest sub-figures). FrameAligned -> LeftOnly automatically aligns the right-hand sides in this case (a consequence of the widths of the figures being equal).

  • Finally, in the fourth, FrameAligned -> None means no extra padding is added to the figures, so their frames aren't lined up. There's no extraneous white space, though.

enter image description here

Code

The code requires the padding functions in the question above, included here for completeness:

padding[g_Graphics] := With[
 {im = Image[Show[g, LabelStyle -> White, Background -> White]]}
 , BorderDimensions[im]
]
leftRightPadding[graphicsSequence__Graphics] := {
  1 + Max /@ Transpose@(First /@ #) & @ (padding /@ List@graphicsSequence)
  , {Automatic, Automatic}
}
maxPadding[graphicsSequence__Graphics] := 1 + {
  Max /@ Transpose@(First /@ #)
  , Max /@ Transpose@(Last /@ #)
 } & @ (padding /@ List @ graphicsSequence)

Clear[alignedGraphicsColumn]
Options[alignedGraphicsColumn] = Join[{Spacings -> 0, FrameAligned -> LeftOnly, SubImageWidth -> Automatic}, Options[Graphics]];
alignedGraphicsColumn::faligned = "Value of option FrameAligned is not All, None, or LeftRight.";
alignedGraphicsColumn::width = "Value of option SubImageWidth is not Full, Automatic, or a positive machine number.";

alignedGraphicsColumn[list_, opts : OptionsPattern[]] := Module[{sizes, width, plots, optWidth = OptionValue[SubImageWidth]}
  , Which[
      And[optWidth =!= Automatic, optWidth =!= Full, ! NumericQ@optWidth]
      , Return[Message[alignedGraphicsColumn::width]]
      , optWidth === Automatic, plots = list
      , optWidth === Full
      , plots = Show[#, ImageSize -> Max@First@(ImageDimensions /@ list)] & /@ list
      , NumericQ@optWidth
      , Which[! Element[optWidth, Reals], Return[Message[alignedGraphicsColumn::width]]
      , optWidth <= 0, Return[Message[alignedGraphicsColumn::width]]
      , optWidth > 0, plots = Show[#, ImageSize -> optWidth] & /@ list]
      , True
      , plots = list
    ]
  ; Which[And @@ (OptionValue[FrameAligned] =!= # & /@ {LeftOnly, All, None})
      , Return[Message[alignedGraphicsColumn::faligned]]
      , OptionValue[FrameAligned] === LeftOnly
      , plots = Show[#, ImagePadding -> leftRightPadding @@ plots] & /@ plots
      , OptionValue[FrameAligned] === All
      , plots = Show[#, ImagePadding -> maxPadding @@ plots] & /@ plots
      , OptionValue[FrameAligned] === None
      , True
    ]
  ; sizes = ImageDimensions /@ plots
  ; width = Max@sizes[[All, 1]]
  ; sizes = sizes[[All, 2]] + Join[{0}, ConstantArray[OptionValue[Spacings], Length@plots - 1]]
  ; Graphics[
      Table[
        Inset[plots[[i]], {0, -Plus @@ sizes[[;; i]]}, ImageScaled[{0, 0}]]
        , {i, Length[plots]}
      ]
      , ImageSize -> {width, Plus @@ sizes}
      , ImagePadding -> None
      , PlotRange -> {{0, width}, {-Plus @@ sizes, 0}}
      , AspectRatio -> Plus @@ sizes/width
      , PlotRangePadding -> None
      , FilterRules[{opts}, Options[Graphics]]
    ]
 ]

alignedGraphicsGrid

Since this one is not very robust yet, I'll just leave an example along with the code. There are lots of things which are pretty inelegant, so...

Example

This code:

alignedGraphicsGrid[{{p1, p1}, {p1, p1}, {p1, p1}} /. HoldPattern[FrameLabel -> _] :> Sequence[]
  , Spacings -> {-22, -20}, ImageWidth -> 400]

leads to this figure:

enter image description here

Code

Clear[alignedGraphicsGrid]
Options[alignedGraphicsGrid] =  Join[{Spacings -> 0, ImageWidth -> Automatic}, Options[Graphics]];
alignedGraphicsGrid::width = "Value of option ImageWidth is not Automatic or a positive machine number.";

alignedGraphicsGrid[list_, opts : OptionsPattern[]] := Module[
  {size, sizes, width, height, plots, space = OptionValue[Spacings], optWidth = OptionValue[ImageWidth]}
  , plots = Map[Show[#, ImagePadding -> maxPadding @@ Flatten@list] &, list, {2}]
  ; size = ImageDimensions@plots[[1, 1]]
  ; sizes = space {Range[0, Last@#] /. {a___, b_, c_} :> {a, b, b}, Range[0, First@#]} + size {Range[0, Last@#], Range[1 + First@#]} & @Dimensions@plots
  ; Which[
     And[optWidth =!= Automatic, ! NumericQ@optWidth]
     , Return[Message[alignedGraphicsGrid::width]]
     , NumericQ @ optWidth
     , plots = Map[Show[#, ImageSize -> optWidth/sizes[[1, -1]] ImageDimensions[#][[1]]] &, plots, {2}]; sizes = sizes*optWidth/sizes[[1, -1]]
     , optWidth === Automatic
     , plots = list
    ]
  ; height = sizes[[2, -2]]; width = sizes[[1, -1]]
  ; Graphics[
     MapThread[
      Inset[#1, #2, ImageScaled[{0, 0}]] &
      , {plots, Transpose@Outer[List, sizes[[1, ;; -2]], -sizes[[2, ;; -2]]]}
      , 2
     ]
     , ImageSize -> {sizes[[1, -1]], height}
     , ImagePadding -> None
     , PlotRange -> {{0, width}, {-height, 0}}
     , AspectRatio -> height/width
     , PlotRangePadding -> None
     , FilterRules[{opts}, Options[Graphics]]
    ]
 ]
$\endgroup$
  • $\begingroup$ `alignedGraphicsColumn' does a great job of formatting plots for publication. Thanks! When I use it, "FrameStyle" causes the frame labels to be clipped. How do you change the frame color, thickness when you use this? $\endgroup$ – axsvl77 Dec 15 '17 at 18:52
  • $\begingroup$ @axsvl77. I never do. This code is likely not very robust to those kinds of changes, since I didn't plan for them. I actually have some ideas as to how to make it better, but I don't really have the time to work on it right now. $\endgroup$ – march Dec 15 '17 at 18:59

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.