diff --git a/pygmt/src/ternary.py b/pygmt/src/ternary.py index 3112fbc3344..b34e2895af1 100644 --- a/pygmt/src/ternary.py +++ b/pygmt/src/ternary.py @@ -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 @@ -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, @@ -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( diff --git a/pygmt/tests/test_ternary.py b/pygmt/tests/test_ternary.py index ba0fbe4ddcc..f328d7b6083 100644 --- a/pygmt/tests/test_ternary.py +++ b/pygmt/tests/test_ternary.py @@ -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") @@ -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", ) @@ -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", ) @@ -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", + )