Skip to content

Commit 938e8ec

Browse files
seismanyvonnefroehlichmichaelgrund
authored
Figure.logo: Add parameters position/width/height to specify logo position and dimensions (#4014)
Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> Co-authored-by: Michael Grund <23025878+michaelgrund@users.noreply.github.com>
1 parent 8d57fdd commit 938e8ec

File tree

6 files changed

+228
-28
lines changed

6 files changed

+228
-28
lines changed

examples/gallery/embellishments/gmt_logo.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77

88
# %%
99
import pygmt
10+
from pygmt.params import Position
1011

1112
fig = pygmt.Figure()
1213
fig.basemap(region=[0, 10, 0, 2], projection="X6c", frame=True)
1314

1415
# Add the GMT logo in the Top Right (TR) corner of the current plot, scaled up to be 3
1516
# centimeters wide and offset by 0.3 cm in x-direction and 0.6 cm in y-direction.
16-
fig.logo(position="jTR+o0.3c/0.6c+w3c")
17-
17+
fig.logo(position=Position("TR", offset=(0.3, 0.6)), width="3c")
1818
fig.show()

pygmt/src/_common.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from pathlib import Path
88
from typing import Any, ClassVar, Literal
99

10-
from pygmt.exceptions import GMTValueError
10+
from pygmt.exceptions import GMTInvalidInput, GMTValueError
11+
from pygmt.params.position import Position
1112
from pygmt.src.which import which
1213

1314

@@ -244,3 +245,113 @@ def from_params(
244245
if set(param_list).issubset(set(params)):
245246
return cls(convention, component=component) # type: ignore[arg-type]
246247
raise GMTValueError(params, description="focal mechanism parameters")
248+
249+
250+
def _parse_position(
251+
position: Position | Sequence[float | str] | str | None,
252+
kwdict: dict[str, Any],
253+
default: Position | None,
254+
) -> Position | str:
255+
"""
256+
Parse the "position" parameter for embellishment-plotting functions.
257+
258+
Parameters
259+
----------
260+
position
261+
The position argument to parse. It can be one of the following:
262+
263+
- A :class:`pygmt.params.Position` object.
264+
- A sequence of two values representing x and y coordinates in plot coordinates.
265+
- A 2-character justification code.
266+
- A raw GMT command string (for backward compatibility).
267+
- ``None``, in which case the default position is used.
268+
kwdict
269+
The keyword arguments dictionary that conflicts with ``position`` if
270+
``position`` is given as a raw GMT command string.
271+
default
272+
The default Position object to use if ``position`` is ``None``.
273+
274+
Returns
275+
-------
276+
position
277+
The parsed Position object or raw GMT command string.
278+
279+
Examples
280+
--------
281+
>>> from pygmt.params import Position
282+
>>> _parse_position(
283+
... Position((3, 3), cstype="mapcoords"),
284+
... kwdict={"width": None, "height": None},
285+
... default=Position((0, 0), cstype="plotcoords"),
286+
... )
287+
Position(refpoint=(3, 3), cstype='mapcoords')
288+
289+
>>> _parse_position(
290+
... (3, 3),
291+
... kwdict={"width": None, "height": None},
292+
... default=Position((0, 0), cstype="plotcoords"),
293+
... )
294+
Position(refpoint=(3, 3), cstype='plotcoords')
295+
>>> _parse_position(
296+
... "TL",
297+
... kwdict={"width": None, "height": None},
298+
... default=Position((0, 0), cstype="plotcoords"),
299+
... )
300+
Position(refpoint='TL', cstype='inside')
301+
302+
>>> _parse_position(
303+
... None,
304+
... kwdict={"width": None, "height": None},
305+
... default=Position((0, 0), cstype="plotcoords"),
306+
... )
307+
Position(refpoint=(0, 0), cstype='plotcoords')
308+
309+
>>> _parse_position(
310+
... "x3c/4c+w2c",
311+
... kwdict={"width": None, "height": None},
312+
... default=Position((0, 0), cstype="plotcoords"),
313+
... )
314+
'x3c/4c+w2c'
315+
316+
>>> _parse_position(
317+
... "x3c/4c+w2c",
318+
... kwdict={"width": 2, "height": None},
319+
... default=Position((0, 0), cstype="plotcoords"),
320+
... )
321+
Traceback (most recent call last):
322+
...
323+
pygmt.exceptions.GMTInvalidInput: Parameter 'position' is given with a raw GMT...
324+
325+
>>> _parse_position(
326+
... 123,
327+
... kwdict={"width": None, "height": None},
328+
... default=Position((0, 0), cstype="plotcoords"),
329+
... )
330+
Traceback (most recent call last):
331+
...
332+
pygmt.exceptions.GMTInvalidInput: Invalid type for parameter 'position':...
333+
"""
334+
335+
_valid_anchors = {f"{h}{v}" for v in "TMB" for h in "LCR"} | {
336+
f"{v}{h}" for v in "TMB" for h in "LCR"
337+
}
338+
match position:
339+
case str() if position in _valid_anchors: # Anchor code
340+
position = Position(position, cstype="inside")
341+
case str(): # Raw GMT command string.
342+
if any(v is not None for v in kwdict.values()):
343+
msg = (
344+
"Parameter 'position' is given with a raw GMT command string, and "
345+
f"conflicts with parameters {', '.join(repr(c) for c in kwdict)}."
346+
)
347+
raise GMTInvalidInput(msg)
348+
case Sequence() if len(position) == 2: # A sequence of x and y coordinates.
349+
position = Position(position, cstype="plotcoords")
350+
case Position(): # Already a Position object.
351+
pass
352+
case None if default is not None: # Set default position.
353+
position = default
354+
case _:
355+
msg = f"Invalid type for parameter 'position': {type(position)}."
356+
raise GMTInvalidInput(msg)
357+
return position

pygmt/src/inset.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def inset(
109109
Examples
110110
--------
111111
>>> import pygmt
112-
>>> from pygmt.params import Box
112+
>>> from pygmt.params import Box, Position
113113
>>>
114114
>>> # Create the larger figure
115115
>>> fig = pygmt.Figure()
@@ -126,9 +126,8 @@ def inset(
126126
... dcw="MG+gred",
127127
... )
128128
...
129-
>>> # Map elements outside the "with" statement are plotted in the main
130-
>>> # figure
131-
>>> fig.logo(position="jBR+o0.2c+w3c")
129+
>>> # Map elements outside the "with" statement are plotted in the main figure
130+
>>> fig.logo(position=Position("BR", offset=0.2), width="3c")
132131
>>> fig.show()
133132
"""
134133
self._activate_figure()

pygmt/src/logo.py

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,51 @@
55
from collections.abc import Sequence
66
from typing import Literal
77

8+
from pygmt._typing import AnchorCode
89
from pygmt.alias import Alias, AliasSystem
910
from pygmt.clib import Session
10-
from pygmt.helpers import build_arg_list, fmt_docstring, use_alias
11-
from pygmt.params import Box
11+
from pygmt.exceptions import GMTInvalidInput
12+
from pygmt.helpers import build_arg_list, fmt_docstring
13+
from pygmt.params import Box, Position
14+
from pygmt.src._common import _parse_position
1215

1316

1417
@fmt_docstring
15-
@use_alias(D="position")
16-
def logo(
18+
def logo( # noqa: PLR0913
1719
self,
20+
position: Position | Sequence[float | str] | AnchorCode | None = None,
21+
width: float | str | None = None,
22+
height: float | str | None = None,
23+
box: Box | bool = False,
24+
style: Literal["standard", "url", "no_label"] = "standard",
1825
projection: str | None = None,
1926
region: Sequence[float | str] | str | None = None,
20-
style: Literal["standard", "url", "no_label"] = "standard",
21-
box: Box | bool = False,
2227
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
2328
| bool = False,
2429
panel: int | Sequence[int] | bool = False,
25-
transparency: float | None = None,
2630
perspective: float | Sequence[float] | str | bool = False,
31+
transparency: float | None = None,
2732
**kwargs,
2833
):
29-
r"""
34+
"""
3035
Plot the GMT logo.
3136
32-
By default, the GMT logo is 2 inches wide and 1 inch high and
33-
will be positioned relative to the current plot origin.
34-
Use various options to change this and to place a transparent or
35-
opaque rectangular map panel behind the GMT logo.
37+
.. figure:: https://docs.generic-mapping-tools.org/6.6/_images/GMT_coverlogo.png
38+
:alt: GMT logo
39+
:align: center
40+
:width: 300px
41+
42+
By default, the GMT logo is 2 inches wide and 1 inch high and will be positioned
43+
relative to the current plot origin.
3644
3745
Full GMT docs at :gmt-docs:`gmtlogo.html`.
3846
39-
$aliases
47+
**Aliases:**
48+
49+
.. hlist::
50+
:columns: 3
51+
52+
- D = position, **+w**: width, **+h**: height
4053
- F = box
4154
- J = projection
4255
- R = region
@@ -48,12 +61,22 @@ def logo(
4861
4962
Parameters
5063
----------
51-
$projection
52-
$region
53-
position : str
54-
[**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\
55-
**+w**\ *width*\ [**+j**\ *justify*]\ [**+o**\ *dx*\ [/*dy*]].
56-
Set reference point on the map for the image.
64+
position
65+
Position of the GMT logo on the plot. It can be specified in multiple ways:
66+
67+
- A :class:`pygmt.params.Position` object to fully control the reference point,
68+
anchor point, and offset.
69+
- A sequence of two values representing the x and y coordinates in plot
70+
coordinates, e.g., ``(1, 2)`` or ``("1c", "2c")``.
71+
- A :doc:`2-character justification code </techref/justification_codes>` for a
72+
position inside the plot, e.g., ``"TL"`` for Top Left corner inside the plot.
73+
74+
If not specified, defaults to the lower-left corner of the plot (position
75+
``(0, 0)`` with anchor ``"BL"``).
76+
width
77+
height
78+
Width or height of the GMT logo. Since the aspect ratio is fixed, only one of
79+
the two can be specified. [Default is 2 inches wide and 1 inch high].
5780
box
5881
Draw a background box behind the logo. If set to ``True``, a simple rectangular
5982
box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box appearance,
@@ -65,14 +88,32 @@ def logo(
6588
- ``"standard"``: The text label "The Generic Mapping Tools".
6689
- ``"no_label"``: Skip the text label.
6790
- ``"url"``: The URL to the GMT website.
91+
$projection
92+
$region
6893
$verbose
6994
$panel
70-
$transparency
7195
$perspective
96+
$transparency
7297
"""
7398
self._activate_figure()
7499

100+
position = _parse_position(
101+
position,
102+
kwdict={"width": width, "height": height},
103+
default=Position((0, 0), cstype="plotcoords"), # Default to (0,0) in plotcoords
104+
)
105+
106+
# width and height are mutually exclusive.
107+
if width is not None and height is not None:
108+
msg = "Cannot specify both 'width' and 'height'."
109+
raise GMTInvalidInput(msg)
110+
75111
aliasdict = AliasSystem(
112+
D=[
113+
Alias(position, name="position"),
114+
Alias(height, name="height", prefix="+h"),
115+
Alias(width, name="width", prefix="+w"),
116+
],
76117
F=Alias(box, name="box"),
77118
S=Alias(
78119
style, name="style", mapping={"standard": "l", "url": "u", "no_label": "n"}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 6fa301eb7cd0e467285160cf261f9a10
3+
size: 44372
4+
hash: md5
5+
path: test_logo_default_position.png

pygmt/tests/test_logo.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import pytest
66
from pygmt import Figure
7+
from pygmt.exceptions import GMTInvalidInput
8+
from pygmt.params import Position
79

810

911
@pytest.mark.benchmark
@@ -17,12 +19,54 @@ def test_logo():
1719
return fig
1820

1921

22+
@pytest.mark.mpl_image_compare
23+
def test_logo_default_position():
24+
"""
25+
Test that the default position is at the plot origin when no position is specified.
26+
"""
27+
fig = Figure()
28+
fig.basemap(region=[0, 10, 0, 10], projection="X10c", frame=True)
29+
fig.logo()
30+
return fig
31+
32+
2033
@pytest.mark.mpl_image_compare
2134
def test_logo_on_a_map():
2235
"""
2336
Plot the GMT logo at the upper right corner of a map.
2437
"""
2538
fig = Figure()
2639
fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True)
27-
fig.logo(position="jTR+o0.25c/0.25c+w7.5c", box=True)
40+
fig.logo(position=Position("TR", offset=(0.25, 0.25)), width="7.5c", box=True)
2841
return fig
42+
43+
44+
@pytest.mark.mpl_image_compare(filename="test_logo_on_a_map.png")
45+
def test_logo_position_deprecated_syntax():
46+
"""
47+
Test that passing the deprecated GMT CLI syntax string to 'position' works.
48+
"""
49+
fig = Figure()
50+
fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True)
51+
fig.logo(position="jTR+o0.25/0.25+w7.5c", box=True)
52+
return fig
53+
54+
55+
def test_logo_width_and_height():
56+
"""
57+
Test that an error is raised when both width and height are specified.
58+
"""
59+
fig = Figure()
60+
with pytest.raises(GMTInvalidInput):
61+
fig.logo(width="5c", height="5c")
62+
63+
64+
def test_logo_position_mixed_syntax():
65+
"""
66+
Test that an error is raised when mixing new and deprecated syntax in 'position'.
67+
"""
68+
fig = Figure()
69+
with pytest.raises(GMTInvalidInput):
70+
fig.logo(position="jTL", width="5c")
71+
with pytest.raises(GMTInvalidInput):
72+
fig.logo(position="jTL", height="6c")

0 commit comments

Comments
 (0)