Skip to content
Open
2 changes: 2 additions & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ Class-style Parameters
:toctree: generated
:template: autosummary/params.rst

Axis
Box
Frame
Pattern
Position

Expand Down
1 change: 1 addition & 0 deletions pygmt/params/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"""

from pygmt.params.box import Box
from pygmt.params.frame import Axis, Frame
from pygmt.params.pattern import Pattern
from pygmt.params.position import Position
232 changes: 232 additions & 0 deletions pygmt/params/frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
"""
The Axes, Axis, and Frame classes for specifying the frame.
"""

import dataclasses

from pygmt.alias import Alias
from pygmt.exceptions import GMTParameterError
from pygmt.params.base import BaseParam

__doctest_skip__ = ["Axis", "Frame"]


@dataclasses.dataclass(repr=False)
class Axis(BaseParam):
"""
Class for setting up one axis of a plot.

Examples
--------
To specify the same attributes for all axes, with intervals of 4 for annotations,
2 for ticks, and 1 for gridlines:

>>> import pygmt
>>> fig = pygmt.Figure()
>>> fig.basemap(
... region=[0, 10, 0, 20],
... projection="X10c/10c",
... frame=Axis(annot=4, tick=2, grid=1),
... )
>>> fig.show()
"""

#: Specify the interval for annoations. It can be ``True`` to let GMT decide the
#: interval automatically; or a value to set a specific interval in the format of
#: *stride*\ [±\ *phase*][*unit*], where, *stride* is the interval, *phase* is the
#: offset to shift the annotations by that amount, and *unit* is one of the
#: :gmt-docs:`18 supported unit codes <reference/options.html#tbl-units>` related to
#: time intervals.
annot: float | bool = False

#: Specify the interval for ticks. Same format as ``annot``.
tick: float | bool = False

#: Specify the interval for gridlines. Same format as ``annot``.
grid: float | bool = False

#: Label for the axis [Default is no label].
label: str | None = None

#: A leading text prefix for the axis annotations (e.g., dollar sign for plots
#: related to money) [For Cartesian plots only].
prefix: str | None = None

#: Unit to append to the axis annotations [For Cartesian plots only].
unit: str | None = None

#: Angle of the axis annotations.
angle: float | None = None

@property
def _aliases(self):
return [
Alias(self.annot, name="annot", prefix="a"),
Alias(self.tick, name="tick", prefix="f"),
Alias(self.grid, name="grid", prefix="g"),
Alias(self.label, name="label", prefix="+l"),
Alias(self.angle, name="angle", prefix="+a"),
Alias(self.prefix, name="prefix", prefix="+p"),
Alias(self.unit, name="unit", prefix="+u"),
]


@dataclasses.dataclass(repr=False)
class _Axes(BaseParam):
"""
A private class to build the Axes part of the Frame class.
"""

axes: str | None = None
title: str | None = None

@property
def _aliases(self):
return [
Alias(self.axes, name="axes"),
Alias(self.title, name="title", prefix="+t"),
]


@dataclasses.dataclass(repr=False)
class Frame(BaseParam):
"""
Class for setting up the frame and axes of a plot.

Examples
--------
To specify the west and south axes with both tick marks and annotations, draw the
east and north axes with tick marks but without annotations:

>>> import pygmt
>>> fig = pygmt.Figure()
>>> fig.basemap(
... region=[0, 10, 0, 20], projection="X10c/10c", frame=Frame(axes="WSen")
... )
>>> fig.show()

To specify the same attributes for all axes, with intervals of 4 for annotations,
2 for ticks, and 1 for gridlines:

>>> fig = pygmt.Figure()
>>> fig.basemap(
... region=[0, 10, 0, 20],
... projection="X10c/10c",
... frame=Frame(axes="WSrt", axis=Axis(annot=4, tick=2, grid=1)),
... )
>>> fig.show()

To specify the attributes for each axis separately:

