Skip to content

Commit ea0b43d

Browse files
committed
Figure.inset: Add parameters position/width/height to specify inset positiona and dimensions
1 parent 626a734 commit ea0b43d

File tree

3 files changed

+120
-56
lines changed

3 files changed

+120
-56
lines changed

pygmt/src/inset.py

Lines changed: 54 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,53 @@
66
from collections.abc import Sequence
77
from typing import Literal
88

9+
from pygmt._typing import AnchorCode
910
from pygmt.alias import Alias, AliasSystem
1011
from pygmt.clib import Session
12+
from pygmt.exceptions import GMTInvalidInput
1113
from pygmt.helpers import (
1214
build_arg_list,
1315
deprecate_parameter,
1416
fmt_docstring,
1517
kwargs_to_strings,
1618
use_alias,
1719
)
18-
from pygmt.params import Box
20+
from pygmt.params import Box, Position
21+
from pygmt.src._common import _parse_position
1922

2023
__doctest_skip__ = ["inset"]
2124

2225

2326
@fmt_docstring
2427
@deprecate_parameter("margin", "clearance", "v0.18.0", remove_version="v0.20.0")
28+
@use_alias(C="clearance")
29+
@kwargs_to_strings(C="sequence")
2530
@contextlib.contextmanager
26-
@use_alias(D="position", C="clearance")
27-
@kwargs_to_strings(D="sequence", M="sequence")
2831
def inset(
2932
self,
30-
projection: str | None = None,
31-
region: Sequence[float | str] | str | None = None,
33+
position: Position | Sequence[float | str] | AnchorCode | None = None,
34+
width: float | str | None = None,
35+
height: float | str | None = None,
3236
box: Box | bool = False,
3337
no_clip: bool = False,
38+
projection: str | None = None,
39+
region: Sequence[float | str] | str | None = None,
3440
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
3541
| bool = False,
3642
**kwargs,
3743
):
3844
r"""
3945
Manage figure inset setup and completion.
4046
41-
This method sets the position, frame, and margins for a smaller figure
42-
inside of the larger figure. Plotting methods that are called within the
47+
48+
This method carves out a sub-region of the current plot canvas and restrict further
49+
plotting to that section of the canvas. Plotting methods that are called within the
4350
context manager are added to the inset figure.
4451
4552
Full GMT docs at :gmt-docs:`inset.html`.
4653
4754
$aliases
55+
- D = position, **+w**: width/height
4856
- F = box
4957
- J = projection
5058
- N = no_clip
@@ -53,45 +61,19 @@ def inset(
5361
5462
Parameters
5563
----------
56-
position : str or list
57-
*xmin/xmax/ymin/ymax*\ [**+r**][**+u**\ *unit*]] \
58-
| [**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\
59-
**+w**\ *width*\ [/*height*][**+j**\ *justify*]\
60-
[**+o**\ *dx*\ [/*dy*]].
61-
62-
*This is the only required parameter.*
63-
Define the map inset rectangle on the map. Specify the rectangle
64-
in one of three ways:
65-
66-
Append **g**\ *lon*/*lat* for map (user) coordinates,
67-
**j**\ *code* or **J**\ *code* for setting the *refpoint* via a
68-
:doc:`2-character justification code </techref/justification_codes>`
69-
that refers to the (invisible)
70-
projected map bounding box, **n**\ *xn*/*yn* for normalized (0-1)
71-
bounding box coordinates, or **x**\ *x*/*y* for plot
72-
coordinates (inches, centimeters, points, append unit).
73-
All but **x** requires both ``region`` and ``projection`` to be
74-
specified. You can offset the reference point via
75-
**+o**\ *dx*/*dy* in the direction implied by *code* or
76-
**+j**\ *justify*.
77-
78-
Alternatively, give *west/east/south/north* of geographic
79-
rectangle bounded by parallels and meridians; append **+r** if the
80-
coordinates instead are the lower left and upper right corners of
81-
the desired rectangle. (Or, give *xmin/xmax/ymin/ymax* of bounding
82-
rectangle in projected coordinates and optionally
83-
append **+u**\ *unit* [Default coordinate unit is meters (**e**)].
84-
85-
Append **+w**\ *width*\ [/*height*] of bounding rectangle or box
86-
in plot coordinates (inches, centimeters, etc.). By default, the
87-
anchor point on the scale is assumed to be the bottom left corner
88-
(**BL**), but this can be changed by appending **+j** followed by a
89-
:doc:`2-character justification code </techref/justification_codes>`
90-
*justify*.
91-
**Note**: If **j** is used then *justify* defaults to the same
92-
as *refpoint*, if **J** is used then *justify* defaults to the
93-
mirror opposite of *refpoint*. Specify inset box attributes via
94-
the ``box`` parameter [Default is outline only].
64+
position
65+
Position of the inset 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+
width
74+
height
75+
Width and height of the inset. Width must be specified and height is set to be
76+
equal to width if not specified.
9577
box
9678
Draw a background box behind the inset. If set to ``True``, a simple rectangular
9779
box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box appearance,
@@ -109,37 +91,56 @@ def inset(
10991
no_clip
11092
Do **not** clip features extruding outside the inset frame boundaries [Default
11193
is ``False``].
112-
$region
94+
11395
$projection
96+
$region
11497
$verbose
11598
11699
Examples
117100
--------
118101
>>> import pygmt
119102
>>> from pygmt.params import Box, Position
120103
>>>
121-
>>> # Create the larger figure
122104
>>> fig = pygmt.Figure()
123105
>>> fig.coast(region="MG+r2", water="lightblue", shorelines="thin")
124-
>>> # Use a "with" statement to initialize the inset context manager
106+
>>> # Use a "with" statement to initialize the inset context manager.
125107
>>> # Setting the position to Top Left and a width of 3.5 centimeters
126-
>>> with fig.inset(position="jTL+w3.5c+o0.2c", clearance=0, box=Box(pen="green")):
127-
... # Map elements under the "with" statement are plotted in the inset
108+
>>> with fig.inset(
109+
... position=Position("TL", offset=0.2),
110+
... width="3.5c",
111+
... clearance=0,
112+
... box=Box(pen="green"),
113+
... ): # Map elements under the "with" statement are plotted in the inset
128114
... fig.coast(
129115
... region="g",
130-
... projection="G47/-20/3.5c",
116+
... projection="G47/-20/?",
131117
... land="gray",
132118
... water="white",
133119
... dcw="MG+gred",
134120
... )
135-
...
136121
>>> # Map elements outside the "with" statement are plotted in the main figure
137122
>>> fig.logo(position=Position("BR", offset=0.2), width="3c")
138123
>>> fig.show()
139124
"""
140125
self._activate_figure()
141126

