Skip to content

Commit 3403501

Browse files
Merge branch 'main' into add-gallery-grdmask
2 parents 98f32db + 8c497c1 commit 3403501

9 files changed

Lines changed: 209 additions & 23 deletions

File tree

doc/api/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ Operations on tabular data
127127
blockmedian
128128
blockmode
129129
filter1d
130-
grdmask
131130
nearneighbor
132131
project
133132
select
@@ -157,6 +156,7 @@ Operations on raster data
157156
grdhisteq.equalize_grid
158157
grdhisteq.compute_bins
159158
grdlandmask
159+
grdmask
160160
grdpaste
161161
grdproject
162162
grdsample

examples/gallery/basemaps/ternary.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
# %%
1616
import pygmt
17-
from pygmt.params import Position
17+
from pygmt.params import Axis, Frame, Position
1818

1919
fig = pygmt.Figure()
2020

@@ -34,11 +34,13 @@
3434
blabel="Water",
3535
clabel="Air",
3636
cmap=True,
37-
frame=[
38-
"aafg+lLimestone component+u %",
39-
"bafg+lWater component+u %",
40-
"cafg+lAir component+u %",
41-
],
37+
frame=Frame(
38+
xaxis=Axis(
39+
annot=True, tick=True, grid=True, label="Limestone component", unit="%"
40+
),
41+
yaxis=Axis(annot=True, tick=True, grid=True, label="Water component", unit="%"),
42+
zaxis=Axis(annot=True, tick=True, grid=True, label="Air component", unit="%"),
43+
),
4244
)
4345

4446
# Add a colorbar indicating the values given in the fourth column of the input dataset

pygmt/params/frame.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import dataclasses
6+
from typing import Literal
67

78
from pygmt.alias import Alias
89
from pygmt.exceptions import GMTParameterError
@@ -45,6 +46,11 @@ class Axis(BaseParam):
4546
#: Specify the interval for gridlines. Same format as ``annot``.
4647
grid: float | str | bool = False
4748

49+
#: Skip annotations that fall exactly at the ends of the axis. Set to ``True`` to
50+
#: skip both ends, or use ``"lower"``/``"upper"`` to skip only the lower/upper
51+
#: end.
52+
end_skip: Literal["lower", "upper"] | bool = False
53+
4854
#: Label for the axis [Default is no label].
4955
label: str | None = None
5056

@@ -73,6 +79,12 @@ def _aliases(self):
7379
Alias(self.annot, name="annot", prefix="a"),
7480
Alias(self.tick, name="tick", prefix="f"),
7581
Alias(self.grid, name="grid", prefix="g"),
82+
Alias(
83+
self.end_skip,
84+
name="end_skip",
85+
prefix="+e",
86+
mapping={"lower": "l", "upper": "u"},
87+
),
7688
Alias(self.label, name="label", prefix="+l"),
7789
Alias(self.alt_label, name="alt_label", prefix="+s"),
7890
Alias(self.hlabel, name="hlabel", prefix="+L"),

