Skip to content

Commit fb718b2

Browse files
seismanmichaelgrundyvonnefroehlich
authored
Figure.shift_origin: Support shifting origins temporarily when used as a context manager (#2509)
Co-authored-by: Michael Grund <23025878+michaelgrund@users.noreply.github.com> Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com>
1 parent 3d4baa6 commit fb718b2

File tree

5 files changed

+166
-19
lines changed

5 files changed

+166
-19
lines changed

pygmt/src/shift_origin.py

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,41 @@
22
shift_origin - Shift plot origin in x and/or y directions.
33
"""
44

5+
import contextlib
6+
57
from pygmt.clib import Session
8+
from pygmt.helpers import build_arg_list
69

710

811
def shift_origin(
912
self, xshift: float | str | None = None, yshift: float | str | None = None
1013
):
1114
r"""
12-
Shift plot origin in x and/or y directions.
15+
Shift the plot origin in x and/or y directions.
16+
17+
The shifts can be permanent or temporary. If used as a standalone method, the shifts
18+
are permanent and apply to all subsequent plots. If used as a context manager, the
19+
shifts are temporary and only apply to the block of code within the context manager.
20+
21+
1. Use as a standalone method to shift the plot origin permanently:
22+
23+
.. code-block:: python
24+
25+
fig.shift_origin(...)
26+
... # Other plot commands
27+
28+
2. Use as a context manager to shift the plot origin temporarily:
29+
30+
.. code-block:: python
31+
32+
with fig.shift_origin(...):
33+
... # Other plot commands
34+
...
1335
14-
This method shifts the plot origin relative to the current origin by *xshift* and
15-
*yshift* in x and y directions, respectively. Optionally, append the length unit
36+
The shifts *xshift* and *yshift* in x and y directions are relative to the current
37+
plot origin. The default unit for shifts is centimeters (**c**) but can be changed
38+
to other units via :gmt-term:`PROJ_LENGTH_UNIT`. Optionally, append the length unit
1639
(**c** for centimeters, **i** for inches, or **p** for points) to the shifts.
17-
Default unit if not explicitly given is **c**, but can be changed to other units via
18-
:gmt-term:`PROJ_LENGTH_UNIT`.
1940
2041
For *xshift*, a special character **w** can also be used, which represents the
2142
bounding box **width** of the previous plot. The full syntax is
@@ -44,23 +65,63 @@ def shift_origin(
4465
4566
Examples
4667
--------
68+
69+
Shifting the plot origin permanently:
70+
4771
>>> import pygmt
4872
>>> fig = pygmt.Figure()
49-
>>> fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True)
50-
>>> # Shift the plot origin in x direction by 12 cm
51-
>>> fig.shift_origin(xshift=12)
52-
>>> fig.basemap(region=[0, 10, 0, 10], projection="X14c/10c", frame=True)
53-
>>> # Shift the plot origin in x direction based on the previous plot width
54-
>>> # Here, the width is 14 cm, and xshift is 16 cm
55-
>>> fig.shift_origin(xshift="w+2c")
73+
>>> fig.basemap(region=[0, 5, 0, 5], projection="X5c/5c", frame=True)
74+
>>> # Shift the plot origin in x direction by 6 cm
75+
>>> fig.shift_origin(xshift=6)
76+
<contextlib._GeneratorContextManager object at ...>
77+
>>> fig.basemap(region=[0, 7, 0, 5], projection="X7c/5c", frame=True)
78+
>>> # Shift the plot origin in x direction based on the previous plot width.
79+
>>> # Here, the width is 7 cm, and xshift is 8 cm.
80+
>>> fig.shift_origin(xshift="w+1c")
81+
<contextlib._GeneratorContextManager object at ...>
82+
>>> fig.basemap(region=[0, 10, 0, 5], projection="X10c/5c", frame=True)
83+
>>> fig.show()
84+
85+
Shifting the plot origin temporarily:
86+
87+
>>> fig = pygmt.Figure()
88+
>>> fig.basemap(region=[0, 5, 0, 5], projection="X5c/5c", frame=True)
89+
>>> # Shift the plot origin in x direction by 6 cm temporarily. The plot origin will
90+
>>> # revert back to the original plot origin after the block of code is executed.
91+
>>> with fig.shift_origin(xshift=6):
92+
... fig.basemap(region=[0, 5, 0, 5], projection="X5c/5c", frame=True)
93+
>>> # Shift the plot origin in y direction by 6 cm temporarily.
94+
>>> with fig.shift_origin(yshift=6):
95+
... fig.basemap(region=[0, 5, 0, 5], projection="X5c/5c", frame=True)
96+
>>> # Shift the plot origin in x and y directions by 6 cm temporarily.
97+
>>> with fig.shift_origin(xshift=6, yshift=6):
98+
... fig.basemap(region=[0, 5, 0, 5], projection="X5c/5c", frame=True)
5699
>>> fig.show()
57100
"""
58101
self._preprocess()
59-
args = ["-T"]
60-
if xshift:
61-
args.append(f"-X{xshift}")
62-
if yshift:
63-
args.append(f"-Y{yshift}")
102+
kwdict = {"T": True, "X": xshift, "Y": yshift}
64103

65104
with Session() as lib:
66-
lib.call_module(module="plot", args=args)
105+
lib.call_module(module="plot", args=build_arg_list(kwdict))
106+
_xshift = lib.get_common("X") # False or xshift in inches
107+
_yshift = lib.get_common("Y") # False or yshift in inches
108+
109+
@contextlib.contextmanager
110+
def _shift_origin_context():
111+
"""
112+
An internal context manager to shift the plot origin temporarily.
113+
"""
114+
try:
115+
yield
116+
finally:
117+
# Revert the plot origin to the original plot origin by shifting it by
118+
# -xshift and -yshift in inches.
119+
kwdict = {
120+
"T": True,
121+
"X": f"{-1.0 * _xshift}i" if _xshift else None,
122+
"Y": f"{-1.0 * _yshift}i" if _yshift else None,
123+
}
124+
with Session() as lib:
125+
lib.call_module(module="plot", args=build_arg_list(kwdict))
126+
127+
return _shift_origin_context()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 6c63b9935618f607fe22608f7561be22
3+
size: 4869
4+
hash: md5
5+
path: test_shift_origin_context_manager.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 3a31df374874ae00920ada0b311b4266
3+
size: 5678
4+
hash: md5
5+
path: test_shift_origin_mixed_modes.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: cd83745f2657ff7daeaba368143db72f
3+
size: 4876
4+
hash: md5
5+
path: test_shift_origin_nested_context_manager.png

pygmt/tests/test_shift_origin.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,16 @@
33
"""
44

55
import pytest
6+
from pygmt import Figure
67
from pygmt.exceptions import GMTInvalidInput
7-
from pygmt.figure import Figure
8+
9+
10+
def _numbered_basemap(fig, number, size=3):
11+
"""
12+
A utility function to create a basemap with a number in the center.
13+
"""
14+
fig.basemap(region=[0, 1, 0, 1], projection=f"X{size}c", frame=0)
15+
fig.text(position="MC", text=number, font="24p")
816

917

1018
@pytest.mark.mpl_image_compare
@@ -27,6 +35,69 @@ def test_shift_origin():
2735
return fig
2836

2937

38+
@pytest.mark.mpl_image_compare
39+
def test_shift_origin_context_manager():
40+
"""
41+
Test if Figure.shift_origin as a context manager shifts origin temporarily.
42+
43+
Expected output is:
44+
| 3 | 4 |
45+
| 1 | 2 |
46+
"""
47+
fig = Figure()
48+
_numbered_basemap(fig, 1, size=2.5)
49+
with fig.shift_origin(xshift=3):
50+
_numbered_basemap(fig, 2, size=2.5)
51+
with fig.shift_origin(yshift=3):
52+
_numbered_basemap(fig, 3, size=2.5)
53+
with fig.shift_origin(xshift=3, yshift=3):
54+
_numbered_basemap(fig, 4, size=2.5)
55+
return fig
56+
57+
58+
@pytest.mark.mpl_image_compare
59+
def test_shift_origin_nested_context_manager():
60+
"""
61+
Test if Figure.shift_origin shifts origin correctly when used in a nested context
62+
manager.
63+
64+
Expected output is:
65+
| 4 | 3 |
66+
| 1 | 2 |
67+
"""
68+
fig = Figure()
69+
_numbered_basemap(fig, 1, size=2.5)
70+
with fig.shift_origin(xshift=3):
71+
_numbered_basemap(fig, 2, size=2.5)
72+
with fig.shift_origin(yshift=3):
73+
_numbered_basemap(fig, 3, size=2.5)
74+
with fig.shift_origin(yshift=3):
75+
_numbered_basemap(fig, 4, size=2.5)
76+
return fig
77+
78+
79+
@pytest.mark.mpl_image_compare
80+
def test_shift_origin_mixed_modes():
81+
"""
82+
Test if Figure.shift_origin works when used as a context manager and as a
83+
method at the same time.
84+
85+
Expected output is:
86+
| | 3 | 4 |
87+
| 1 | 2 | |
88+
"""
89+
fig = Figure()
90+
_numbered_basemap(fig, 1, size=2.5)
91+
with fig.shift_origin(xshift=3):
92+
_numbered_basemap(fig, 2, size=2.5)
93+
fig.shift_origin(xshift=3)
94+
with fig.shift_origin(yshift=3):
95+
_numbered_basemap(fig, 3, size=2.5)
96+
fig.shift_origin(xshift=3, yshift=3)
97+
_numbered_basemap(fig, 4, size=2.5)
98+
return fig
99+
100+
30101
def test_shift_origin_unsupported_xshift_yshift():
31102
"""
32103
Raise an exception if X/Y/xshift/yshift is used.

0 commit comments

Comments
 (0)