6
$\begingroup$

I want to produce a stand-alone PointLegend for a bunch of plots which I generated. Unfortunately, the markers are slightly off-axis. I have applied all the options discussed here, which did improve things, but not to perfection. Let me demonstrate the issue:

layout[pairs_] := 
 Row[Map[Row[{Pane[#[[1]], BaselinePosition -> Center], 
      Pane[#[[2]], BaselinePosition -> Center]}, 
     Alignment -> {Center, Center}, Frame -> True] &, pairs, 1]]

p = PointLegend[
  "DefaultPlotStyle" /. (Method /. 
     Charting`ResolvePlotTheme[Automatic, ListPlot]), Range[5], 
  LegendMarkers -> {Automatic, Large}, LegendLayout -> layout, 
  LegendFunction -> "Frame", 
  LabelStyle -> {FontFamily -> "Arial", FontSize -> 20}, 
  Alignment -> Center]

enter image description here

The diamond at item 3 is too low and the square at item 2 is too high. Here a screenshot with a horizontal ruler to prove the shift:

enter image description here

It takes a while to really see what is wrong but one immediately realizes that things are not well aligned. I don't think this can be used in a presentation.

Are there any mode options to force a true centering of the markers?

$\endgroup$
1
  • $\begingroup$ I agree, it is a duplicate. I haven't used the right keywords to find that previous question. $\endgroup$
    – Felix
    Commented Feb 14, 2017 at 16:32

2 Answers 2

10
$\begingroup$

Direct answer to the question (rewritten after the comments)

It is well known that the default markers are font glyphs and Mathematica can't position font glyphs precisely. If you need precise positioning, you should use primitive-based LegendMarkers. For this purpose I recommend my package PolygonPlotMarkers` :

Needs["PolygonPlotMarkers`"]

markers1 = {
   Graphics[{FaceForm[ColorData[97][1]], EdgeForm[], PolygonMarker["Disk", 1]}],
   Graphics[{FaceForm[ColorData[97][2]], EdgeForm[], PolygonMarker["Square", 1]}],
   Graphics[{FaceForm[ColorData[97][3]], EdgeForm[], PolygonMarker["Diamond", 1]}],
   Graphics[{FaceForm[ColorData[97][4]], EdgeForm[], PolygonMarker["UpTriangle", 1]}],
   Graphics[{FaceForm[ColorData[97][5]], EdgeForm[], PolygonMarker["DownTriangle", 1]}]};

PointLegend[ColorData[97] /@ Range[5], Range[5], LegendMarkerSize -> 30, 
 LegendMarkers -> markers1, LegendLayout -> "Row", 
 LabelStyle -> {FontFamily -> "Arial", FontSize -> 30}]

legend

Let us add horizontal line and check how it looks at different font sizes:

overlay[legend_] := Module[{w, h}, {w, h} = ImageDimensions@Rasterize[legend, "Image"];
      Graphics[{Inset[legend, {0, 0}, {0, 0}, Automatic], Red, Thick, Opacity[.5], 
        Line[{{-1, 0}, {1, 0}}]}, AspectRatio -> h/w, ImageSize -> w]]

t = Table[PointLegend[ColorData[97] /@ Range[5], Range[5], LegendMarkerSize -> 30, 
    LegendMarkers -> markers1, LegendLayout -> "Row", 
    LabelStyle -> {FontFamily -> "Arial", FontSize -> fs}], {fs, {20, 30, 50, 60}}];

overlay /@ t // Column

legends

Note that in the above all the markers are centered relative to their bounding boxes, while on the plot they are placed at their centers of mass. Another problem is that they are inscribed into identical boxes (determined by LegendMarkerSize) and hence have visibly different areas. Both problems can be solved at once by specifying sufficiently large symmetric PlotRange (I also show axes which intersect at the center of mass {0, 0}):

markers2 = Append[#, {PlotRange -> 1, 
      Axes -> True, AxesOrigin -> {0, 0}, Ticks -> False}] & /@ markers1;

PointLegend[ColorData[97] /@ Range[5], Range[5], LegendMarkerSize -> 50, 
  LegendMarkers -> markers2, LegendLayout -> "Row", 
  LabelStyle -> {FontFamily -> "Arial", FontSize -> 30}] // overlay

legend

If one wish to have markers aligned relative to their bounding boxes and at the same time control their sizes, it is achievable by adding PlotRangePadding option without explicit PlotRange (the larger PlotRangePadding, the smaller will be marker):

markers1sized = 
  MapThread[Append[#1, PlotRangePadding -> #2] &, {markers1, {.1, .2, 0, 0, 0}}];

PointLegend[ColorData[97] /@ Range[5], Range[5], LegendMarkerSize -> 30, 
 LegendMarkers -> markers1sized, LegendLayout -> "Row", 
 LabelStyle -> {FontFamily -> "Arial", FontSize -> 30}]

legend

PointLegend is fairly complicated function which has its own set of undocumented "features" what make it difficult to obtain exactly what you wish in every situation. In practice when only a standalone legend is needed it is often easier to construct the legend manually:

Grid[{Flatten@Transpose[{Append[#, ImageSize -> 30] & /@ markers1, Range[5]}]}, 
 Alignment -> {Center, Center}, Spacings -> {{{0.8, 0.5}}, Automatic}, 
 BaseStyle -> {FontFamily -> "Arial", FontSize -> 30}]

legend

Table[overlay@
   Grid[{Flatten@Transpose[{Append[#, ImageSize -> 30] & /@ markers2, Range[5]}]}, 
    Alignment -> {Center, Center}, Spacings -> {{{0.8, 0.5}}, Automatic}, 
    BaseStyle -> {FontFamily -> "Arial", FontSize -> fs}], {fs, {20, 30, 40}}] // Column

column


Offset size, ImageSize and alignment of the markers

When the size of the markers specified in absolute units via Offset (please read "Description of the package" section in the linked post) the alignment of the markers may depend on LegendMarkerSize option of PointLegend (or ImageSize option of Graphics) when explicit PlotRange and/or ImagePadding for the marker isn't specified. This comes from how FrontEnd crops the plot when ImageSize becomes smaller. Here the red point shows the geometric center of the image, and red rectangle show complete plotting range (PlotRange + PlotRagePadding), axes intersect at the center of mass:

opts = {Axes -> True, AxesOrigin -> {0, 0}, Ticks -> False, Background -> LightBlue,
   (*ImagePadding -> 0, *)
   (*PlotRange -> {{-1, 1}, {-1, 1}},*)
   Epilog -> {Red, AbsolutePointSize[5], Point[ImageScaled[{.5, .5}]], 
     EdgeForm[{Red, Thick, Opacity[1]}], FaceForm[], 
     Rectangle[Scaled[{0, 0}], Scaled[{1, 1}]]}};

Table[Graphics[{FaceForm[ColorData[97][2]], EdgeForm[], 
    PolygonMarker["UpTriangle", Offset[40]]}, opts, ImageSize -> {size, size}, 
   BaselinePosition -> Axis], {size, {75, 60, 55, 50, 40, 30}}] // 
 Row[#, Spacer[5], Alignment -> {Center, Axis}] &

row

As one can see, when ImageSize is large, the center of the image coincides with the center of the plotting range almost exactly. But when ImageSize becomes smaller, relative plotting range is reduced and some ImagePadding is added around it non-symmetrically. Hence the center of the image doesn't coincide with the center of the plotting range anymore. We can fix it by disabling ImagePadding (uncomment ImagePadding -> 0 in opts above):

row

Uncommenting also PlotRange -> {{-1, 1}, {-1, 1}} doesn't affects the result because it is the default PlotRange when the plot doesn't contain "tangible" primitives:

row

But the straighforward (and correct) approach to align markers with Offset sizes at their centers of mass is to specify BaselinePosition -> Axis:

opts = {};
markers3 := {
   Graphics[{FaceForm[ColorData[97][1]], EdgeForm[], 
             PolygonMarker["Diamond", Offset[20]]}, opts], 
   Graphics[{FaceForm[ColorData[97][2]], EdgeForm[], 
             PolygonMarker["UpTriangle", Offset[20]]}, opts], 
   Graphics[{FaceForm[ColorData[97][3]], EdgeForm[], 
             PolygonMarker["DownTriangle", Offset[20]]}, opts]};

opts = {BaselinePosition -> Axis, Axes -> True, Ticks -> False};
l = PointLegend[ColorData[97] /@ Range[3], Range[3], LegendMarkerSize -> 35, 
 LegendMarkers -> markers3, LegendLayout -> "Row", 
 LabelStyle -> {FontFamily -> "Arial", FontSize -> 30}]

legend

Aligning such markers relative to bounding boxes is more difficult and can be achieved manually by adding ImageMargins option (which is defined outside of ImageSize):

markers4 = MapThread[
   Append[#1, ImageMargins -> {{0, 0}, #2}] &, {markers3, {{0, 0}, {0, 7}, {7, 0}}}];
PointLegend[ColorData[97] /@ Range[3], Range[3], LegendMarkerSize -> 35, 
 LegendMarkers -> markers4, LegendLayout -> "Row", 
 LabelStyle -> {FontFamily -> "Arial", FontSize -> 30}]

legend


Internal structure of generated legend

By applying ToBoxes to our legend we can see its internal structure on the box level. It is TemplateBox with first argument being the list of labels. The option DisplayFunction contains a function which will be dynamically applied to the first argument in order to generate a static box structure for on-screen display. We see that this function returns GridBox where the pairs consisted of GraphicsBox (containing InsetBox with our marker) and a placeholder for the corresponding label reside. Each GraphicsBox already contains options including BaselinePosition, rows are set to be aligned to Baseline:

Cases[ToBoxes@l, 
 gb_GridBox :> (gb /. 
    g_GraphicsBox :> 
     "GraphicsBox"[Skeleton[…], Options[g, BaselinePosition]]), -1, 1]
{GridBox[{{
    "GraphicsBox"[<<…>>, {BaselinePosition -> Scaled[0.157143] -> Baseline}], #1,
    "GraphicsBox"[<<…>>, {BaselinePosition -> Scaled[0.157143] -> Baseline}], #2, 
    "GraphicsBox"[<<…>>, {BaselinePosition -> Scaled[0.157143] -> Baseline}], #3}}, 
  GridBoxAlignment -> {"Columns" -> {Center, Left}, "Rows" -> {{Baseline}}}, 
  AutoDelete -> False, GridBoxDividers -> {"Columns" -> {{False}}, "Rows" -> {{False}}}, 
  GridBoxItemSize -> {"Columns" -> {{All}}, "Rows" -> {{All}}}, 
  GridBoxSpacings -> {"Columns" -> {{0.8, 0.5}}}]}

We see that BaselinePosition of GraphicsBox aligns to Baseline not its bottom, but the fraction 1/6 (= 0.157143) of its height (this number isn't constant). Fortunately when BaselinePosition is specified in the marker, the setting from the parent GraphicsBox is ignored.

These observations suggest a method to cure the legend. Just set up rows to be aligned at the Center:

alingRowLegend[legend_] := 
 RawBoxes[ToBoxes@legend /. ("Rows" -> {{Baseline}}) -> ("Rows" -> {{Center}})]
$\endgroup$
8
  • 1
    $\begingroup$ Excellent, that is exactly what I need. Sometimes I wonder why all functionality to make publication quality plots has to come from user-defined packages (SciDraw, your package, MaTeX,...) $\endgroup$
    – Felix
    Commented Feb 14, 2017 at 16:34
  • $\begingroup$ It actually seems to me that the markers are normally aligned wrt. their center of mass. I can reproduce your first example only with Offset[20]. When I change that value to 10, it looks like in your last plot. Can you comment on how Offset (which I think controls size) and position are related? $\endgroup$
    – Felix
    Commented Feb 14, 2017 at 17:17
  • $\begingroup$ Playing with the parameters, it seems to me that one needs to set LegendMarkerSize to the same value as FontSize and Offset to exactly 2/3 of that value to achieve proper alignment. And even then, the down triangle ends a bit under the 5 in a pdf export. $\endgroup$
    – Felix
    Commented Feb 14, 2017 at 17:25
  • $\begingroup$ @Felix Right, Offset controls size (please read "Description of the package" section in the linked post). The reason why it also affects alignment comes from the way how FrontEnd by default crops the markers according to the ImageSize (LegendMarkerSize) option in combination with its default settings for BaselinePosition. You can see similar effect with my last example if you set BaselinePosition -> Center, ImageSize -> {30, 30} in align. May be this last method will be even better for your purposes. $\endgroup$ Commented Feb 14, 2017 at 21:21
  • $\begingroup$ @Felix We can control how FrontEnd crops the markers by specifying explicit PlotRange. For example, with PlotRange -> {{-1, 1}, {-1, 1}} and LegendMarkerSize -> 35 we get markers aligned at their center of mass. $\endgroup$ Commented Feb 14, 2017 at 21:24
9
$\begingroup$

I think this is a font issue. Look at:

Style[Row[{\[FilledCircle],\[FilledSquare],\[FilledDiamond]}], FontFamily->"Arial"]

enter image description here

The centering looks better in Times :

Style[Row[{\[FilledCircle],\[FilledSquare],\[FilledDiamond]}], FontFamily->"Times"]

enter image description here

Perhaps the following layout function will serve:

layout[pairs_] := Row @ Map[
    Row[{
        Show[#[[1]], BaselinePosition->Center, BaseStyle->{FontFamily->"Times"}],
        Pane[#[[2]], BaselinePosition->Center]
    }, Frame->True]&,
    pairs
]

This is what I get:

p = PointLegend[
    "DefaultPlotStyle" /. (Method/.Charting`ResolvePlotTheme[Automatic,ListPlot]),
    Range[5],
    LegendMarkers->{Automatic, Large},
    LegendLayout->layout,
    LegendFunction->"Frame",
    LabelStyle->{FontFamily->"Arial", FontSize->20}
]

enter image description here

$\endgroup$
1
  • 4
    $\begingroup$ (+1) I always thought that FontFamily doesn't affect such symbols as \[FilledCircle] in any way because they are taken from the Mathematica's own font. It is weird that FontFamily changes size and positioning. How can it be explained? $\endgroup$ Commented Feb 14, 2017 at 7:54

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