Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
eec0fb7
Initial implemention of the Position class
seisman Nov 16, 2025
69d4d42
Merge branch 'main' into params/position
seisman Nov 20, 2025
539f66f
Fix styling
seisman Nov 20, 2025
97f015f
Add tests and improve docstrings
seisman Nov 23, 2025
854804e
Add to API doc
seisman Nov 23, 2025
6b55dde
Add an inline doctest
seisman Nov 23, 2025
3d629cb
position is not required
seisman Nov 23, 2025
576b822
Default to plotcoords
seisman Nov 23, 2025
f54bec9
Updates
seisman Nov 23, 2025
5a2e20b
Merge branch 'main' into params/position
seisman Nov 24, 2025
2c59b7f
Improve the checking in Figure.logo
seisman Nov 24, 2025
d0b62ec
Merge branch 'main' into params/position
seisman Nov 24, 2025
fe18c87
Improve docstrings
seisman Nov 24, 2025
038161b
Improve docstrings
seisman Nov 24, 2025
a6e75bc
Improve docstrings
seisman Nov 25, 2025
3ec8c06
Improve docstrings
seisman Nov 25, 2025
339ce00
Improve docstrings
seisman Nov 25, 2025
4d616de
Revert changes in logo.py
seisman Nov 25, 2025
ad9e0aa
Simplify tests
seisman Nov 25, 2025
b084e5f
Validate values
seisman Nov 25, 2025
d4ad6e0
type will be validated in the Alias System
seisman Nov 25, 2025
7dc37bd
Use the image from the GMT docs
seisman Nov 25, 2025
bfecb2d
Fix width and alignment
seisman Nov 25, 2025
18b90b3
Improve docstrings
seisman Nov 25, 2025
6b1b5bc
Remove unneeded blank lines
seisman Nov 25, 2025
1eae742
Improve docstrings
seisman Nov 25, 2025
721b46f
Validate anchor code
seisman Nov 25, 2025
669b16d
Merge branch 'main' into params/position
seisman Nov 26, 2025
2a38111
Merge branch 'main' into params/position
seisman Nov 26, 2025
0f9ed6c
offset can be a single value
seisman Nov 26, 2025
7d1b076
Merge branch 'main' into params/position
seisman Nov 29, 2025
a779431
Merge branch 'main' into params/position
seisman Dec 1, 2025
2a9cc92
Merge branch 'main' into params/position
seisman Dec 4, 2025
10a0dfb
Use is_nonstr_iter to check the location parameter
seisman Dec 4, 2025
6f1c2c4
Merge remote-tracking branch 'origin/params/position' into params/pos…
seisman Dec 4, 2025
c27213f
Fix a typo [skip ci]
seisman Dec 4, 2025
d47aaeb
Fix a typo [skip ci]
seisman Dec 4, 2025
7fc6ffc
Fix the wrong logic in checking location
seisman Dec 4, 2025
d82f4ba
Add a tests for passing a single value to offset
seisman Dec 4, 2025
5d29e66
Merge branch 'main' into params/position
seisman Dec 5, 2025
563b5a1
Merge branch 'main' into params/position
seisman Dec 5, 2025
0ec021b
Merge branch 'main' into params/position
seisman Dec 6, 2025
ff23ac8
Merge branch 'main' into params/position
seisman Dec 6, 2025
620da52
Figure.legend: Refactor using the new alias system
seisman Aug 10, 2025
c9c4222
Rename position to refpoint
seisman Dec 7, 2025
0064cde
Fix formatting
seisman Dec 7, 2025
d702ce6
Improve Figure.legend and tests
seisman Dec 7, 2025
ceb345e
Improve docstrings of width/height
seisman Dec 7, 2025
9922da8
Add one test for width/height
seisman Dec 7, 2025
c917192
Fix checking of box
seisman Dec 7, 2025
cf11c13
Update the position argument in gallery examples
seisman Dec 7, 2025
816d8b9
Rename spacing to line_spacing
seisman Dec 7, 2025
7ff9f6a
Fix one more spacing to line_spacing
seisman Dec 7, 2025
82426cd
Improve docstrings
seisman Dec 7, 2025
cdf6377
Fix typos
seisman Dec 7, 2025
0c276dd
Remove the test_legend_position test because it's already covered in …
seisman Dec 7, 2025
5bc0bb1
Check compatibility with old syntax
seisman Dec 7, 2025
90c7ea2
Merge branch 'main' into params/position
seisman Dec 8, 2025
9a19c1a
Merge branch 'main' into params/position
seisman Dec 8, 2025
ff6392d
Update pygmt/params/position.py
seisman Dec 9, 2025
2310b22
Update pygmt/params/position.py [skip ci]
seisman Dec 9, 2025
a3185e8
Fix styling
seisman Dec 9, 2025
e153ebf
Rename type to cstype
seisman Dec 9, 2025
ed31e0f
Merge branch 'main' into params/position
seisman Dec 9, 2025
be36403
Merge branch 'params/position' into refactor/legend
seisman Dec 9, 2025
5bf51be
Fix typos
seisman Dec 9, 2025
b45fa78
Fix CM to MC
seisman Dec 9, 2025
3480fd1
Merge branch 'main' into params/position
seisman Dec 10, 2025
e0b2071
Merge branch 'params/position' into refactor/legend
seisman Dec 10, 2025
36f2519
Fix typos
seisman Dec 10, 2025
b58c7ff
Merge branch 'main' into refactor/legend
seisman Dec 11, 2025
19e00c4
Update pygmt/tests/test_legend.py [skip ci]
seisman Dec 11, 2025
2f58300
Update pygmt/tests/test_legend.py [skip ci]
seisman Dec 11, 2025
ebe510e
Merge branch 'main' into refactor/legend
seisman Dec 12, 2025
1b4b78d
Merge branch 'main' into refactor/legend
seisman Dec 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions examples/gallery/basemaps/double_y_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class can control which axes should be plotted and optionally show annotations,
# %%
import numpy as np
import pygmt
from pygmt.params import Position

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

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