pygmt/src/grdmask.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,14 @@ def grdmask(
128128
"""
129129
Create mask grid from polygons or point coverage.
130130
131-
Reads one or more files containing polygon or data point coordinates, and creates a
132-
grid where nodes that fall inside, on the edge, or outside the polygons (or within
133-
the search radius from data points) are assigned values based on the ``outside``,
134-
``edge``, and ``inside`` parameters.
131+
Takes one or more polygons or data point coordinates, and creates a grid where nodes
132+
that fall inside, on the edge, or outside the polygons or within the search radius
133+
from data points are assigned values based on the ``outside``, ``edge``, and
134+
``inside`` parameters.
135135
136-
The mask grid can be used to mask out specific regions in other grids using
137-
:func:`pygmt.grdmath` or similar tools. For masking based on coastline features,
138-
consider using :func:`pygmt.grdlandmask` instead.
136+
The mask grid can be used to mask out specific regions from another grid. For
137+
masking based on coastline features, consider using :func:`pygmt.grdlandmask`
138+
instead.
139139
140140
Full GMT docs at :gmt-docs:`grdmask.html`.
141141
@@ -156,7 +156,7 @@ def grdmask(
156156
Pass in either a file name to an ASCII data table, a 2-D $table_classes
157157
containing the polygon(s) or data points. Input can be:
158158
159-
- **Polygon mode**: One or more files containing closed polygon coordinates
159+
- **Polygon mode**: One or more polygons with closed coordinates
160160
- **Point coverage mode**: Data points (used with ``search_radius`` parameter)
161161
$outgrid
162162
$spacing
@@ -165,10 +165,13 @@ def grdmask(
165165
inside
166166
Set the value assigned to nodes outside, on the edge, or inside the polygons.
167167
Can be any number, or one of ``None``, ``"NaN"``, and ``np.nan`` for NaN.
168+
Defaults are ``0`` for ``outside``, ``0`` for ``edge``, and ``1`` for
169+
``inside``. When setting these values, keep in mind you are creating a mask grid
170+
which is thought to be applied to a real grid in a second step.
168171
169-
``inside`` and ``edge`` can also be set to one of the following values:
172+
``edge`` and ``inside`` can also be set to one of the following values:
170173
171-
- ``"z"``: Use the z-value from polygon data (segment header ``-Zzval``,
174+
- ``"z"``: Use the z-values from polygon data (segment header ``-Zzval``,
172175
``-Lheader``, or via ``-aZ=name``).
173176
- ``"id"``: Use a running polygon ID number.
174177

pygmt/src/ternary.py

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,103 @@
88
from pygmt._typing import PathLike, TableLike
99
from pygmt.alias import Alias, AliasSystem
1010
from pygmt.clib import Session
11+
from pygmt.exceptions import GMTValueError
1112
from pygmt.helpers import build_arg_list, fmt_docstring, use_alias
13+
from pygmt.params import Axis, Frame
14+
from pygmt.params.frame import _Axes
15+
16+
17+
def _ternary_frame(frame):
18+
"""
19+
Convert 'frame' to ternary-compatible format.
20+
21+
For ternary diagrams, GMT uses axis names **a**, **b**, **c** instead of **x**,
22+
**y**, **z**, and there are no primary/secondary axes. This function converts a
23+
:class:`pygmt.params.Frame` or :class:`pygmt.params.Axis` object to a string or
24+
a list of strings with the correct axis prefixes.
25+
26+
Parameters
27+
----------
28+
frame : Frame, Axis, str, list, or bool
29+
The frame parameter to convert.
30+
31+
Returns
32+
-------
33+
str, bool, or list of str
34+
The converted frame parameter. For Frame inputs, returns a list of strings;
35+
for Axis, str, bool, or list inputs, returns the value directly.
36+
37+
Examples
38+
--------
39+
>>> from pygmt.params import Axis, Frame
40+
>>> _ternary_frame(Axis(annot=True, tick=True, grid=True))
41+
['afg', '']
42+
>>> _ternary_frame(
43+
... Frame(title="Title", axis=Axis(annot=True, tick=True, grid=True))
44+
... )
45+
['+tTitle', 'afg']
46+
>>> _ternary_frame(
47+
... Frame(
48+
... title="Title",
49+
... xaxis=Axis(annot=True, tick=True, grid=True, label="Water"),
50+
... yaxis=Axis(annot=True, tick=True, grid=True, label="Air"),
51+
... zaxis=Axis(annot=True, tick=True, grid=True, label="Limestone"),
52+
... )
53+
... )
54+
['+tTitle', 'aafg+lWater', 'bafg+lAir', 'cafg+lLimestone']
55+
>>> _ternary_frame(Frame(fill="lightblue", axis=Axis(annot=True)))
56+
['+glightblue', 'a']
57+
>>> _ternary_frame("afg")
58+
['afg', '']
59+
>>> _ternary_frame(True)
60+
True
61+
>>> _ternary_frame(["aafg+lWater", "bafg+lAir", "cafg+lLimestone"])
62+
['aafg+lWater', 'bafg+lAir', 'cafg+lLimestone']
63+
>>> _ternary_frame("none")
64+
'none'
65+
>>> _ternary_frame(Frame(axes="WSen", axis=Axis(annot=True)))
66+
Traceback (most recent call last):
67+
pygmt.exceptions.GMTValueError: ...
68+
>>> _ternary_frame(Frame(xaxis2=Axis(annot=True)))
69+
Traceback (most recent call last):
70+
pygmt.exceptions.GMTValueError: ...
71+
"""
72+
if isinstance(frame, Axis):
73+
axis_str = str(frame)
74+
if axis_str:
75+
return [axis_str, ""]
76+
return axis_str
77+
if isinstance(frame, Frame):
78+
_attributes = ["title", "subtitle", "fill", "axis", "xaxis", "yaxis", "zaxis"]
79+
if any(
80+
_attr not in _attributes and getattr(frame, _attr) for _attr in vars(frame)
81+
):
82+
raise GMTValueError(
83+
repr(frame),
84+
description="frame setting",
85+
reason="For ternary diagrams, only Frame attributes "
86+
f"{', '.join(repr(_attr) for _attr in _attributes)} are supported.",
87+
)
88+
frame_settings = _Axes(
89+
title=frame.title, subtitle=frame.subtitle, fill=frame.fill
90+
)
91+
params = [
92+
Alias(frame_settings) if str(frame_settings) else Alias(None),
93+
Alias(frame.axis),
94+
Alias(frame.xaxis, prefix="a"),
95+
Alias(frame.yaxis, prefix="b"),
96+
Alias(frame.zaxis, prefix="c"),
97+
]
98+
result = [par._value for par in params if par._value is not None]
99+
# When only general axis settings are used without frame-level settings
100+
# (title/fill) or axis-specific settings (xaxis/yaxis/zaxis), GMT needs
101+
# a bare -B to draw the frame border. E.g., -Bafg alone doesn't draw it.
102+
if not str(frame_settings) and not any((frame.xaxis, frame.yaxis, frame.zaxis)):
103+
result.append("")
104+
return result
105+
if isinstance(frame, str) and frame not in {"", "none", "+n"}:
106+
return [frame, ""]
107+
return frame
12108

13109

14110
@fmt_docstring
@@ -20,7 +116,7 @@ def ternary( # noqa: PLR0913
20116
blabel: str | None = None,
21117
clabel: str | None = None,
22118
region: Sequence[float | str] | str | None = None,
23-
frame: str | Sequence[str] | Literal["none"] | bool = False,
119+
frame: str | Sequence[str] | Literal["none"] | bool | Frame | Axis = False,
24120
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
25121
| bool = False,
26122
panel: int | Sequence[int] | bool = False,
@@ -65,6 +161,9 @@ def ternary( # noqa: PLR0913
65161
[*amin*, *amax*, *bmin*, *bmax*, *cmin*, *cmax*].
66162
Give the min and max limits for each of the three axes **a**, **b**,
67163
and **c**.
164+
$frame
165+
For ternary diagrams, use :class:`pygmt.params.Frame` ``xaxis``, ``yaxis``, and
166+
``zaxis`` attributes to set the **a**, **b**, and **c** axes, respectively.
68167
$cmap
69168
$fill
70169
alabel
@@ -85,15 +184,14 @@ def ternary( # noqa: PLR0913
85184
$transparency
86185
"""
87186
self._activate_figure()
88-
89187
# -Lalabel/blabel/clabel. '-' means skipping the label.
90188
_labels = [v if v is not None else "-" for v in (alabel, blabel, clabel)]
91189
labels = _labels if any(v != "-" for v in _labels) else None
92190

93191
aliasdict = AliasSystem(
94192
L=Alias(labels, name="alabel/blabel/clabel", sep="/", size=3),
95193
).add_common(
96-
B=frame,
194+
B=_ternary_frame(frame),
97195
R=region,
98196
V=verbose,
99197
c=panel,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 97854ed4620b20f7fdf8fc685b6f7d3a
3+
size: 57608
4+
hash: md5
5+
path: test_ternary_axis.png
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 0d4303390ab992ab68cdf7fc045cb9d9
3+
size: 10357
4+
hash: md5
5+
path: test_ternary_no_frame.png

pygmt/tests/test_params_frame.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ def test_params_axis():
1414
assert str(Axis(annot=True)) == "a"
1515
assert str(Axis(annot=True, tick=True, grid=True)) == "afg"
1616
assert str(Axis(annot=30, tick=15, grid=5)) == "a30f15g5"
17+
assert str(Axis(annot=30, end_skip=True)) == "a30+e"
18+
assert str(Axis(annot=30, end_skip="lower")) == "a30+el"
19+
assert str(Axis(annot=30, end_skip="upper")) == "a30+eu"
1720
assert str(Axis(annot=30, prefix="$", unit="m")) == "a30+p$+um"
1821
assert str(Axis(annot=30, label="LABEL")) == "a30+lLABEL"
1922
assert str(Axis(annot=30, alt_label="LABEL2")) == "a30+sLABEL2"

pygmt/tests/test_ternary.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import numpy as np
66
import pytest
77
from pygmt import Figure
8+
from pygmt.params import Axis, Frame
89

910

1011
@pytest.fixture(scope="module", name="array")
@@ -58,7 +59,11 @@ def test_ternary(array):
5859
region=[0, 100, 0, 100, 0, 100],
5960
cmap="red,orange,yellow,green,blue,violet",
6061
width="10c",
61-
frame=["bafg+lAir", "cafg+lLimestone", "aafg+lWater"],
62+
frame=Frame(
63+
xaxis=Axis(annot=True, tick=True, grid=True, label="Water"),
64+
yaxis=Axis(annot=True, tick=True, grid=True, label="Air"),
65+
zaxis=Axis(annot=True, tick=True, grid=True, label="Limestone"),
66+
),
6267
style="c0.1c",
6368
pen="thinnest",
6469
)
@@ -80,7 +85,11 @@ def test_ternary_3_labels(array):
8085
alabel="A",
8186
blabel="B",
8287
clabel="C",
83-
frame=["bafg+lAir", "cafg+lLimestone", "aafg+lWater"],
88+
frame=Frame(
89+
xaxis=Axis(annot=True, tick=True, grid=True, label="Water"),
90+
yaxis=Axis(annot=True, tick=True, grid=True, label="Air"),
91+
zaxis=Axis(annot=True, tick=True, grid=True, label="Limestone"),
92+
),
8493
style="c0.1c",
8594
pen="thinnest",
8695
)
@@ -99,7 +108,56 @@ def test_ternary_1_label(array):
99108
cmap="red,orange,yellow,green,blue,violet",
100109
width="10c",
101110
alabel="A",
102-
frame=["bafg+lAir", "cafg+lLimestone", "aafg+lWater"],
111+
frame=Frame(
112+
xaxis=Axis(annot=True, tick=True, grid=True, label="Water"),
113+
yaxis=Axis(annot=True, tick=True, grid=True, label="Air"),
114+
zaxis=Axis(annot=True, tick=True, grid=True, label="Limestone"),
115+
),
116+
style="c0.1c",
117+
pen="thinnest",
118+
)
119+
return fig
120+
121+
122+
@pytest.mark.parametrize(
123+
"frame",
124+
[
125+
Axis(annot=True, tick=True, grid=True),
126+
"afg",
127+
Frame(axis=Axis(annot=True, tick=True, grid=True)),
128+
],
129+
ids=["axis", "string", "frame-axis"],
130+
)
131+
@pytest.mark.mpl_image_compare(filename="test_ternary_axis.png")
132+
def test_ternary_axis(array, frame):
133+
"""
134+
Test plotting a ternary chart with equivalent frame settings.
135+
"""
136+
fig = Figure()
137+
fig.ternary(
138+
data=array,
139+
region=[0, 100, 0, 100, 0, 100],
140+
cmap="red,orange,yellow,green,blue,violet",
141+
width="10c",
142+
frame=frame,
143+
style="c0.1c",
144+
pen="thinnest",
145+
)
146+
return fig
147+
148+
149+
@pytest.mark.mpl_image_compare
150+
def test_ternary_no_frame(array):
151+
"""
152+
Test plotting a ternary chart with frame set to "none".
153+
"""
154+
fig = Figure()
155+
fig.ternary(
156+
data=array,
157+
region=[0, 100, 0, 100, 0, 100],
158+
cmap="red,orange,yellow,green,blue,violet",
159+
width="10c",
160+
frame="none",
103161
style="c0.1c",
104162
pen="thinnest",
105163
)

0 commit comments

Comments
 (0)