Source code for graphinglib.legend_artists

from matplotlib.artist import Artist
from matplotlib.collections import LineCollection
from matplotlib.colors import is_color_like
from matplotlib.legend import Legend
from matplotlib.legend_handler import HandlerLineCollection
from matplotlib.lines import Line2D
from matplotlib.markers import MarkerStyle
from matplotlib.patches import Polygon, Patch
from matplotlib.transforms import Transform
from matplotlib.typing import ColorType
from numpy import array, full_like
from typing import Any, Literal, Optional, Protocol, Sequence, runtime_checkable


class HandlerMultipleLines(HandlerLineCollection):
    """
    Custom Handler for `LineCollection <https://matplotlib.org/stable/api/collections_api.html#matplotlib.collections.LineCollection>`_ objects.

    .. seealso:: The Matplotlib documentation on `legend handlers <https://matplotlib.org/stable/api/legend_handler_api.html>`_.
    """

    def create_artists(
        self,
        legend: Legend,
        orig_handle: Artist,
        xdescent: float,
        ydescent: float,
        width: float,
        height: float,
        fontsize: float,
        trans: Transform,
    ) -> list[Line2D]:
        numlines = len(orig_handle.get_segments())
        xdata, _ = self.get_xdata(legend, xdescent, ydescent, width, height, fontsize)
        lines = []
        ydata = full_like(xdata, height / (numlines + 1))
        for i in range(numlines):
            line = Line2D(xdata, ydata * (numlines - i) - ydescent)
            self.update_prop(line, orig_handle, legend)
            try:
                color = orig_handle.get_colors()[i]
            except IndexError:
                color = orig_handle.get_colors()[0]
            try:
                dashes = orig_handle.get_dashes()[i]
            except IndexError:
                dashes = orig_handle.get_dashes()[0]
            if dashes[1] is not None:
                line.set_dashes(dashes[1])
            line.set_color(color)
            line.set_transform(trans)
            line.set_linewidth(2)
            lines.append(line)
        return lines


class HandlerMultipleVerticalLines(HandlerLineCollection):
    """
    Custom handler for :class:`~graphinglib.legend_artists.VerticalLineCollection` objects.

    .. seealso:: The Matplotlib documentation on `legend handlers <https://matplotlib.org/stable/api/legend_handler_api.html>`_.
    """

    def create_artists(
        self,
        legend: Legend,
        orig_handle: Artist,
        xdescent: float,
        ydescent: float,
        width: float,
        height: float,
        fontsize: float,
        trans: Transform,
    ) -> list[Line2D]:
        numlines = len(orig_handle.get_segments())
        lines = []
        xdata = array([width / (numlines + 1), width / (numlines + 1)])
        ydata = array([0, height])
        for i in range(numlines):
            line = Line2D(xdata * (numlines - i) - xdescent, ydata - ydescent)
            self.update_prop(line, orig_handle, legend)
            try:
                color = orig_handle.get_colors()[i]
            except IndexError:
                color = orig_handle.get_colors()[0]
            try:
                dashes = orig_handle.get_dashes()[i]
            except IndexError:
                dashes = orig_handle.get_dashes()[0]
            if dashes[1] is not None:
                line.set_dashes(dashes[1])
            line.set_color(color)
            line.set_transform(trans)
            line.set_linewidth(2)
            lines.append(line)
        return lines


class VerticalLineCollection(LineCollection):
    """
    Dummy class for vertical `LineCollection <https://matplotlib.org/stable/api/collections_api.html#matplotlib.collections.LineCollection>`_.
    """

    pass


def histogram_legend_artist(
    legend: Legend,
    orig_handle: Artist,
    xdescent: float,
    ydescent: float,
    width: float,
    height: float,
    fontsize: float,
) -> Polygon:
    """
    The custom :class:`~graphinglib.data_plotting_1d.Histogram` legend artist.
    """
    xy = array(
        [[0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 0], [0, 4, 4, 2.5, 2.5, 5, 5, 1.5, 1.5, 0, 0]]
    ).T
    xy[:, 0] = width * xy[:, 0] / 4 + xdescent
    xy[:, 1] = height * xy[:, 1] / 5 - ydescent
    patch = Polygon(xy)
    return patch