>>> fig = pygmt.Figure()
>>> fig.basemap(
... region=[0, 10, 0, 20],
... projection="X10c/10c",
... frame=Frame(
... axes="WSrt",
... xaxis=Axis(annot=4, tick=2, grid=1, label="X Label"),
... yaxis=Axis(annot=5, tick=2.5, grid=1, label="Y Label"),
... ),
... )
>>> fig.show()
"""

#: Controls which axes are drawn and whether they are annotated, using a combination
#: of the codes below. Axis ommitted from the set will not be drawn.
#:
#: For a 2-D plot, there are four axes: west, east, south, and north (or left,
#: right, bottom, top if you prefer); For a 3-D plot, there is an extra Z-axis.
#: They can be denoted by the following codes:
#:
#: - **W** (west), **E** (east), **S** (south), **N** (north), **Z**: Draw axes with
#: both tick marks and annotations.
#: - **w** (west), **e** (east), **s** (south), **n** (north), **z**: Draw axes with
#: tick marks but without annotations.
#: - **l** (left), **r** (right), **b** (bottom), **t** (top), **u** (up): Draw axes
#: without tick marks or annotations.
#:
#: For examples:
#:
#: - ``"WS"``: Draw the west and south axes with both tick marks and annotations,
#: but do not draw the east and north axes.
#: - ``"WSen"``: Draw the west and south axes with both tick marks and annotations,
#: draw the east and north axes with tick marks but without annotations.
#: - ``"WSrt"``: Draw the west and south axes with both tick marks and annotations,
#: draw the east and north axes without tick marks or annotations.
#: - ``"WSrtZ"``: Draw the west and south axes with both tick marks and annotations,
#: draw the east and north axes without tick marks or annotations, and draw the
#: z-axis with both tick marks and annotations.
#:
#: For a 3-D plot, if the z-axis code is specified, a single vertical axis will be
#: drawn at the most suitable corner by default. Append any combination of the
#: corner IDs from 1 to 4 to draw one or more vertical axes at the corresponding
#: corners (e.g., ``"WSrtZ1234"``):
#:
#: - **1**: the south-western (lower-left) corner
#: - **2**: the south-eastern (lower-right) corner
#: - **3**: the north-eastern (upper-right) corner
#: - **4**: the north-western (upper-left) corner
axes: str | None = None

#: The title string centered above the plot frame [Default is no title].
title: str | None = None

#: Specify the attributes for axes by an :class:`Axis` object.
#:
#: The attributes for each axis can be specified in two ways: (1) specifying the
#: same attributes for all axes using the ``axis`` parameter; (2) specifying the
#: attributes for each axis separately using the ``xaxis``, ``yaxis``, ``zaxis``
#: parameter for the x-, y, and z-axes, respectively.
#:
#: GMT uses the notion of primary (the default) and secondary axes, while secondary
#: axes are optional and mostly used for time axes annotations. To specify the
#: attributes for the secondary axes, use the ``xaxis2``, ``yaxis2``, and ``zaxis2``
#: parameters.
axis: Axis | None = None
xaxis: Axis | None = None
yaxis: Axis | None = None
zaxis: Axis | None = None
xaxis2: Axis | None = None
yaxis2: Axis | None = None
zaxis2: Axis | None = None

def _validate(self):
"""
Validate the parameters of the Frame class.
"""
if self.axis is not None and any(
[self.xaxis, self.yaxis, self.zaxis, self.xaxis2, self.yaxis2, self.zaxis2]
):
raise GMTParameterError(
conflicts_with=(
"axis",
["xaxis", "yaxis", "zaxis", "xaxis2", "yaxis2", "zaxis2"],
),
reason="Either 'axis' or the individual axis parameters can be set, but not both.",
)

@property
def _aliases(self):
# _Axes() maps to an empty string, which becomes '-B' without arguments and is
# invalid when combined with individual axis settings (e.g., '-B -Bxaf -Byaf').
frame_settings = _Axes(axes=self.axes, title=self.title)
return [
Alias(frame_settings) if str(frame_settings) else Alias(None),
Alias(self.axis, name="axis"),
Alias(self.xaxis, name="xaxis", prefix="px" if self.xaxis2 else "x"),
Alias(self.yaxis, name="yaxis", prefix="py" if self.yaxis2 else "y"),
Alias(self.zaxis, name="zaxis", prefix="pz" if self.zaxis2 else "z"),
Alias(self.xaxis2, name="xaxis2", prefix="sx"),
Alias(self.yaxis2, name="yaxis2", prefix="sy"),
Alias(self.zaxis2, name="zaxis2", prefix="sz"),
]

def __iter__(self):
"""
Iterate over the aliases of the class.

