Skip to content
Open
93 changes: 92 additions & 1 deletion pygmt/src/ternary.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,95 @@
from pygmt._typing import PathLike, TableLike
from pygmt.alias import Alias, AliasSystem
from pygmt.clib import Session
from pygmt.exceptions import GMTParameterError
from pygmt.helpers import build_arg_list, fmt_docstring, use_alias
from pygmt.params import Axis, Frame


def _ternary_frame(frame):
"""
Convert a Frame/Axis parameter to ternary-compatible format.

For ternary diagrams, GMT uses axis names **a**, **b**, **c** instead of
**x**, **y**, **z**, and there are no primary/secondary axes. This function
converts a :class:`pygmt.params.Frame` or :class:`pygmt.params.Axis` object
to a list of strings with the correct ternary prefixes.

Parameters
----------
frame : Frame, Axis, str, list, or bool
The frame parameter to convert.

Returns
-------
str, list of str, or bool
The converted frame parameter.

Examples
--------
>>> from pygmt.params import Axis, Frame
>>> _ternary_frame(Axis(annot=True, tick=True, grid=True))
'afg'
>>> _ternary_frame(Axis(annot=True, tick=True))
'af'
>>> _ternary_frame(
... Frame(title="Title", axis=Axis(annot=True, tick=True, grid=True))
... )
['+tTitle', 'afg']
>>> _ternary_frame(
... Frame(
... title="Title",
... xaxis=Axis(annot=True, tick=True, grid=True, label="Water"),
... yaxis=Axis(annot=True, tick=True, grid=True, label="Air"),
... zaxis=Axis(annot=True, tick=True, grid=True, label="Limestone"),
... )
... )
['+tTitle', 'aafg+lWater', 'bafg+lAir', 'cafg+lLimestone']
>>> _ternary_frame("afg")
'afg'
>>> _ternary_frame(True)
True
>>> _ternary_frame(["afg", "aafg+lWater"])
['afg', 'aafg+lWater']
>>> _ternary_frame(Frame(axes="WSen", axis=Axis(annot=True)))
Traceback (most recent call last):
pygmt.exceptions.GMTParameterError: ...
>>> _ternary_frame(Frame(xaxis2=Axis(annot=True)))
Traceback (most recent call last):
pygmt.exceptions.GMTParameterError: ...
"""
if isinstance(frame, Axis):
return str(frame)
if isinstance(frame, Frame):
if frame.axes:
raise GMTParameterError(
conflicts_with=("frame", ["frame.axes"]),
reason="For ternary diagrams, Frame.axes (e.g., 'WSen') is not supported.",
)
if any((frame.xaxis2, frame.yaxis2, frame.zaxis2)):
raise GMTParameterError(
conflicts_with=(
"frame",
["frame.xaxis2", "frame.yaxis2", "frame.zaxis2"],
),
reason="For ternary diagrams, secondary axes are not supported.",
)
parts = []
if frame.title:
parts.append(f"+t{frame.title}")
# Uniform axis setting (applies to all three ternary axes)
if frame.axis:
parts.append(str(frame.axis))
# Per-axis: xaxis→a, yaxis→b, zaxis→c
for ternary_prefix, axis_obj in [
("a", frame.xaxis),
("b", frame.yaxis),
("c", frame.zaxis),
]:
if axis_obj:
parts.append(f"{ternary_prefix}{axis_obj}")
return parts
return frame


@fmt_docstring
Expand All @@ -20,7 +108,7 @@ def ternary( # noqa: PLR0913
blabel: str | None = None,
clabel: str | None = None,
region: Sequence[float | str] | str | None = None,
frame: str | Sequence[str] | Literal["none"] | bool = False,
frame: str | Sequence[str] | Literal["none"] | bool | Frame | Axis = False,
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
| bool = False,
panel: int | Sequence[int] | bool = False,
Expand Down Expand Up @@ -90,6 +178,9 @@ def ternary( # noqa: PLR0913
_labels = [v if v is not None else "-" for v in (alabel, blabel, clabel)]
labels = _labels if any(v != "-" for v in _labels) else None

# Convert Frame/Axis to ternary-compatible format.
frame = _ternary_frame(frame)

aliasdict = AliasSystem(
L=Alias(labels, name="alabel/blabel/clabel", sep="/", size=3),
).add_common(
Expand Down
66 changes: 63 additions & 3 deletions pygmt/tests/test_ternary.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import numpy as np
import pytest
from pygmt import Figure
from pygmt.exceptions import GMTParameterError
from pygmt.params import Axis, Frame


@pytest.fixture(scope="module", name="array")
Expand Down Expand Up @@ -58,7 +60,11 @@ def test_ternary(array):
region=[0, 100, 0, 100, 0, 100],
cmap="red,orange,yellow,green,blue,violet",
width="10c",
frame=["bafg+lAir", "cafg+lLimestone", "aafg+lWater"],
frame=Frame(
xaxis=Axis(annot=True, tick=True, grid=True, label="Water"),
yaxis=Axis(annot=True, tick=True, grid=True, label="Air"),
zaxis=Axis(annot=True, tick=True, grid=True, label="Limestone"),
),
style="c0.1c",
pen="thinnest",
)
Expand All @@ -80,7 +86,11 @@ def test_ternary_3_labels(array):
alabel="A",
blabel="B",
clabel="C",
frame=["bafg+lAir", "cafg+lLimestone", "aafg+lWater"],
frame=Frame(
xaxis=Axis(annot=True, tick=True, grid=True, label="Water"),
yaxis=Axis(annot=True, tick=True, grid=True, label="Air"),
zaxis=Axis(annot=True, tick=True, grid=True, label="Limestone"),
),
style="c0.1c",
pen="thinnest",
)
Expand All @@ -99,8 +109,58 @@ def test_ternary_1_label(array):
cmap="red,orange,yellow,green,blue,violet",
width="10c",
alabel="A",
frame=["bafg+lAir", "cafg+lLimestone", "aafg+lWater"],
frame=Frame(
xaxis=Axis(annot=True, tick=True, grid=True, label="Water"),
yaxis=Axis(annot=True, tick=True, grid=True, label="Air"),
zaxis=Axis(annot=True, tick=True, grid=True, label="Limestone"),
),
style="c0.1c",
pen="thinnest",
)
return fig


def test_ternary_axis(array):
"""
Test plotting a ternary chart with Axis object for frame.
"""
fig = Figure()
fig.ternary(
data=array,
region=[0, 100, 0, 100, 0, 100],
cmap="red,orange,yellow,green,blue,violet",
width="10c",
frame=Axis(annot=True, tick=True, grid=True),
style="c0.1c",
pen="thinnest",
)


def test_ternary_frame_axes_not_supported(array):
"""
Test that Frame.axes is rejected for ternary diagrams.
"""
fig = Figure()
with pytest.raises(GMTParameterError, match=r"Frame\.axes"):
fig.ternary(
data=array,
region=[0, 100, 0, 100, 0, 100],
width="10c",
frame=Frame(axes="WSen", axis=Axis(annot=True)),
style="c0.1c",
)


def test_ternary_frame_secondary_axes_not_supported(array):
"""
Test that secondary axes are rejected for ternary diagrams.
"""
fig = Figure()
with pytest.raises(GMTParameterError, match="secondary axes"):
fig.ternary(
data=array,
region=[0, 100, 0, 100, 0, 100],
width="10c",
frame=Frame(xaxis2=Axis(annot=True)),
style="c0.1c",
)
Loading