127+
position = _parse_position(
128+
position,
129+
kwdict={"width": width, "height": height},
130+
default=Position((0, 0), cstype="plotcoords"), # Default to (0,0) in plotcoords
131+
)
132+
133+
# width is mandatory.
134+
if width is None and not isinstance(position, str):
135+
msg = "Parameter 'width' must be specified."
136+
raise GMTInvalidInput(msg)
137+
142138
aliasdict = AliasSystem(
139+
D=[
140+
Alias(position, name="position"),
141+
Alias(width, name="width", prefix="+w"), # +wwidth/height
142+
Alias(height, name="height", prefix="/"),
143+
],
143144
F=Alias(box, name="box"),
144145
N=Alias(no_clip, name="no_clip"),
145146
).add_common(
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: a99b3af4882f90e34ded939d9b5adcdc
3+
size: 31554
4+
hash: md5
5+
path: test_inset_default_position.png

pygmt/tests/test_inset.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import pytest
66
from pygmt import Figure
7-
from pygmt.params import Box
7+
from pygmt.exceptions import GMTInvalidInput
8+
from pygmt.params import Box, Position
89

910

1011
@pytest.mark.benchmark
@@ -15,7 +16,12 @@ def test_inset_aliases():
1516
"""
1617
fig = Figure()
1718
fig.basemap(region="MG+r2", frame="afg")
18-
with fig.inset(position="jTL+w3.5c+o0.2c", clearance=0, box=Box(pen="green")):
19+
with fig.inset(
20+
position=Position("TL", offset=0.2),
21+
width="3.5c",
22+
clearance=0,
23+
box=Box(pen="green"),
24+
):
1925
fig.basemap(region="g", projection="G47/-20/4c", frame="afg")
2026
return fig
2127

@@ -28,7 +34,59 @@ def test_inset_context_manager():
2834
"""
2935
fig = Figure()
3036
fig.basemap(region=[-74, -69.5, 41, 43], projection="M9c", frame=True)
31-
with fig.inset(position="jBL+w3c+o0.2c", clearance=0, box=Box(pen="black")):
37+
with fig.inset(
38+
position=Position("BL", offset=0.2),
39+
width="3c",
40+
clearance=0,
41+
box=Box(pen="black"),
42+
):
3243
fig.basemap(region=[-80, -65, 35, 50], projection="M3c", frame="afg")
3344
fig.basemap(rose="jTR+w3c") # Pass rose argument with basemap after the inset
3445
return fig
46+
47+
48+
@pytest.mark.mpl_image_compare
49+
def test_inset_default_position():
50+
"""
51+
Test that the inset defaults to the lower-left corner when no position is given.
52+
"""
53+
fig = Figure()
54+
fig.basemap(region="MG+r2", frame="afg")
55+
with fig.inset(width="3.5c", box=True):
56+
fig.basemap(region="g", projection="G47/-20/?", frame="afg")
57+
return fig
58+
59+
60+
@pytest.mark.mpl_image_compare(filename="test_inset_aliases.png")
61+
def test_inset_deprecated_position():
62+
"""
63+
Test that the deprecated raw GMT CLI string for position still works.
64+
"""
65+
fig = Figure()
66+
fig.basemap(region="MG+r2", frame="afg")
67+
with fig.inset(position="jTL+w3.5c+o0.2c", clearance=0, box=Box(pen="green")):
68+
fig.basemap(region="g", projection="G47/-20/4c", frame="afg")
69+
return fig
70+
71+
72+
def test_inset_invalid_inputs():
73+
"""
74+
Test that an error is raised when invalid inputs are provided.
75+
"""
76+
fig = Figure()
77+
fig.basemap(region="MG+r2", frame="afg")
78+
79+
# Width is not given
80+
with pytest.raises(GMTInvalidInput):
81+
with fig.inset(position=Position("TL")):
82+
pass
83+
84+
# Height is given but width is not given
85+
with pytest.raises(GMTInvalidInput):
86+
with fig.inset(position=Position("TL"), height="5c"):
87+
pass
88+
89+
# Old position syntax conflicts with width/height
90+
with pytest.raises(GMTInvalidInput):
91+
with fig.inset(position="jTL+w3.5c", width="3.5c"):
92+
pass

0 commit comments

Comments
 (0)