Skip to content

Commit 5e1ed53

Browse files
seismanweiji14yvonnefroehlich
authored
Add the Frame/Axis class for setting frame and axes [Part 1] (#4406)
Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com> Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com>
1 parent 2541547 commit 5e1ed53

4 files changed

Lines changed: 332 additions & 0 deletions

File tree

doc/api/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ Class-style Parameters
217217
:toctree: generated
218218
:template: autosummary/params.rst
219219

220+
Axis
220221
Box
222+
Frame
221223
Pattern
222224
Position
223225

pygmt/params/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"""
44

55
from pygmt.params.box import Box
6+
from pygmt.params.frame import Axis, Frame
67
from pygmt.params.pattern import Pattern
78
from pygmt.params.position import Position

pygmt/params/frame.py

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
"""
2+
The Axis and Frame classes for specifying the frame.
3+
"""
4+
5+
import dataclasses
6+
7+
from pygmt.alias import Alias
8+
from pygmt.exceptions import GMTParameterError
9+
from pygmt.params.base import BaseParam
10+
11+
__doctest_skip__ = ["Axis", "Frame"]
12+
13+
14+
@dataclasses.dataclass(repr=False)
15+
class Axis(BaseParam):
16+
"""
17+
Class for setting up one axis of a plot.
18+
19+
Examples
20+
--------
21+
To specify the same attributes for x- and y-axes, with intervals of 4 for
22+
annotations, 2 for ticks, and 1 for gridlines:
23+
24+
>>> import pygmt
25+
>>> fig = pygmt.Figure()
26+
>>> fig.basemap(
27+
... region=[0, 10, 0, 20],
28+
... projection="X10c/10c",
29+
... frame=Axis(annot=4, tick=2, grid=1),
30+
... )
31+
>>> fig.show()
32+
"""
33+
34+
#: Specify the interval for annotations. It can be ``True`` to let GMT decide the
35+
#: interval automatically; or a value to set a specific interval in the format of
36+
#: *stride*\ [±\ *phase*][*unit*], where, *stride* is the interval, *phase* is the
37+
#: offset to shift the annotations by that amount, and *unit* is one of the
38+
#: :gmt-docs:`18 supported unit codes <reference/options.html#tbl-units>` related to
39+
#: time intervals.
40+
annot: float | str | bool = False
41+
42+
#: Specify the interval for ticks. Same format as ``annot``.
43+
tick: float | str | bool = False
44+
45+
#: Specify the interval for gridlines. Same format as ``annot``.
46+
grid: float | str | bool = False
47+
48+
#: Label for the axis [Default is no label].
49+
label: str | None = None
50+
51+
#: A leading text prefix for the axis annotations (e.g., dollar sign for plots
52+
#: related to money) [For Cartesian plots only].
53+
prefix: str | None = None
54+
55+
#: Unit to append to the axis annotations [For Cartesian plots only].
56+
unit: str | None = None
57+
58+
#: Angle of the axis annotations.
59+
angle: float | None = None
60+
61+
@property
62+
def _aliases(self):
63+
return [
64+
Alias(self.annot, name="annot", prefix="a"),
65+
Alias(self.tick, name="tick", prefix="f"),
66+
Alias(self.grid, name="grid", prefix="g"),
67+
Alias(self.label, name="label", prefix="+l"),
68+
Alias(self.angle, name="angle", prefix="+a"),
69+
Alias(self.prefix, name="prefix", prefix="+p"),
70+
Alias(self.unit, name="unit", prefix="+u"),
71+
]
72+
73+
74+
@dataclasses.dataclass(repr=False)
75+
class _Axes(BaseParam):
76+
"""
77+
A private class to build the Axes part of the Frame class.
78+
"""
79+
80+
axes: str | None = None
81+
title: str | None = None
82+
83+
@property
84+
def _aliases(self):
85+
return [
86+
Alias(self.axes, name="axes"),
87+
Alias(self.title, name="title", prefix="+t"),
88+
]
89+
90+
91+
@dataclasses.dataclass(repr=False)
92+
class Frame(BaseParam):
93+
"""
94+
Class for setting up the frame and axes of a plot.
95+
96+
Examples
97+
--------
98+
To draw the west and south axes with both ticks and annotations, and draw the east
99+
and north axes with ticks but without annotations:
100+
101+
>>> import pygmt
102+
>>> fig = pygmt.Figure()
103+
>>> fig.basemap(
104+
... region=[0, 10, 0, 20], projection="X10c/10c", frame=Frame(axes="WSen")
105+
... )
106+
>>> fig.show()
107+
108+
To draw the west and south axes with both ticks and annotations, and draw the east
109+
and north axes without ticks and annotations. For west and south axes, specify
110+
the same attributes for both axes using the ``axis`` parameter, with intervals of 4
111+
for annotations, 2 for ticks, and 1 for gridlines.
112+
113+
>>> fig = pygmt.Figure()
114+
>>> fig.basemap(
115+
... region=[0, 10, 0, 20],
116+
... projection="X10c/10c",
117+
... frame=Frame(axes="WSrt", axis=Axis(annot=4, tick=2, grid=1)),
118+
... )
119+
>>> fig.show()
120+
121+
To specify the attributes for x- and y-axes separately:
122+
123+
>>> fig = pygmt.Figure()
124+
>>> fig.basemap(
125+
... region=[0, 10, 0, 20],
126+
... projection="X10c/10c",
127+
... frame=Frame(
128+
... axes="WSrt",
129+
... xaxis=Axis(annot=4, tick=2, grid=1, label="X Label"),
130+
... yaxis=Axis(annot=5, tick=2.5, grid=1, label="Y Label"),
131+
... ),
132+
... )
133+
>>> fig.show()
134+
"""
135+
136+
#: Controls which axes are drawn and whether they are annotated, using a combination
137+
#: of the codes below. Axis ommitted from the set will not be drawn.
138+
#:
139+
#: For a 2-D plot, there are four axes: west, east, south, and north (or left,
140+
#: right, bottom, top if you prefer); For a 3-D plot, there is an extra Z-axis.
141+
#: They can be denoted by the following codes:
142+
#:
143+
#: - **W** (west), **E** (east), **S** (south), **N** (north), **Z**: Draw axes with
144+
#: both ticks and annotations.
145+
#: - **w** (west), **e** (east), **s** (south), **n** (north), **z**: Draw axes with
146+
#: ticks but without annotations.
147+
#: - **l** (left), **r** (right), **b** (bottom), **t** (top), **u** (up): Draw axes
148+
#: without ticks or annotations.
149+
#:
150+
#: For examples:
151+
#:
152+
#: - ``"WS"``: Draw the west and south axes with both ticks and annotations, but do
153+
#: not draw the east and north axes.
154+
#: - ``"WSen"``: Draw the west and south axes with both ticks and annotations, draw
155+
#: the east and north axes with ticks but without annotations.
156+
#: - ``"WSrt"``: Draw the west and south axes with both ticks and annotations, draw
157+
#: the east and north axes without ticks or annotations.
158+
#: - ``"WSrtZ"``: Draw the west and south axes with both ticks and annotations, draw
159+
#: the east and north axes without ticks or annotations, and draw the z-axis with
160+
#: both ticks and annotations.
161+
#:
162+
#: For a 3-D plot, if the z-axis code is specified, a single vertical axis will be
163+
#: drawn at the most suitable corner by default. Append any combination of the
164+
#: corner IDs from 1 to 4 to draw one or more vertical axes at the corresponding
165+
#: corners (e.g., ``"WSrtZ1234"``):
166+
#:
167+
#: - **1**: the south-western (lower-left) corner
168+
#: - **2**: the south-eastern (lower-right) corner
169+
#: - **3**: the north-eastern (upper-right) corner
170+
#: - **4**: the north-western (upper-left) corner
171+
axes: str | None = None
172+
173+
#: The title string centered above the plot frame [Default is no title].
174+
title: str | None = None
175+
176+
#: Specify the attributes for axes by an :class:`Axis` object.
177+
#:
178+
#: The attributes for each axis can be specified in two ways: (1) specifying the
179+
#: same attributes for all axes using the ``axis`` parameter; (2) specifying the
180+
#: attributes for each axis separately using the ``xaxis``, ``yaxis``, ``zaxis``
181+
#: parameter for the x-, y, and z-axes, respectively.
182+
#:
183+
#: GMT uses the notion of primary (the default) and secondary axes, while secondary
184+
#: axes are optional and mostly used for time axes annotations. To specify the
185+
#: attributes for the secondary axes, use the ``xaxis2``, ``yaxis2``, and ``zaxis2``
186+
#: parameters.
187+
axis: Axis | None = None
188+
xaxis: Axis | None = None
189+
yaxis: Axis | None = None
190+
zaxis: Axis | None = None
191+
xaxis2: Axis | None = None
192+
yaxis2: Axis | None = None
193+
zaxis2: Axis | None = None
194+
195+
def _validate(self):
196+
"""
197+
Validate the parameters of the Frame class.
198+
"""
199+
if self.axis is not None and any(
200+
[self.xaxis, self.yaxis, self.zaxis, self.xaxis2, self.yaxis2, self.zaxis2]
201+
):
202+
raise GMTParameterError(
203+
conflicts_with=(
204+
"axis",
205+
["xaxis", "yaxis", "zaxis", "xaxis2", "yaxis2", "zaxis2"],
206+
),
207+
reason="Either 'axis' or the individual axis parameters can be set, but not both.",
208+
)
209+
210+
@property
211+
def _aliases(self):
212+
# _Axes() maps to an empty string, which becomes '-B' without arguments and is
213+
# invalid when combined with individual axis settings (e.g., '-B -Bxaf -Byaf').
214+
frame_settings = _Axes(axes=self.axes, title=self.title)
215+
return [
216+
Alias(frame_settings) if str(frame_settings) else Alias(None),
217+
Alias(self.axis, name="axis"),
218+
Alias(self.xaxis, name="xaxis", prefix="px" if self.xaxis2 else "x"),
219+
Alias(self.yaxis, name="yaxis", prefix="py" if self.yaxis2 else "y"),
220+
Alias(self.zaxis, name="zaxis", prefix="pz" if self.zaxis2 else "z"),
221+
Alias(self.xaxis2, name="xaxis2", prefix="sx"),
222+
Alias(self.yaxis2, name="yaxis2", prefix="sy"),
223+
Alias(self.zaxis2, name="zaxis2", prefix="sz"),
224+
]
225+
226+
def __iter__(self):
227+
"""
228+
Iterate over the aliases of the class.
229+
230+
Yields
231+
------
232+
The value of each alias in the class. None are excluded.
233+
"""
234+
yield from (alias._value for alias in self._aliases if alias._value is not None)

pygmt/tests/test_params_frame.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
Test the Frame and Axis classes.
3+
"""
4+
5+
from pygmt.params import Axis, Frame
6+
7+
8+
def test_params_axis():
9+
"""
10+
Test the Axis class.
11+
"""
12+
assert str(Axis(annot=True)) == "a"
13+
assert str(Axis(annot=True, tick=True, grid=True)) == "afg"
14+
assert str(Axis(annot=30, tick=15, grid=5)) == "a30f15g5"
15+
assert str(Axis(annot=30, label="LABEL")) == "a30+lLABEL"
16+
assert str(Axis(annot=30, prefix="$", unit="m")) == "a30+p$+um"
17+
18+
19+
def test_params_frame_only():
20+
"""
21+
Test the Frame class.
22+
"""
23+
assert str(Frame("WSen")) == "WSen"
24+
assert str(Frame(axes="WSEN", title="My Title")) == "WSEN+tMy Title"
25+
26+
27+
def test_params_frame_axis():
28+
"""
29+
Test the Frame class with uniform axis setting.
30+
"""
31+
frame = Frame(axes="lrtb", title="My Title", axis=Axis(annot=30, tick=15, grid=10))
32+
assert list(frame) == ["lrtb+tMy Title", "a30f15g10"]
33+
34+
frame = Frame(
35+
axes="WSEN",
36+
title="My Title",
37+
axis=Axis(annot=True, tick=True, grid=True, label="LABEL"),
38+
)
39+
assert list(frame) == ["WSEN+tMy Title", "afg+lLABEL"]
40+
41+
42+
def test_params_frame_separate_axes():
43+
"""
44+
Test the Frame class with separate axis settings.
45+
"""
46+
frame = Frame(
47+
xaxis=Axis(annot=10, tick=5, grid=2.5),
48+
yaxis=Axis(annot=20, tick=10, grid=5),
49+
)
50+
assert list(frame) == ["xa10f5g2.5", "ya20f10g5"]
51+
52+
frame = Frame(
53+
axes="lrtb",
54+
title="My Title",
55+
xaxis=Axis(annot=10, tick=5, grid=2),
56+
yaxis=Axis(annot=20, tick=10, grid=4),
57+
)
58+
assert list(frame) == ["lrtb+tMy Title", "xa10f5g2", "ya20f10g4"]
59+
60+
frame = Frame(
61+
axes="WSEN",
62+
title="My Title",
63+
xaxis=Axis(annot=True, tick=True, grid=True, label="X-LABEL"),
64+
yaxis=Axis(annot=True, tick=True, grid=True, label="Y-LABEL"),
65+
)
66+
assert list(frame) == ["WSEN+tMy Title", "xafg+lX-LABEL", "yafg+lY-LABEL"]
67+
68+
69+
def test_params_frame_separate_axis_secondary():
70+
"""
71+
Test the Frame class with separate axis settings including secondary axes.
72+
"""
73+
frame = Frame(
74+
axes="lrtb",
75+
title="My Title",
76+
xaxis=Axis(annot=10, tick=5, grid=2),
77+
xaxis2=Axis(annot=15, tick=7, grid=3),
78+
yaxis=Axis(annot=20, tick=10, grid=4),
79+
yaxis2=Axis(annot=25, tick=12, grid=5),
80+
)
81+
assert list(frame) == [
82+
"lrtb+tMy Title",
83+
"pxa10f5g2",
84+
"pya20f10g4",
85+
"sxa15f7g3",
86+
"sya25f12g5",
87+
]
88+
89+
frame = Frame(
90+
axes="WSEN",
91+
title="My Title",
92+
xaxis=Axis(annot=True, tick=True, grid=True, label="X-LABEL"),
93+
yaxis=Axis(annot=True, tick=True, grid=True, label="Y-LABEL"),
94+
)
95+
assert list(frame) == ["WSEN+tMy Title", "xafg+lX-LABEL", "yafg+lY-LABEL"]

0 commit comments

Comments
 (0)