@runtime_checkable
class LegendElement(Protocol):
    """
    This class implements a legend element that can be used to create custom legend entries for the
    :meth:`~graphinglib.SmartFigure.set_custom_legend` method. It should not be used on its own and must be subclassed
    to create specific legend elements that implement the `handle` property, which returns a Matplotlib artist. All
    parameters are also available as properties.
    """

    @property
    def handle(self) -> Artist:
        """
        Returns the Matplotlib artist that represents this legend element.
        """
        raise NotImplementedError("Subclasses must implement this method.")

    @property
    def label(self) -> str:
        return self._label

    @label.setter
    def label(self, value: str) -> None:
        self._label = value

    @property
    def alpha(self) -> float:
        return self._alpha

    @alpha.setter
    def alpha(self, value: float) -> None:
        if not isinstance(value, (int, float)):
            raise TypeError("Alpha value must be a number.")
        if not (0 <= value <= 1):
            raise ValueError("Alpha value must be between 0 and 1.")
        self._alpha = value

    def _color_setter(self, attr: str, value: ColorType) -> None:
        if value is not None:
            if not is_color_like(value):
                raise ValueError(f"'{value}' is not a valid color.")
        setattr(self, f"_{attr}", value)

    def _number_setter(self, attr: str, value: float) -> None:
        if not isinstance(value, (int, float)):
            raise TypeError(f"'{value}' is not a valid number.")
        if value < 0:
            raise ValueError(f"'{value}' cannot be negative.")
        setattr(self, f"_{attr}", value)

    def _line_style_setter(
        self,
        attr: str,
        value: Literal["-", "--", "-.", ":", "solid", "dashed", "dashdot", "dotted"]
        | tuple[float, Sequence],
    ) -> None:
        if isinstance(value, str):
            if value not in [
                "-",
                "--",
                "-.",
                ":",
                "solid",
                "dashed",
                "dashdot",
                "dotted",
            ]:
                raise ValueError(f"'{value}' is not a valid line style.")
        elif isinstance(value, tuple):
            if len(value) != 2 or not all(
                isinstance(x, (int, float)) for x in [value[0], *value[1]]
            ):
                raise ValueError(f"'{value}' is not a valid line style tuple.")
        else:
            raise TypeError(f"'{value}' is not a valid line style type.")
        setattr(self, f"_{attr}", value)


