Source code for pygmt.src.ternary

"""
ternary - Plot data on ternary diagrams.
"""

from collections.abc import Sequence
from typing import Literal

from pygmt._typing import PathLike, TableLike
from pygmt.alias import Alias, AliasSystem
from pygmt.clib import Session
from pygmt.exceptions import GMTValueError
from pygmt.helpers import build_arg_list, fmt_docstring, use_alias
from pygmt.params import Axis, Frame
from pygmt.params.frame import _Axes


def _ternary_frame(frame):
    """
    Convert 'frame' 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 string or
    a list of strings with the correct axis prefixes.

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

    Returns
    -------
    str, bool, or list of str
        The converted frame parameter. For Frame inputs, returns a list of strings;
        for Axis, str, bool, or list inputs, returns the value directly.

    Examples
    --------
    >>> from pygmt.params import Axis, Frame
    >>> _ternary_frame(Axis(annot=True, tick=True, grid=True))
    ['afg', '']
    >>> _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(Frame(fill="lightblue", axis=Axis(annot=True)))
    ['+glightblue', 'a']
    >>> _ternary_frame("afg")
    ['afg', '']
    >>> _ternary_frame(True)
    True
    >>> _ternary_frame(["aafg+lWater", "bafg+lAir", "cafg+lLimestone"])
    ['aafg+lWater', 'bafg+lAir', 'cafg+lLimestone']
    >>> _ternary_frame("none")
    'none'
    >>> _ternary_frame(Frame(axes="WSen", axis=Axis(annot=True)))
    Traceback (most recent call last):
    pygmt.exceptions.GMTValueError: ...
    >>> _ternary_frame(Frame(xaxis2=Axis(annot=True)))
    Traceback (most recent call last):
    pygmt.exceptions.GMTValueError: ...
    """
    if isinstance(frame, Axis):
        axis_str = str(frame)
        if axis_str:
            return [axis_str, ""]
        return axis_str
    if isinstance(frame, Frame):
        _attributes = ["title", "subtitle", "fill", "axis", "xaxis", "yaxis", "zaxis"]
        if any(
            _attr not in _attributes and getattr(frame, _attr) for _attr in vars(frame)
        ):
            raise GMTValueError(
                repr(frame),
                description="frame setting",
                reason="For ternary diagrams, only Frame attributes "
                f"{', '.join(repr(_attr) for _attr in _attributes)} are supported.",
            )
        frame_settings = _Axes(
            title=frame.title, subtitle=frame.subtitle, fill=frame.fill
        )
        params = [
            Alias(frame_settings) if str(frame_settings) else Alias(None),
            Alias(frame.axis),
            Alias(frame.xaxis, prefix="a"),
            Alias(frame.yaxis, prefix="b"),
            Alias(frame.zaxis, prefix="c"),
        ]
        result = [par._value for par in params if par._value is not None]
        # When only general axis settings are used without frame-level settings
        # (title/fill) or axis-specific settings (xaxis/yaxis/zaxis), GMT needs
        # a bare -B to draw the frame border. E.g., -Bafg alone doesn't draw it.
        if not str(frame_settings) and not any((frame.xaxis, frame.yaxis, frame.zaxis)):
            result.append("")
        return result
    if isinstance(frame, str) and frame not in {"", "none", "+n"}:
        return [frame, ""]
    return frame


@fmt_docstring
@use_alias(C="cmap", G="fill", JX="width", S="style", W="pen")
def ternary(  # noqa: PLR0913
    self,
    data: PathLike | TableLike,
    alabel: str | None = None,
    blabel: str | None = None,
    clabel: str | None = None,
    region: Sequence[float | str] | str | None = None,
    frame: Frame | Axis | Literal["none"] | str | Sequence[str] | bool = False,
    verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
    | bool = False,
    panel: int | Sequence[int] | bool = False,
    perspective: float | Sequence[float] | str | bool = False,
    transparency: float | None = None,
    **kwargs,
):
    r"""
    Plot data on ternary diagrams.

    Reads (*a*,\ *b*,\ *c*\ [,\ *z*]) records from *data* and plots symbols at
    those locations on a ternary diagram. If a symbol is selected and no symbol
    size given, then we will interpret the fourth column of the input data as
    symbol size. Symbols whose *size* is <= 0 are skipped. If no symbols are
    specified then the symbol code (see ``style`` below) must be present as
    last column in the input.  If ``style`` is not specified then we instead
    plot lines or polygons.

    Full GMT docs at :gmt-docs:`ternary.html`.

    $aliases
       - B = frame
       - L = alabel/blabel/clabel
       - R = region
       - V = verbose
       - c = panel
       - p = perspective
       - t = transparency

    Parameters
    ----------
    data
        Pass in either a file name to an ASCII data table, a Python list, a 2-D
        $table_classes.
    width : str
        Set the width of the figure by passing a number, followed by
        a unit (**i** for inches, **c** for centimeters). Use a negative width
        to indicate that positive axes directions be clock-wise
        [Default lets the a, b, c axes be positive in a
        counter-clockwise direction].
    region : str or list
        [*amin*, *amax*, *bmin*, *bmax*, *cmin*, *cmax*].
        Give the min and max limits for each of the three axes **a**, **b**,
        and **c**.
    $frame
        For ternary diagrams, use :class:`pygmt.params.Frame` ``xaxis``, ``yaxis``, and
        ``zaxis`` attributes to set the **a**, **b**, and **c** axes, respectively.
    $cmap
    $fill
    alabel
        Set the label for the *a* vertex where the component is 100%. The label is
        placed at a distance of three times the :gmt-term:`MAP_LABEL_OFFSET` setting
        from the corner.
    blabel
        Same as ``alabel`` but for the *b* vertex.
    clabel
        Same as ``alabel`` but for the *c* vertex.
    style : str
        *symbol*\[\ *size*].
        Plot individual symbols in a ternary diagram.
    $pen
    $verbose
    $panel
    $perspective
    $transparency
    """
    self._activate_figure()
    # -Lalabel/blabel/clabel. '-' means skipping the label.
    _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

    aliasdict = AliasSystem(
        L=Alias(labels, name="alabel/blabel/clabel", sep="/", size=3),
    ).add_common(
        B=_ternary_frame(frame),
        R=region,
        V=verbose,
        c=panel,
        p=perspective,
        t=transparency,
    )
    aliasdict.merge(kwargs)

    with Session() as lib:
        with lib.virtualfile_in(check_kind="vector", data=data) as vintbl:
            lib.call_module(
                module="ternary",
                args=build_arg_list(aliasdict, infile=vintbl),
            )