Skip to content

Commit 1ee48d1

Browse files
authored
Figure.legend: Add parameters position/width/height/line_spacing to specify legend position and properties (#4046)
1 parent 3f7c24d commit 1ee48d1

9 files changed

Lines changed: 183 additions & 97 deletions

File tree

examples/gallery/basemaps/double_y_axes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class can control which axes should be plotted and optionally show annotations,
2020
# %%
2121
import numpy as np
2222
import pygmt
23+
from pygmt.params import Position
2324

2425
# Generate two sample Y-data from one common X-data
2526
x = np.linspace(1.0, 9.0, num=9)
@@ -63,8 +64,7 @@ class can control which axes should be plotted and optionally show annotations,
6364
# Plot points for y2-data
6465
fig.plot(x=x, y=y2, style="s0.28c", fill="red", label="y2")
6566

66-
# Create a legend in the Top Left (TL) corner of the plot with an
67-
# offset of 0.1 centimeters
68-
fig.legend(position="jTL+o0.1c", box=True)
67+
# Create a legend in the Top Left (TL) corner of the plot with a 0.1-cm offset.
68+
fig.legend(position=Position("TL", offset=0.1), box=True)
6969

7070
fig.show()

examples/gallery/embellishments/legend.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@
22
Legend
33
======
44
5-
The :meth:`pygmt.Figure.legend` method can automatically create a legend for
6-
symbols plotted using :meth:`pygmt.Figure.plot`. A legend entry is only added
7-
when the ``label`` parameter is used to state the desired text. Optionally,
8-
to adjust the legend, users can append different modifiers. A list of all
9-
available modifiers can be found at :gmt-docs:`gmt.html#l-full`. To create a
10-
multiple-column legend **+N** is used with the desired number of columns.
11-
For more complicated legends, users may want to write an ASCII file with
12-
instructions for the layout of the legend items and pass it to the ``spec``
13-
parameter of :meth:`pygmt.Figure.legend`. For details on how to set up such a
5+
The :meth:`pygmt.Figure.legend` method can automatically create a legend for symbols
6+
plotted using :meth:`pygmt.Figure.plot`. A legend entry is only added when the ``label``
7+
parameter is used to state the desired text. Optionally, to adjust the legend, users can
8+
append different modifiers. A list of all available modifiers can be found at
9+
:gmt-docs:`gmt.html#l-full`. To create a multiple-column legend **+N** is used with the
10+
desired number of columns. For more complicated legends, users may want to write an
11+
ASCII file with instructions for the layout of the legend items and pass it to the
12+
``spec`` parameter of :meth:`pygmt.Figure.legend`. For details on how to set up such a
1413
file, please see the GMT documentation at :gmt-docs:`legend.html#legend-codes`.
1514
"""
1615

1716
# %%
1817
import numpy as np
1918
import pygmt
19+
from pygmt.params import Position
2020

2121
# Set up some test data
2222
x = np.arange(-10, 10.2, 0.2)
@@ -39,13 +39,11 @@
3939

4040
# Use the label parameter to state the text label for the legend entry
4141
fig.plot(x=x, y=y1, pen="1p,green3", label="sin(x)+1.1")
42-
4342
fig.plot(x=x, y=y2, style="c0.07c", fill="dodgerblue", label="cos(x)+1.1")
4443

45-
# Add a legend to the plot; place it within the plot bounding box with both
46-
# reference ("J") and anchor ("+j") points being the Top Right (TR) corner and an
47-
# offset of 0.2 centimeters in x- and y-directions; surround the legend with a box
48-
fig.legend(position="JTR+jTR+o0.2c", box=True)
44+
# Add a legend to the plot at the Top Right (TR) corner with a 0.2-cm offset in x- and
45+
# y-directions; surround the legend with a box.
46+
fig.legend(position=Position("TR", offset=0.2), box=True)
4947

5048
# -----------------------------------------------------------------------------
5149
# Bottom: Horizontal legend (here two columns)
@@ -55,8 +53,8 @@
5553

5654
fig.plot(x=x, y=y4, style="s0.07c", fill="orange", label="cos(x/2)-1.1")
5755

58-
# For a multi-column legend, users have to provide the width via "+w", here it is
59-
# set to 6 centimeters; reference and anchor points are the Bottom Right (BR) corner
60-
fig.legend(position="JBR+jBR+o0.2c+w6c", box=True)
56+
# For a multi-column legend, users have to provide the width, here it is set to 6 cm;
57+
# the legend is placed at the Bottom Right (BR) corner.
58+
fig.legend(position=Position("BR", offset=0.2), width="6c", box=True)
6159

6260
fig.show()

examples/gallery/lines/hlines_vlines.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# In Cartesian coordinate systems lines are plotted as straight lines.
1313

1414
import pygmt
15-
from pygmt.params import Box
15+
from pygmt.params import Box, Position
1616

1717
fig = pygmt.Figure()
1818

@@ -32,7 +32,7 @@
3232
fig.hlines(
3333
y=[2, 3], xmin=[0, 1], xmax=[7, 7.5], pen="1.5p,dodgerblue3", label="Lines 7 & 8"
3434
)
35-
fig.legend(position="JBR+jBR+o0.2c", box=Box(pen="1p", fill="white"))
35+
fig.legend(position=Position("BR", offset=0.2), box=Box(pen="1p", fill="white"))
3636

3737
fig.shift_origin(xshift="w+2c")
3838

examples/tutorials/advanced/legends.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import io
1111

1212
import pygmt
13-
from pygmt.params import Box
13+
from pygmt.params import Box, Position
1414

1515
# %%
1616
# Create an auto-legend
@@ -47,9 +47,8 @@
4747
# Adjust the position
4848
# -------------------
4949
#
50-
# Use the ``position`` parameter to adjust the position of the legend. Add an offset via
51-
# **+o** for the x- and y-directions. Additionally append **+w** to adjust the width
52-
# of the legend. Note, no box is drawn by default if ``position`` is used.
50+
# Use the ``position`` parameter to adjust the position of the legend. Note, no box is
51+
# drawn by default if ``position`` is used.
5352

5453
fig = pygmt.Figure()
5554
fig.basemap(region=[-5, 5, -5, 5], projection="X5c", frame=True)
@@ -58,20 +57,17 @@
5857
fig.plot(x=1, y=0, style="t0.3c", fill="pink", pen="black", label="pink triangle")
5958
fig.plot(x=[-3, 3], y=[-2, -2], pen="darkred", label="darkred line")
6059

61-
# Set the reference point to the Top Left corner within (lowercase "j") the bounding box
62-
# of the plot and use offsets of 0.3 and 0.2 centimeters in the x- and y-directions,
63-
# respectively.
64-
fig.legend(position="jTL+o0.3c/0.2c")
60+
# Set the reference point to the Top Left corner inside the plot and use offsets of 0.3
61+
# and 0.2 centimeters in the x- and y-directions, respectively.
62+
fig.legend(position=Position("TL", offset=(0.3, 0.2)))
6563

6664
fig.show()
6765

6866

6967
# %%
7068
# Add a box
7169
# ---------
72-
# Use the ``box`` parameter for adjusting the box around the legend. The outline of the
73-
# box can be adjusted by appending **+p**. Append **+g** to fill the legend with a color
74-
# (or pattern) [Default is no fill]. The default of ``position`` is preserved.
70+
# Use the ``box`` parameter for adjusting the box around the legend.
7571

7672
fig = pygmt.Figure()
7773
fig.basemap(region=[-5, 5, -5, 5], projection="X5c", frame="rltb+glightgray")
@@ -80,7 +76,7 @@
8076
fig.plot(x=1, y=0, style="t0.3c", fill="pink", pen="black", label="pink triangle")
8177
fig.plot(x=[-3, 3], y=[-2, -2], pen="darkred", label="darkred line")
8278

83-
fig.legend(position="jTL+o0.3c/0.2c", box=True)
79+
fig.legend(position=Position("TL", offset=(0.3, 0.2)), box=True)
8480

8581
fig.shift_origin(xshift="w+1c")
8682
fig.basemap(region=[-5, 5, -5, 5], projection="X5c", frame="rltb+glightgray")
@@ -91,7 +87,9 @@
9187

9288
# Add a box with a 2-point thick blue, solid outline and a white fill with a
9389
# transparency of 30 percent ("@30").
94-
fig.legend(position="jTL+o0.3c/0.2c", box=Box(pen="2p,blue", fill="white@30"))
90+
fig.legend(
91+
position=Position("TL", offset=(0.3, 0.2)), box=Box(pen="2p,blue", fill="white@30")
92+
)
9593

9694
fig.show()
9795

@@ -146,15 +144,14 @@
146144

147145
# %%
148146
# Now, we can add a legend based on this :class:`io.StringIO` object. For multi-columns
149-
# legends, the width (**+w**) has to be specified via a the ``position`` parameter.
150-
147+
# legends, the width must be specified.
151148
fig = pygmt.Figure()
152149
# Note, that we are now using a Mercator projection
153150
fig.basemap(region=[-5, 5, -5, 5], projection="M10c", frame=True)
154-
155151
# Pass the io.StringIO object to the "spec" parameter
156-
fig.legend(spec=spec_io, position="jMC+w9c", box=Box(pen="1p,gray50", fill="gray95"))
157-
152+
fig.legend(
153+
spec=spec_io, position="MC", width="9c", box=Box(pen="1p,gray50", fill="gray95")
154+
)
158155
fig.show()
159156

160157
# sphinx_gallery_thumbnail_number = 4

examples/tutorials/basics/plot.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import io
1414

1515
import pygmt
16+
from pygmt.params import Position
1617

1718
# %%
1819
# For example, let's load the sample dataset of tsunami generating earthquakes
@@ -71,7 +72,7 @@
7172
legend = io.StringIO(
7273
"\n".join(f"S 0.4 c {0.02 * 2**m:.2f} - 1p 1 Mw {m}" for m in [3, 4, 5])
7374
)
74-
fig.legend(spec=legend, position="jBR+o0.2c+l2", box=True)
75+
fig.legend(spec=legend, position=Position("BR", offset=0.2), line_spacing=2.0, box=True)
7576
fig.show()
7677

7778
# %%
@@ -101,7 +102,7 @@
101102
pen="black",
102103
)
103104
fig.colorbar(frame="xaf+lDepth (km)")
104-
fig.legend(spec=legend, position="jBR+o0.2c+l2", box=True)
105+
fig.legend(spec=legend, position=Position("BR", offset=0.2), line_spacing=2.0, box=True)
105106
fig.show()
106107

107108
# sphinx_gallery_thumbnail_number = 3

pygmt/src/legend.py

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,52 @@
66
from collections.abc import Sequence
77
from typing import Literal
88

9-
from pygmt._typing import PathLike
9+
from pygmt._typing import AnchorCode, PathLike
1010
from pygmt.alias import Alias, AliasSystem
1111
from pygmt.clib import Session
1212
from pygmt.exceptions import GMTTypeError
13-
from pygmt.helpers import (
14-
build_arg_list,
15-
data_kind,
16-
fmt_docstring,
17-
is_nonstr_iter,
18-
use_alias,
19-
)
20-
from pygmt.params import Box
13+
from pygmt.helpers import build_arg_list, data_kind, fmt_docstring, is_nonstr_iter
14+
from pygmt.params import Box, Position
15+
from pygmt.src._common import _parse_position
2116

2217

2318
@fmt_docstring
24-
@use_alias(D="position")
2519
def legend( # noqa: PLR0913
2620
self,
2721
spec: PathLike | io.StringIO | None = None,
28-
scale: float | None = None,
29-
position="JTR+jTR+o0.2c",
22+
position: Position | Sequence[float | str] | AnchorCode | None = None,
23+
width: float | str | None = None,
24+
height: float | str | None = None,
25+
line_spacing: float | None = None,
3026
box: Box | bool = False,
27+
scale: float | None = None,
3128
projection: str | None = None,
3229
region: Sequence[float | str] | str | None = None,
3330
frame: str | Sequence[str] | bool = False,
3431
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
3532
| bool = False,
3633
panel: int | Sequence[int] | bool = False,
37-
transparency: float | None = None,
3834
perspective: float | Sequence[float] | str | bool = False,
35+
transparency: float | None = None,
3936
**kwargs,
4037
):
41-
r"""
38+
"""
4239
Plot a legend.
4340
44-
Makes legends that can be overlaid on maps. Reads specific
45-
legend-related information from an input file, or automatically creates
46-
legend entries from plotted symbols that have labels. Unless otherwise
47-
noted, annotations will be made using the primary annotation font and
48-
size in effect (i.e., :gmt-term:`FONT_ANNOT_PRIMARY`).
41+
Makes legends that can be overlaid on plots. It reads specific legend-related
42+
information from an input file, a :class:`io.StringIO` object, or automatically
43+
creates legend entries from plotted symbols that have labels. Unless otherwise
44+
noted, annotations will be made using the primary annotation font and size in effect
45+
(i.e., :gmt-term:`FONT_ANNOT_PRIMARY`).
4946
5047
Full GMT docs at :gmt-docs:`legend.html`.
5148
52-
$aliases
49+
**Aliases:**
50+
51+
.. hlist::
52+
:columns: 3
53+
54+
- D = position, **+w**: width/height, **+l**: line_spacing
5355
- B = frame
5456
- F = box
5557
- J = projection
@@ -71,13 +73,32 @@ def legend( # noqa: PLR0913
7173
- A :class:`io.StringIO` object containing the legend specification
7274
7375
See :gmt-docs:`legend.html` for the definition of the legend specification.
74-
position : str
75-
[**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\
76-
**+w**\ *width*\ [/*height*]\ [**+j**\ *justify*]\ [**+l**\ *spacing*]\
77-
[**+o**\ *dx*\ [/*dy*]].
78-
Define the reference point on the map for the legend. By default, uses
79-
**JTR**\ **+jTR**\ **+o**\ 0.2c which places the legend at the Top Right corner
80-
inside the map frame, with a 0.2 cm offset.
76+
position
77+
Position of the legend on the plot. It can be specified in multiple ways:
78+
79+
- A :class:`pygmt.params.Position` object to fully control the reference point,
80+
anchor point, and offset.
81+
- A sequence of two values representing the x- and y-coordinates in plot
82+
coordinates, e.g., ``(1, 2)`` or ``("1c", "2c")``.
83+
- A :doc:`2-character justification code </techref/justification_codes>` for a
84+
position inside the plot, e.g., ``"TL"`` for Top Left corner inside the plot.
85+
86+
If not specified, defaults to the Top Right corner inside the plot with a 0.2-cm
87+
offset.
88+
width
89+
height
90+
Width and height of the legend box. If not given, the width and height are
91+
computed automatically based on the contents of the legend specification. If
92+
unit is ``%`` (percentage) then width is computed as that fraction of the plot
93+
width. If height is given as percentage then height is recomputed as that
94+
fraction of the legend width (not plot height).
95+
96+
**Note:** Currently, the automatic height calculation only works when legend
97+
codes **D**, **H**, **L**, **S**, or **V** are used and that the number of
98+
symbol columns (**N**) is 1.
99+
line_spacing
100+
The line-spacing factor between legend entries in units of the current font size
101+
[Default is 1.1].
81102
box
82103
Draw a background box behind the legend. If set to ``True``, a simple
83104
rectangular box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box
@@ -95,11 +116,20 @@ def legend( # noqa: PLR0913
95116
"""
96117
self._activate_figure()
97118

98-
# Default position and box when not specified.
99-
if kwargs.get("D") is None:
100-
kwargs["D"] = position
101-
if box is False and kwargs.get("F") is None:
102-
box = Box(pen="1p", fill="white") # Default box
119+
# Set default box if both position and box are not given.
120+
# The default position will be set later in _parse_position().
121+
if kwargs.get("D", position) is None and kwargs.get("F", box) is False:
122+
box = Box(pen="1p", fill="white")
123+
124+
position = _parse_position(
125+
position,
126+
kwdict={"width": width, "height": height, "line_spacing": line_spacing},
127+
default=Position("TR", offset=0.2), # Default to TR with 0.2-cm offset.
128+
)
129+
130+
# Set width to 0 (auto calculated) if height is given but width is not.
131+
if height is not None and width is None:
132+
width = 0
103133

104134
kind = data_kind(spec)
105135
if kind not in {"empty", "file", "stringio"}:
@@ -110,6 +140,12 @@ def legend( # noqa: PLR0913
110140
)
111141

112142
aliasdict = AliasSystem(
143+
D=[
144+
Alias(position, name="position"),
145+
Alias(width, name="width", prefix="+w"), # +wwidth/height
146+
Alias(height, name="height", prefix="/"),
147+
Alias(line_spacing, name="line_spacing", prefix="+l"),
148+
],
113149
F=Alias(box, name="box"),
114150
S=Alias(scale, name="scale"),
115151
).add_common(

pygmt/tests/baseline/test_legend_position.png.dvc

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 88cdea7af99c1edd19ade5b4c9b6c09e
3+
size: 115141
4+
hash: md5
5+
path: test_legend_width_height.png

0 commit comments

Comments
 (0)