[docs] class LegendLine(LegendElement): """ This class implements a legend line wrapping the `Line2D <https://matplotlib.org/stable/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D>`_ object for creating custom legend entries with the :meth:`~graphinglib.SmartFigure.set_custom_legend` method. All parameters are also available as properties. Parameters ---------- label : str The label for the legend line. color : ColorType The color of the line. gap_color : ColorType, optional The color of the gaps in the line (for dashed lines). line_width : float, optional The width of the line in points. Typical range is ``0.5`` to ``4``. Defaults to ``2.0``. line_style : {"-", "--", "-.", ":", "solid", "dashed", "dashdot", "dotted"} or tuple of float and sequence, optional The style of the line, which can be `any pattern supported by Matplotlib <https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.Patch.html#matplotlib.patches.Patch.set_linestyle>`_. Defaults to ``"-"`` (solid line). alpha : float, optional The transparency level of the line, between ``0`` (fully transparent) and ``1`` (fully opaque). Defaults to ``1.0``. Notes ----- Color parameters accept Matplotlib color formats: named colors (``"blue"``), short color strings (``"b"``), hex strings (``"#0000ff"``), grayscale strings (``"0.5"``), and RGB/RGBA tuples with values between ``0`` and ``1`` (``(0, 0, 1)`` or ``(0, 0, 1, 0.5)``). """
[docs] def __init__( self, label: str, color: ColorType, gap_color: Optional[ColorType] = None, line_width: float = 2.0, line_style: Literal[ "-", "--", "-.", ":", "solid", "dashed", "dashdot", "dotted" ] | tuple[float, Sequence] = "-", alpha: float = 1.0, ) -> None: self.label = label self.color = color self.gap_color = gap_color self.line_width = line_width self.line_style = line_style self.alpha = alpha
@property def handle(self) -> Line2D: """ Returns the Matplotlib Line2D artist that represents this legend line. """ return Line2D( [], [], label=self._label, color=self._color, gapcolor=self._gap_color, linewidth=self._line_width, linestyle=self._line_style, alpha=self._alpha, ) @property def color(self) -> ColorType: return self._color @color.setter def color(self, value: ColorType) -> None: self._color_setter("color", value) @property def gap_color(self) -> ColorType: return self._gap_color @gap_color.setter def gap_color(self, value: ColorType) -> None: self._color_setter("gap_color", value) @property def line_width(self) -> float: return self._line_width @line_width.setter def line_width(self, value: float) -> None: self._number_setter("line_width", value) @property def line_style( self, ) -> ( Literal["-", "--", "-.", ":", "solid", "dashed", "dashdot", "dotted"] | tuple[float, Sequence] ): return self._line_style @line_style.setter def line_style( self, value: Literal["-", "--", "-.", ":", "solid", "dashed", "dashdot", "dotted"] | tuple[float, Sequence], ) -> None: self._line_style_setter("line_style", value)
[docs] class LegendMarker(LegendElement): """ This class implements a legend marker wrapping the `Line2D <https://matplotlib.org/stable/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D>`_ object with a marker style set for creating custom legend entries with the :meth:`~graphinglib.SmartFigure.set_custom_legend` method. All parameters are also available as properties. Parameters ---------- label : str The label for the legend line. face_color : ColorType The color of the marker. face_color_alt : ColorType, optional The alternative face color of the marker (for markers with two colors). edge_color : ColorType, optional The color of the marker edge. edge_width : float, optional The width of the marker edge in points. Typical range is ``0`` to ``3``. Defaults to ``1.0``. marker_size : float, optional The size of the marker in points. Typical range is ``4`` to ``12``. Defaults to ``6.0``. marker_style : Any, optional The style of the marker, which can be `any marker style supported by Matplotlib <https://matplotlib.org/stable/api/markers_api.html#matplotlib.markers.MarkerStyle>`_. Values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``, ``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, and ``"_"``. Defaults to ``"o"`` (circle). fill_style : {"full", "left", "right", "bottom", "top"}, optional The fill style of the marker. Values are ``"full"``, ``"left"``, ``"right"``, ``"bottom"``, and ``"top"``. Defaults to ``"full"``. alpha : float, optional The transparency level of the marker, between ``0`` (fully transparent) and ``1`` (fully opaque). Defaults to ``1.0``. Notes ----- Color parameters accept Matplotlib color formats: named colors (``"blue"``), short color strings (``"b"``), hex strings (``"#0000ff"``), grayscale strings (``"0.5"``), and RGB/RGBA tuples with values between ``0`` and ``1`` (``(0, 0, 1)`` or ``(0, 0, 1, 0.5)``). """
[docs] def __init__( self, label: str, face_color: Optional[ColorType] = None, face_color_alt: Optional[ColorType] = None, edge_color: Optional[ColorType] = None, edge_width: float = 1.0, marker_size: float = 6.0, marker_style: Any = "o", fill_style: Literal["full", "left", "right", "bottom", "top"] = "full", alpha: float = 1.0, ) -> None: self.label = label self.face_color = face_color self.face_color_alt = face_color_alt self.edge_color = edge_color self.edge_width = edge_width self.marker_size = marker_size self.marker_style = marker_style self.fill_style = fill_style self.alpha = alpha
@property def handle(self) -> Line2D: """ Returns the Matplotlib Line2D artist that represents this legend marker. """ return Line2D( [], [], linestyle="none", label=self._label, markerfacecolor=self._face_color if self._face_color is not None else "none", markerfacecoloralt=self._face_color_alt if self._face_color_alt is not None else "none", markeredgecolor=self._edge_color if self._edge_color is not None else "none", markeredgewidth=self._edge_width, markersize=self._marker_size, marker=self._marker_style, fillstyle=(self._fill_style if self._fill_style is not None else "none"), alpha=self._alpha, ) @property def face_color(self) -> ColorType: return self._face_color @face_color.setter def face_color(self, value: ColorType) -> None: self._color_setter("face_color", value) @property def face_color_alt(self) -> ColorType: return self._face_color_alt @face_color_alt.setter def face_color_alt(self, value: ColorType) -> None: self._color_setter("face_color_alt", value) @property def edge_color(self) -> ColorType: return self._edge_color @edge_color.setter def edge_color(self, value: ColorType) -> None: self._color_setter("edge_color", value) @property def edge_width(self) -> float: return self._edge_width @edge_width.setter def edge_width(self, value: float) -> None: self._number_setter("edge_width", value) @property def marker_size(self) -> float: return self._marker_size @marker_size.setter def marker_size(self, value: float) -> None: self._number_setter("marker_size", value) @property def marker_style(self) -> Any: return self._marker_style @marker_style.setter def marker_style(self, value: Any) -> None: try: MarkerStyle(value) # Validate the marker style except Exception: raise ValueError(f"'{value}' is not a valid marker style.") self._marker_style = value @property def fill_style(self) -> Literal["full", "left", "right", "bottom", "top"]: return self._fill_style @fill_style.setter def fill_style( self, value: Literal["full", "left", "right", "bottom", "top"] ) -> None: if value is not None: if value not in MarkerStyle.fillstyles: raise ValueError(f"'{value}' is not a valid fill style.") self._fill_style = value
[docs] class LegendPatch(LegendElement): """ This class implements a legend patch wrapping the `Patch <https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.Patch.html#matplotlib.patches.Patch>`_ object for creating custom legend entries with the :meth:`~graphinglib.SmartFigure.set_custom_legend` method. All parameters are also available as properties. Parameters ---------- label : str The label for the legend patch. face_color : ColorType The face color of the patch. edge_color : ColorType, optional The edge color of the patch. line_width : float, optional The width of the patch edge and hatch (if present) in points. Typical range is ``0.5`` to ``4``. Defaults to ``1.0``. line_style : {"-", "--", "-.", ":", "solid", "dashed", "dashdot", "dotted"} or tuple of float and sequence, optional The style of the patch edge, which can be `any pattern supported by Matplotlib <https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.Patch.html#matplotlib.patches.Patch.set_linestyle>`_. Defaults to ``"-"`` (solid line). hatch : {"/", "\\", "|", "-", "+", "x", "o", "O", ".", "*"}, optional The hatch pattern of the patch, which can be `any hatch pattern supported by Matplotlib <https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.Patch.html#matplotlib.patches.Patch.set_hatch>`_. Values include ``"/"``, ``"\\"``, ``"|"``, ``"-"``, ``"+"``, ``"x"``, ``"o"``, ``"O"``, ``"."``, and ``"*"``. alpha : float, optional The transparency level of the patch, between ``0`` (fully transparent) and ``1`` (fully opaque). Defaults to ``1.0``. Notes ----- Color parameters accept Matplotlib color formats: named colors (``"blue"``), short color strings (``"b"``), hex strings (``"#0000ff"``), grayscale strings (``"0.5"``), and RGB/RGBA tuples with values between ``0`` and ``1`` (``(0, 0, 1)`` or ``(0, 0, 1, 0.5)``). """
[docs] def __init__( self, label: str, face_color: Optional[ColorType] = None, edge_color: Optional[ColorType] = None, line_width: float = 1.0, line_style: Literal[ "-", "--", "-.", ":", "solid", "dashed", "dashdot", "dotted" ] | tuple[float, Sequence] = "-", hatch: Literal["/", "\\", "|", "-", "+", "x", "o", "O", ".", "*"] = None, alpha: float = 1.0, ) -> None: self.label = label self.face_color = face_color self.edge_color = edge_color self.line_width = line_width self.line_style = line_style self.hatch = hatch self.alpha = alpha
@property def handle(self) -> Patch: """ Returns the Matplotlib Patch artist that represents this legend patch. """ return Patch( label=self._label, facecolor=(self._face_color if self._face_color is not None else "none"), edgecolor=(self._edge_color if self._edge_color is not None else "none"), linewidth=self._line_width, linestyle=self._line_style, hatch=self._hatch, alpha=self._alpha, fill=(self._face_color is not None), ) @property def face_color(self) -> ColorType: return self._face_color @face_color.setter def face_color(self, value: ColorType) -> None: self._color_setter("face_color", value) @property def edge_color(self) -> ColorType: return self._edge_color @edge_color.setter def edge_color(self, value: ColorType) -> None: self._color_setter("edge_color", value) @property def line_width(self) -> float: return self._line_width @line_width.setter def line_width(self, value: float) -> None: self._number_setter("line_width", value) @property def line_style( self, ) -> ( Literal["-", "--", "-.", ":", "solid", "dashed", "dashdot", "dotted"] | tuple[float, Sequence] ): return self._line_style @line_style.setter def line_style( self, value: Literal["-", "--", "-.", ":", "solid", "dashed", "dashdot", "dotted"] | tuple[float, Sequence], ) -> None: self._line_style_setter("line_style", value) @property def hatch(self) -> Literal["/", "\\", "|", "-", "+", "x", "o", "O", ".", "*"]: return self._hatch @hatch.setter def hatch( self, value: Literal["/", "\\", "|", "-", "+", "x", "o", "O", ".", "*"] ) -> None: if value is not None: # This logic is adapted from matplotlib's hatch validation valid_hatch_patterns = set(r"-+|/\xXoO.*") invalids = set(value).difference(valid_hatch_patterns) if invalids: raise ValueError(f"Invalid hatch pattern(s): {invalids}") self._hatch = value