fig.show()
32 changes: 15 additions & 17 deletions examples/gallery/embellishments/legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
Legend
======

The :meth:`pygmt.Figure.legend` method can automatically create a legend for
symbols plotted using :meth:`pygmt.Figure.plot`. A legend entry is only added
when the ``label`` parameter is used to state the desired text. Optionally,
to adjust the legend, users can append different modifiers. A list of all
available modifiers can be found at :gmt-docs:`gmt.html#l-full`. To create a
multiple-column legend **+N** is used with the desired number of columns.
For more complicated legends, users may want to write an ASCII file with
instructions for the layout of the legend items and pass it to the ``spec``
parameter of :meth:`pygmt.Figure.legend`. For details on how to set up such a
The :meth:`pygmt.Figure.legend` method can automatically create a legend for symbols
plotted using :meth:`pygmt.Figure.plot`. A legend entry is only added when the ``label``
parameter is used to state the desired text. Optionally, to adjust the legend, users can
append different modifiers. A list of all available modifiers can be found at
:gmt-docs:`gmt.html#l-full`. To create a multiple-column legend **+N** is used with the
desired number of columns. For more complicated legends, users may want to write an
ASCII file with instructions for the layout of the legend items and pass it to the
``spec`` parameter of :meth:`pygmt.Figure.legend`. For details on how to set up such a
file, please see the GMT documentation at :gmt-docs:`legend.html#legend-codes`.
"""

# %%
import numpy as np
import pygmt
from pygmt.params import Position

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

# Use the label parameter to state the text label for the legend entry
fig.plot(x=x, y=y1, pen="1p,green3", label="sin(x)+1.1")

fig.plot(x=x, y=y2, style="c0.07c", fill="dodgerblue", label="cos(x)+1.1")

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JTR+jTR+0.2c is equivalent to jTR+0.2 (j means inside, J means outside), so I have change it to Position("TR", offset=0.2).

# Add a legend to the plot at the Top Right (TR) corner with a 0.2-cm offset in x- and
# y-directions; surround the legend with a box.
fig.legend(position=Position("TR", offset=0.2), box=True)

# -----------------------------------------------------------------------------
# Bottom: Horizontal legend (here two columns)
Expand All @@ -55,8 +53,8 @@

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

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

fig.show()
4 changes: 2 additions & 2 deletions examples/gallery/lines/hlines_vlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# In Cartesian coordinate systems lines are plotted as straight lines.

import pygmt
from pygmt.params import Box
from pygmt.params import Box, Position

fig = pygmt.Figure()

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

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

Expand Down
36 changes: 18 additions & 18 deletions examples/tutorials/advanced/legends.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import io

import pygmt
from pygmt.params import Box
from pygmt.params import Box, Position

# %%
# Create an auto-legend
Expand Down Expand Up @@ -47,9 +47,8 @@
# Adjust the position
# -------------------
#
# Use the ``position`` parameter to adjust the position of the legend. Add an offset via
# **+o** for the x- and y-directions. Additionally append **+w** to adjust the width
# of the legend. Note, no box is drawn by default if ``position`` is used.
# Use the ``position`` parameter to adjust the position of the legend. Note, no box is
# drawn by default if ``position`` is used.

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

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

fig.show()


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

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

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

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

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

fig.show()

Expand Down Expand Up @@ -146,15 +144,17 @@

# %%
# Now, we can add a legend based on this :class:`io.StringIO` object. For multi-columns
# legends, the width (**+w**) has to be specified via a the ``position`` parameter.

# legends, the width must be specified.
fig = pygmt.Figure()
# Note, that we are now using a Mercator projection
fig.basemap(region=[-5, 5, -5, 5], projection="M10c", frame=True)

# Pass the io.StringIO object to the "spec" parameter
fig.legend(spec=spec_io, position="jMC+w9c", box=Box(pen="1p,gray50", fill="gray95"))

fig.legend(
spec=spec_io,
position=Position("MC"),
width="9c",
box=Box(pen="1p,gray50", fill="gray95"),
)
fig.show()

# sphinx_gallery_thumbnail_number = 4
5 changes: 3 additions & 2 deletions examples/tutorials/basics/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io

import pygmt
from pygmt.params import Position

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

# %%
Expand Down Expand Up @@ -101,7 +102,7 @@
pen="black",
)
fig.colorbar(frame="xaf+lDepth (km)")
fig.legend(spec=legend, position="jBR+o0.2c+l2", box=True)
fig.legend(spec=legend, position=Position("BR", offset=0.2), line_spacing=2.0, box=True)
fig.show()

# sphinx_gallery_thumbnail_number = 3
95 changes: 65 additions & 30 deletions pygmt/src/legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,21 @@
from pygmt._typing import PathLike
from pygmt.alias import Alias, AliasSystem
from pygmt.clib import Session
from pygmt.exceptions import GMTTypeError
from pygmt.helpers import (
build_arg_list,
data_kind,
fmt_docstring,
is_nonstr_iter,
use_alias,
)
from pygmt.params import Box
from pygmt.exceptions import GMTInvalidInput, GMTTypeError
from pygmt.helpers import build_arg_list, data_kind, fmt_docstring, is_nonstr_iter
from pygmt.params import Box, Position


@fmt_docstring
@use_alias(D="position")
def legend( # noqa: PLR0913
self,
spec: PathLike | io.StringIO | None = None,
scale: float | None = None,
position="JTR+jTR+o0.2c",
position: Position | None = None,
width: float | str | None = None,
height: float | str | None = None,
line_spacing: float | None = None,
box: Box | bool = False,
scale: float | None = None,
projection: str | None = None,
region: Sequence[float | str] | str | None = None,
frame: str | Sequence[str] | bool = False,
Expand All @@ -38,18 +34,23 @@ def legend( # noqa: PLR0913
perspective: float | Sequence[float] | str | bool = False,
**kwargs,
):
r"""
"""
Plot a legend.

Makes legends that can be overlaid on maps. Reads specific
legend-related information from an input file, or automatically creates
legend entries from plotted symbols that have labels. Unless otherwise
noted, annotations will be made using the primary annotation font and
size in effect (i.e., :gmt-term:`FONT_ANNOT_PRIMARY`).
Makes legends that can be overlaid on plots. It reads specific legend-related
information from an input file, a :class:`io.StringIO` object, or automatically
creates legend entries from plotted symbols that have labels. Unless otherwise
noted, annotations will be made using the primary annotation font and size in effect
(i.e., :gmt-term:`FONT_ANNOT_PRIMARY`).

Full GMT docs at :gmt-docs:`legend.html`.

$aliases
**Aliases:**

.. hlist::
:columns: 3

- D = position, **+w**: width/height, **+l**: line_spacing
- B = frame
- F = box
- J = projection
Expand All @@ -71,13 +72,26 @@ def legend( # noqa: PLR0913
- A :class:`io.StringIO` object containing the legend specification

See :gmt-docs:`legend.html` for the definition of the legend specification.
position : str
[**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\
**+w**\ *width*\ [/*height*]\ [**+j**\ *justify*]\ [**+l**\ *spacing*]\
[**+o**\ *dx*\ [/*dy*]].
Define the reference point on the map for the legend. By default, uses
**JTR**\ **+jTR**\ **+o**\ 0.2c which places the legend at the Top Right corner
inside the map frame, with a 0.2 cm offset.
position
Specify the position of the legend on the plot. If not specified, defaults to
the top right corner inside the plot with a 0.2-cm offset. See
:class:`pygmt.params.Position` for details.
width
height
Specify the width and height of the legend box in plot coordinates (inches, cm,
etc.). If not given, the width and height are computed automatically based on
the contents of the legend specification.

If unit is ``%`` (percentage) then width is computed as that fraction of the
plot width. If height is given as percentage then height is recomputed as that
fraction of the legend width (not plot height).

**Note:** Currently, the automatic height calculation only works when legend
codes **D**, **H**, **L**, **S**, or **V** are used and that the number of
symbol columns (**N**) is 1.
line_spacing
Specify the line-spacing factor between legend entries in units of the current
font size [Default is 1.1].
box
Draw a background box behind the legend. If set to ``True``, a simple
rectangular box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box
Expand All @@ -95,12 +109,27 @@ def legend( # noqa: PLR0913
"""
self._activate_figure()

