Source code for pygmt.src.hlines

"""
hlines - Plot horizontal lines.
"""

from collections.abc import Sequence

import numpy as np
from pygmt.exceptions import GMTValueError

__doctest_skip__ = ["hlines"]


def hlines(
    self,
    y: float | Sequence[float],
    xmin: float | Sequence[float] | None = None,
    xmax: float | Sequence[float] | None = None,
    pen: str | None = None,
    label: str | None = None,
    no_clip: bool = False,
    perspective: str | bool | None = None,
):
    """
    Plot one or multiple horizontal line(s).

    This method is a high-level wrapper around :meth:`pygmt.Figure.plot` that focuses on
    plotting horizontal lines at Y-coordinates specified by the ``y`` parameter. The
    ``y`` parameter can be a single value (for a single horizontal line) or a sequence
    of values (for multiple horizontal lines).

    By default, the X-coordinates of the start and end points of the lines are set to
    be the X-limits of the current plot, but this can be overridden by specifying the
    ``xmin`` and ``xmax`` parameters. ``xmin`` and ``xmax`` can be either a single
    value or a sequence of values. If a single value is provided, it is applied to all
    lines. If a sequence is provided, the length of ``xmin`` and ``xmax`` must match
    the length of ``y``.

    The term "horizontal" lines can be interpreted differently in different coordinate
    systems:

    - **Cartesian**: lines are plotted as straight lines.
    - **Polar**: lines are plotted as arcs along a constant radius.
    - **Geographic**: lines are plotted as arcs along parallels (i.e., constant
      latitude).

    Parameters
    ----------
    y
        Y-coordinates to plot the lines. It can be a single value (for a single line)
        or a sequence of values (for multiple lines).
    xmin/xmax
        X-coordinates of the start/end point(s) of the line(s). If ``None``, defaults to
        the X-limits of the current plot. ``xmin`` and ``xmax`` can be either a single
        value or a sequence of values. If a single value is provided, it is applied to
        all lines. If a sequence is provided, the length of ``xmin`` and ``xmax`` must
        match the length of ``y``.
    pen
        Pen attributes for the line(s), in the format of *width,color,style*.
    label
        Label for the line(s), to be displayed in the legend.
    no_clip
        Do **not** clip lines outside the plot region. Only makes sense in the Cartesian
        coordinate system. [Default is ``False`` to clip lines at the plot region.]
    perspective
        Select perspective view and set the azimuth and elevation angle of the
        viewpoint. Refer to :meth:`pygmt.Figure.plot` for details.

    Examples
    --------
    >>> import pygmt
    >>> fig = pygmt.Figure()
    >>> fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True)
    >>> fig.hlines(y=1, pen="1p,black", label="Line at y=1")
    >>> fig.hlines(y=2, xmin=2, xmax=8, pen="1p,red,-", label="Line at y=2")
    >>> fig.hlines(y=[3, 4], xmin=3, xmax=7, pen="1p,black,.", label="Lines at y=3,4")
    >>> fig.hlines(y=[5, 6], xmin=4, xmax=9, pen="1p,red", label="Lines at y=5,6")
    >>> fig.hlines(
    ...     y=[7, 8], xmin=[0, 1], xmax=[7, 8], pen="1p,blue", label="Lines at y=7,8"
    ... )
    >>> fig.legend()
    >>> fig.show()
    """
    self._activate_figure()

    # Determine the x limits from the current plot region if not specified.
    if xmin is None or xmax is None:
        xlimits = self.region[:2]
        if xmin is None:
            xmin = xlimits[0]
        if xmax is None:
            xmax = xlimits[1]

    # Ensure y/xmin/xmax are 1-D arrays.
    _y = np.atleast_1d(y)
    _xmin = np.atleast_1d(xmin)
    _xmax = np.atleast_1d(xmax)

    nlines = len(_y)  # Number of lines to plot.

    # Check if xmin/xmax are scalars or have the expected length.
    if _xmin.size not in {1, nlines} or _xmax.size not in {1, nlines}:
        _value = f"{_xmin.size}, {_xmax.size}"
        raise GMTValueError(
            _value,
            description="size for 'xmin'/'xmax'",
            reason=f"'xmin'/'xmax' are expected to be scalars or have lengths {nlines!r}.",
        )

    # Repeat xmin/xmax to match the length of y if they are scalars.
    if nlines != 1:
        if _xmin.size == 1:
            _xmin = np.repeat(_xmin, nlines)
        if _xmax.size == 1:
            _xmax = np.repeat(_xmax, nlines)

    # Call the Figure.plot method to plot the lines.
    for i in range(nlines):
        # Special handling for label.
        # 1. Only specify a label when plotting the first line.
        # 2. The -l option can accept comma-separated labels for labeling multiple lines
        #    with auto-coloring enabled. We don't need this feature here, so we need to
        #    replace comma with \054 if the label contains commas.
        _label = label.replace(",", "\\054") if label and i == 0 else None

        # By default, points are connected as great circle arcs in geographic coordinate
        # systems and straight lines in Cartesian coordinate systems (including polar
        # projection). To plot "horizontal" lines along constant latitude (in geographic
        # coordinate systems) or constant radius (in polar projection), we need to
        # resample the line to at least 4 points.
        npoints = 4  # 2 for Cartesian, at least 4 for geographic and polar projections.
        self.plot(
            x=np.linspace(_xmin[i], _xmax[i], npoints),
            y=[_y[i]] * npoints,
            pen=pen,
            label=_label,
            no_clip=no_clip,
            perspective=perspective,
            straight_line="x",
        )