plotting - Remove the extra white-space padding introduced by implicit use of Inset in GraphicsColumn
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 Frame
d below on the right:
(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 amazingcontourDensityPlot
. (This allowsExport
ing to pdf that eliminates the lines of the density plot mesh.)Set the correct
FontSize
andImageSize
so that the exported figure will match the font size and column width used in the journal, based on suggestions found inSzabolcs
'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
fromSzabolcs
's answer here to align the figures in aGraphicsColumn
. UseEpilog
in theGraphicsColumn
to add the annotations $(a)$ and $(b)$. (This is important: I cannot useColumn
because this disallowsEpilog
.)Manually go into the automated
Inset
s thatGraphicsColumn
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 theFrame
) 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 FrameLabel
s, the actual aspect ratios of the figures are necessarily different. Therefore, we need to add equal ImagePadding
s 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)
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
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 Export
ed Row
of the Column
solution and the hacked GraphicsColumn
solution side-by-side. They are almost indistinguishable.
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:
Second, the non-hacked GraphicsColumn
solution with second figure selected:
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:
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
This is the final mystery, I guess. Thank you for slogging through this long tale.
Answer
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 ofundistortedGraphicsColumn
, 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 theColumn
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 outerGraphics
but not to the sub-plots that areInset
in the outerGraphics
. This may or may not be useful.alignedGraphicsGrid
is not very robust yet and really only works for plots whose plot regions have the sameAspectRatio
and whose image widths are all the same. In addition, sometimes the plots will be cut off, and I need to manually add someImagePadding
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 onSciDraw
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 ofImageSize -> x
. Full
scales all of the sub-plots so that they all have the width of the sub-plot with the largest width, and theImageSize
is then this width.Automatic
is the default: theImageSize
is the width of the sub-plot with the largest width.
- Choosing a positive real number
FrameAligned
determines the alignment functionality.All
: all plots get the sameImagePadding
, and so the plots get lined up nicely, with lots of space between them, which can be fixed withSpacings
.LeftOnly
: all plots get the sameImagePadding
on the left and the right, which aligns the left sides of the frames only, unless all sub-plots have the sameImageSize
.None
: applies noImagePadding
, so the plots do not line up.
Spacings
: works as advertised inGraphicsGrid
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 of20
between the sub-figures, and we have added aLightBlue
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.
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:
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]]
]
]
Comments
Post a Comment