Skip to main content

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



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

Comments

Popular posts from this blog

plotting - Filling between two spheres in SphericalPlot3D

Manipulate[ SphericalPlot3D[{1, 2 - n}, {θ, 0, Pi}, {ϕ, 0, 1.5 Pi}, Mesh -> None, PlotPoints -> 15, PlotRange -> {-2.2, 2.2}], {n, 0, 1}] I cant' seem to be able to make a filling between two spheres. I've already tried the obvious Filling -> {1 -> {2}} but Mathematica doesn't seem to like that option. Is there any easy way around this or ... Answer There is no built-in filling in SphericalPlot3D . One option is to use ParametricPlot3D to draw the surfaces between the two shells: Manipulate[ Show[SphericalPlot3D[{1, 2 - n}, {θ, 0, Pi}, {ϕ, 0, 1.5 Pi}, PlotPoints -> 15, PlotRange -> {-2.2, 2.2}], ParametricPlot3D[{ r {Sin[t] Cos[1.5 Pi], Sin[t] Sin[1.5 Pi], Cos[t]}, r {Sin[t] Cos[0 Pi], Sin[t] Sin[0 Pi], Cos[t]}}, {r, 1, 2 - n}, {t, 0, Pi}, PlotStyle -> Yellow, Mesh -> {2, 15}]], {n, 0, 1}]

plotting - Plot 4D data with color as 4th dimension

I have a list of 4D data (x position, y position, amplitude, wavelength). I want to plot x, y, and amplitude on a 3D plot and have the color of the points correspond to the wavelength. I have seen many examples using functions to define color but my wavelength cannot be expressed by an analytic function. Is there a simple way to do this? Answer Here a another possible way to visualize 4D data: data = Flatten[Table[{x, y, x^2 + y^2, Sin[x - y]}, {x, -Pi, Pi,Pi/10}, {y,-Pi,Pi, Pi/10}], 1]; You can use the function Point along with VertexColors . Now the points are places using the first three elements and the color is determined by the fourth. In this case I used Hue, but you can use whatever you prefer. Graphics3D[ Point[data[[All, 1 ;; 3]], VertexColors -> Hue /@ data[[All, 4]]], Axes -> True, BoxRatios -> {1, 1, 1/GoldenRatio}]

plotting - Mathematica: 3D plot based on combined 2D graphs

I have several sigmoidal fits to 3 different datasets, with mean fit predictions plus the 95% confidence limits (not symmetrical around the mean) and the actual data. I would now like to show these different 2D plots projected in 3D as in but then using proper perspective. In the link here they give some solutions to combine the plots using isometric perspective, but I would like to use proper 3 point perspective. Any thoughts? Also any way to show the mean points per time point for each series plus or minus the standard error on the mean would be cool too, either using points+vertical bars, or using spheres plus tubes. Below are some test data and the fit function I am using. Note that I am working on a logit(proportion) scale and that the final vertical scale is Log10(percentage). (* some test data *) data = Table[Null, {i, 4}]; data[[1]] = {{1, -5.8}, {2, -5.4}, {3, -0.8}, {4, -0.2}, {5, 4.6}, {1, -6.4}, {2, -5.6}, {3, -0.7}, {4, 0.04}, {5, 1.0}, {1, -6.8}, {2, -4.7}, {3, -1.