Yields
------
The value of each alias in the class. None are excluded.
"""
yield from (alias._value for alias in self._aliases if alias._value is not None)
95 changes: 95 additions & 0 deletions pygmt/tests/test_params_frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""
Test the Frame and Axis classes.
"""

from pygmt.params import Axis, Frame


def test_params_axis():
"""
Test the Axis class.
"""
assert str(Axis(annot=True)) == "a"
assert str(Axis(annot=True, tick=True, grid=True)) == "afg"
assert str(Axis(annot=30, tick=15, grid=5)) == "a30f15g5"
assert str(Axis(annot=30, label="LABEL")) == "a30+lLABEL"
assert str(Axis(annot=30, prefix="$", unit="m")) == "a30+p$+um"


def test_params_frame_only():
"""
Test the Frame class.
"""
assert str(Frame("WSen")) == "WSen"
assert str(Frame(axes="WSEN", title="My Title")) == "WSEN+tMy Title"


def test_params_frame_axis():
"""
Test the Frame class with uniform axis setting.
"""
frame = Frame(axes="lrtb", title="My Title", axis=Axis(annot=30, tick=15, grid=10))
assert list(frame) == ["lrtb+tMy Title", "a30f15g10"]

frame = Frame(
axes="WSEN",
title="My Title",
axis=Axis(annot=True, tick=True, grid=True, label="LABEL"),
)
assert list(frame) == ["WSEN+tMy Title", "afg+lLABEL"]


def test_params_frame_separate_axes():
"""
Test the Frame class with separate axis settings.
"""
frame = Frame(
xaxis=Axis(annot=10, tick=5, grid=2.5),
yaxis=Axis(annot=20, tick=10, grid=5),
)
assert list(frame) == ["xa10f5g2.5", "ya20f10g5"]

frame = Frame(
axes="lrtb",
title="My Title",
xaxis=Axis(annot=10, tick=5, grid=2),
yaxis=Axis(annot=20, tick=10, grid=4),
)
assert list(frame) == ["lrtb+tMy Title", "xa10f5g2", "ya20f10g4"]

frame = Frame(
axes="WSEN",
title="My Title",
xaxis=Axis(annot=True, tick=True, grid=True, label="X-LABEL"),
yaxis=Axis(annot=True, tick=True, grid=True, label="Y-LABEL"),
)
assert list(frame) == ["WSEN+tMy Title", "xafg+lX-LABEL", "yafg+lY-LABEL"]


def test_params_frame_separate_axis_secondary():
"""
Test the Frame class with separate axis settings including secondary axes.
"""
frame = Frame(
axes="lrtb",
title="My Title",
xaxis=Axis(annot=10, tick=5, grid=2),
xaxis2=Axis(annot=15, tick=7, grid=3),
yaxis=Axis(annot=20, tick=10, grid=4),
yaxis2=Axis(annot=25, tick=12, grid=5),
)
assert list(frame) == [
"lrtb+tMy Title",
"pxa10f5g2",
"pya20f10g4",
"sxa15f7g3",
"sya25f12g5",
]

frame = Frame(
axes="WSEN",
title="My Title",
xaxis=Axis(annot=True, tick=True, grid=True, label="X-LABEL"),
yaxis=Axis(annot=True, tick=True, grid=True, label="Y-LABEL"),
)
assert list(frame) == ["WSEN+tMy Title", "xafg+lX-LABEL", "yafg+lY-LABEL"]
Loading