# Default position and box when not specified.
if kwargs.get("D") is None:
kwargs["D"] = position
if box is False and kwargs.get("F") is None:
# Prior PyGMT v0.17.0, 'position' can accept a raw GMT CLI string. Check for
# conflicts with other parameters.
if isinstance(position, str) and any(
v is not None for v in (width, height, line_spacing)
):
msg = (
"Parameter 'position' is given with a raw GMT command string, and conflicts "
"with parameters 'width', 'height', and 'line_spacing'."
)
raise GMTInvalidInput(msg)

# Set default position if not specified.
if kwargs.get("D", position) is None:
position = Position("TR", anchor="TR", offset=0.2)
if kwargs.get("F", box) is False:
box = Box(pen="1p", fill="white") # Default box

# Set default width to 0 if height is given but width is not.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The syntax is +w<width>/<height>. If height is given and width is not, we should pass +w0/<height>, not +w/<height>.

if height is not None and width is None:
width = 0

kind = data_kind(spec)
if kind not in {"empty", "file", "stringio"}:
raise GMTTypeError(type(spec))
Expand All @@ -110,6 +139,12 @@ def legend( # noqa: PLR0913
)

aliasdict = AliasSystem(
D=[
Alias(position, name="position"),
Alias(width, name="width", prefix="+w"), # +wwidth/height
Alias(height, name="height", prefix="/"),
Alias(line_spacing, name="line_spacing", prefix="+l"),
],
F=Alias(box, name="box"),
S=Alias(scale, name="scale"),
).add_common(
Expand Down
5 changes: 0 additions & 5 deletions pygmt/tests/baseline/test_legend_position.png.dvc

This file was deleted.

5 changes: 5 additions & 0 deletions pygmt/tests/baseline/test_legend_width_height.png.dvc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
outs:
- md5: 88cdea7af99c1edd19ade5b4c9b6c09e
size: 115141
hash: md5
path: test_legend_width_height.png
Loading
Loading