Source code for graphinglib.graph_elements

from __future__ import annotations

from .inherit import INHERIT, Inherit

from copy import deepcopy
from dataclasses import dataclass, field
from typing import Literal, Optional, Protocol, runtime_checkable

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import LineCollection
from matplotlib.figure import Figure as MPLFigure
from numpy.typing import ArrayLike

from .legend_artists import VerticalLineCollection
from .tools import _copy_with_overrides

try:
    from typing import Self
except ImportError:
    from typing_extensions import Self


@runtime_checkable
class Plottable(Protocol):
    """
    Dummy class for a general plottable object.

    .. attention:: Not to be used directly.

    """

    def copy_with(self, **kwargs) -> Self:
        """
        Returns a deep copy of the Plottable with specified attributes overridden. This is useful when multiple
        properties need to be changed in copies of Plottable objects, as it allows to modify the attributes in a single
        call.

        Parameters
        ----------
        **kwargs
            Public writable properties to override in the copied Plottable. The keys should be property names to
            modify and the values are the new values for those properties.

        Returns
        -------
        Self
            A new instance with the specified attributes overridden.

        Examples
        --------
        Copy an existing Curve and change the color and line_style at the same time::

            curve = Curve(x_data, y_data, color='blue')
            new_curve = curve.copy_with(color='red', line_style='dashed')
        """
        return _copy_with_overrides(self, **kwargs)

    def __deepcopy__(self, memo: dict) -> Self:
        """
        Creates a deep copy of the Plottable instance, intentionally excluding the 'handle' attribute from the copy.
        This avoids issues when copying a Plottable that has been previously drawn and stored.
        """
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        excluded_attrs = ["handle"]
        for property_, value in self.__dict__.items():
            if property_ not in excluded_attrs:
                result.__dict__[property_] = deepcopy(value, memo)
        for attr in excluded_attrs:
            if hasattr(self, attr):
                setattr(result, attr, None)
        return result

    def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs) -> None:
        """
        Plots the element in the specified
        `Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_.
        """
        pass


class GraphingException(Exception):
    """
    General exception raised for the GraphingLib modules.
    """

    pass


[docs] class Hlines(Plottable): """ This class implements simple horizontal lines. Parameters ---------- y : ArrayLike Vertical positions at which the lines should be plotted. x_min : ArrayLike, optional Horizontal start position of the lines. Each lines can have a different start. If not specified, lines will span the entire axes. Defaults to ``None``. x_max : ArrayLike, optional Horizontal end position of the lines. Each lines can habe a different end. If not specified, lines will span the entire axes. Defaults to ``None``. label : str, optional Label to be displayed in the legend. colors : list[str] Colors to use for the lines. One color for every line or a color per line can be specified. Default depends on the ``figure_style`` configuration. line_widths : list[float] Line widths to use for the lines. One width for every line or a width per line can be specified. Typical range is ``0.5`` to ``4``. Default depends on the ``figure_style`` configuration. line_styles : list[str] Line styles to use for the lines. One style for every line or a style per line can be specified. Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and ``"dotted"``. Default depends on the ``figure_style`` configuration. alpha : float Opacity of the lines. Range is ``0`` (transparent) to ``1`` (opaque). Default depends on the ``figure_style`` configuration. 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, y: ArrayLike, x_min: Optional[ArrayLike] = None, x_max: Optional[ArrayLike] = None, label: Optional[str] = None, colors: list[str] | str | Inherit = INHERIT, line_widths: list[float] | float | Inherit = INHERIT, line_styles: list[str] | str | Inherit = INHERIT, alpha: float | Inherit = INHERIT, ) -> None: self._in_init = True self._y = None self._x_min = None self._x_max = None self._colors = INHERIT self._line_widths = INHERIT self._line_styles = INHERIT self.y = y self.x_min = x_min self.x_max = x_max self.label = label self.colors = colors self.line_widths = line_widths self.line_styles = line_styles self._alpha = alpha self._in_init = False self._validate_state()
def _validate_state(self) -> None: if (self._x_min is None) ^ (self._x_max is None): raise GraphingException( "Either both x_min and x_max are specified or none of them" ) if isinstance(self._y, (int, float)) and isinstance( self._colors, (list, np.ndarray) ): if len(self._colors) > 1: raise GraphingException( "There can't be multiple colors for a single line!" ) if isinstance(self._y, (int, float)) and isinstance( self._line_styles, (list, np.ndarray) ): if len(self._line_styles) > 1: raise GraphingException( "There can't be multiple line styles for a single line!" ) if isinstance(self._y, (int, float)) and isinstance( self._line_widths, (list, np.ndarray) ): if len(self._line_widths) > 1: raise GraphingException( "There can't be multiple line widths for a single line!" ) if isinstance(self._y, (list, np.ndarray)): if isinstance(self._colors, list) and len(self._y) != len(self._colors): raise GraphingException( "There must be the same number of colors and lines!" ) if isinstance(self._line_styles, list) and len(self._y) != len( self._line_styles ): raise GraphingException( "There must be the same number of line styles and lines!" ) if isinstance(self._line_widths, list) and len(self._y) != len( self._line_widths ): raise GraphingException( "There must be the same number of line widths and lines!" ) @property def y(self) -> ArrayLike: return self._y @y.setter def y(self, y: ArrayLike) -> None: if isinstance(y, (list, np.ndarray)): self._y = np.asarray(y) else: self._y = y if not self._in_init: self._validate_state() @property def x_min(self) -> ArrayLike | None: return self._x_min @x_min.setter def x_min(self, x_min: Optional[ArrayLike]) -> None: if isinstance(x_min, (list, np.ndarray)): self._x_min = np.asarray(x_min) else: self._x_min = x_min if not self._in_init: self._validate_state() @property def x_max(self) -> ArrayLike | None: return self._x_max @x_max.setter def x_max(self, x_max: Optional[ArrayLike]) -> None: if isinstance(x_max, (list, np.ndarray)): self._x_max = np.asarray(x_max) else: self._x_max = x_max if not self._in_init: self._validate_state() @property def label(self) -> Optional[str]: return self._label @label.setter def label(self, label: Optional[str]) -> None: self._label = label @property def colors(self) -> list[str] | str: return self._colors @colors.setter def colors(self, colors: list[str] | str) -> None: self._colors = colors if not self._in_init: self._validate_state() @property def line_widths(self) -> list[float] | float: return self._line_widths @line_widths.setter def line_widths(self, line_widths: list[float] | float) -> None: self._line_widths = line_widths if not self._in_init: self._validate_state() @property def line_styles(self) -> list[str] | str: return self._line_styles @line_styles.setter def line_styles(self, line_styles: list[str] | str) -> None: self._line_styles = line_styles if not self._in_init: self._validate_state() @property def alpha(self) -> float | Inherit: return self._alpha
[docs] def copy(self) -> Self: """ Returns a deep copy of the :class:`~graphinglib.graph_elements.Hlines` object. """ return deepcopy(self)
def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs) -> None: """ Plots the element in the specified `Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_. """ if isinstance(self._y, (list, np.ndarray)) and len(self._y) > 1: if self._x_max is not None and self._x_min is not None: params = { "colors": self._colors, "linestyles": self._line_styles, "linewidths": self._line_widths, "alpha": self._alpha, } params = {k: v for k, v in params.items() if v != INHERIT} axes.hlines( self._y, self._x_min, self._x_max, zorder=z_order, **params, ) params.pop("linewidths") else: params = { "color": self._colors, "linestyle": self._line_styles, "linewidth": self._line_widths, "alpha": self._alpha, } params = {k: v for k, v in params.items() if v != INHERIT} for i in range(len(self._y)): axes.axhline( self._y[i], zorder=z_order, **{ k: v if isinstance(v, (int, float, str)) else v[i] for k, v in params.items() }, ) params.pop("linewidth") self.handle = LineCollection( [[(0, 0)]] * (len(self._y) if len(self._y) <= 3 else 3), **params, ) else: if self._x_max is not None and self._x_min is not None: params = { "colors": self._colors, "linestyles": self._line_styles, "linewidths": self._line_widths, "alpha": self._alpha, } params = {k: v for k, v in params.items() if v != INHERIT} axes.hlines( self._y, self._x_min, self._x_max, zorder=z_order, **params, ) params.pop("linewidths") else: params = { "color": self._colors, "linestyle": self._line_styles, "linewidth": self._line_widths, "alpha": self._alpha, } params = {k: v for k, v in params.items() if v != INHERIT} axes.axhline(self._y, zorder=z_order, **params) params.pop("linewidth") if isinstance(self._y, (int, float)): self.handle = LineCollection([[(0, 0)]] * 1, **params) else: self.handle = LineCollection( [[(0, 0)]] * (len(self._y) if len(self._y) <= 3 else 3), **params, )
[docs] class Vlines(Plottable): """ This class implements simple vertical lines. Parameters ---------- x : ArrayLike Horizontal positions at which the lines should be plotted. y_min : ArrayLike, optional Vertical start position of the lines. Each line can have a different start. If not specified, lines will span the entire axes. Defaults to ``None``. y_max : ArrayLike, optional Vertical end position of the lines. Each line can habe a different end. If not specified, lines will span the entire axes. Defaults to ``None``. label : str, optional Label to be displayed in the legend. colors : list[str] Colors to use for the lines. One color for every line or a color per line can be specified. Default depends on the ``figure_style`` configuration. line_widths : list[float] Line widths to use for the lines. One width for every line or a width per line can be specified. Typical range is ``0.5`` to ``4``. Default depends on the ``figure_style`` configuration. line_styles : list[str] Line styles to use for the lines. One style for every line or a style per line can be specified. Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and ``"dotted"``. Default depends on the ``figure_style`` configuration. alpha : float Opacity of the lines. Range is ``0`` (transparent) to ``1`` (opaque). Default depends on the ``figure_style`` configuration. 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, x: ArrayLike, y_min: Optional[ArrayLike] = None, y_max: Optional[ArrayLike] = None, label: Optional[str] = None, colors: list[str] | str | Inherit = INHERIT, line_widths: list[float] | float | Inherit = INHERIT, line_styles: list[str] | str | Inherit = INHERIT, alpha: float | Inherit = INHERIT, ) -> None: self._in_init = True self._x = None self._y_min = None self._y_max = None self._colors = INHERIT self._line_styles = INHERIT self._line_widths = INHERIT self.x = x self.y_min = y_min self.y_max = y_max self.label = label self.colors = colors self.line_styles = line_styles self.line_widths = line_widths self._alpha = alpha self._in_init = False self._validate_state()
def _validate_state(self) -> None: if isinstance(self._x, (int, float)) and isinstance( self._colors, (list, np.ndarray) ): if len(self._colors) > 1: raise GraphingException( "There can't be multiple colors for a single line!" ) if isinstance(self._x, (int, float)) and isinstance( self._line_styles, (list, np.ndarray) ): if len(self._line_styles) > 1: raise GraphingException( "There can't be multiple line styles for a single line!" ) if isinstance(self._x, (int, float)) and isinstance( self._line_widths, (list, np.ndarray) ): if len(self._line_widths) > 1: raise GraphingException( "There can't be multiple line widths for a single line!" ) if isinstance(self._x, (list, np.ndarray)): if isinstance(self._colors, list) and len(self._x) != len(self._colors): raise GraphingException( "There must be the same number of colors and lines!" ) if isinstance(self._line_styles, list) and len(self._x) != len( self._line_styles ): raise GraphingException( "There must be the same number of line styles and lines!" ) if isinstance(self._line_widths, list) and len(self._x) != len( self._line_widths ): raise GraphingException( "There must be the same number of line widths and lines!" ) @property def x(self) -> ArrayLike: return self._x @x.setter def x(self, x: ArrayLike) -> None: if isinstance(x, (list, np.ndarray)): self._x = np.asarray(x) else: self._x = x if not self._in_init: self._validate_state() @property def y_min(self) -> ArrayLike | None: return self._y_min @y_min.setter def y_min(self, y_min: Optional[ArrayLike]) -> None: if isinstance(y_min, (list, np.ndarray)): self._y_min = np.asarray(y_min) else: self._y_min = y_min @property def y_max(self) -> ArrayLike | None: return self._y_max @y_max.setter def y_max(self, y_max: Optional[ArrayLike]) -> None: if isinstance(y_max, (list, np.ndarray)): self._y_max = np.asarray(y_max) else: self._y_max = y_max @property def label(self) -> Optional[str]: return self._label @label.setter def label(self, label: Optional[str]) -> None: self._label = label @property def colors(self) -> list[str] | str: return self._colors @colors.setter def colors(self, colors: list[str] | str) -> None: self._colors = colors if not self._in_init: self._validate_state() @property def line_widths(self) -> list[float] | float: return self._line_widths @line_widths.setter def line_widths(self, line_widths: list[float] | float) -> None: self._line_widths = line_widths if not self._in_init: self._validate_state() @property def line_styles(self) -> list[str] | str: return self._line_styles @line_styles.setter def line_styles(self, line_styles: list[str] | str) -> None: self._line_styles = line_styles if not self._in_init: self._validate_state() @property def alpha(self) -> float | Inherit: return self._alpha @alpha.setter def alpha(self, alpha: float | Inherit) -> None: self._alpha = alpha
[docs] def copy(self) -> Self: """ Returns a deep copy of the :class:`~graphinglib.graph_elements.Vlines` object. """ return deepcopy(self)
def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs) -> None: """ Plots the element in the specified `Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_. """ if isinstance(self._x, (list, np.ndarray)) and len(self._x) > 1: if self._y_min is not None and self._y_max is not None: params = { "colors": self._colors, "linestyles": self._line_styles, "linewidths": self._line_widths, "alpha": self._alpha, } params = {k: v for k, v in params.items() if v != INHERIT} axes.vlines( self._x, self._y_min, self._y_max, zorder=z_order, **params, ) params.pop("linewidths") else: params = { "color": self._colors, "linestyle": self._line_styles, "linewidth": self._line_widths, "alpha": self._alpha, } params = {k: v for k, v in params.items() if v != INHERIT} for i in range(len(self._x)): axes.axvline( self._x[i], zorder=z_order, **{ k: v if isinstance(v, (int, float, str)) else v[i] for k, v in params.items() }, ) params.pop("linewidth") self.handle = VerticalLineCollection( [[(0, 0)]] * (len(self._x) if len(self._x) <= 4 else 4), **params, ) else: if self._y_min is not None and self._y_max is not None: params = { "colors": self._colors, "linestyles": self._line_styles, "linewidths": self._line_widths, "alpha": self._alpha, } params = {k: v for k, v in params.items() if v != INHERIT} axes.vlines( self._x, self._y_min, self._y_max, zorder=z_order, **params, ) params.pop("linewidths") else: params = { "color": self._colors, "linestyle": self._line_styles, "linewidth": self._line_widths, "alpha": self._alpha, } params = {k: v for k, v in params.items() if v != INHERIT} axes.axvline(self._x, zorder=z_order, **params) params.pop("linewidth") if isinstance(self._x, (int, float)): self.handle = VerticalLineCollection([[(0, 0)]] * 1, **params) else: self.handle = VerticalLineCollection( [[(0, 0)]] * (len(self._x) if len(self._x) <= 4 else 4), **params, )
[docs] class Point(Plottable): """ This class implements a point object. The :class:`~graphinglib.graph_elements.Point` object can be used to show important coordinates in a plot or add a label to some point. Parameters ---------- x, y : float The x and y coordinates of the :class:`~graphinglib.graph_elements.Point`. label : str, optional Label to be attached to the :class:`~graphinglib.graph_elements.Point`. face_color : str or None Face color of the marker. Default depends on the ``figure_style`` configuration. edge_color : str or None Edge color of the marker. Default depends on the ``figure_style`` configuration. marker_size : float Size of the marker. Typical range is ``10`` to ``100``. Default depends on the ``figure_style`` configuration. marker_style : str Style of the marker. Values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``, ``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, and ``"_"``. Default depends on the ``figure_style`` configuration. edge_width : float Edge width of the marker. Typical range is ``0`` to ``3``. Default depends on the ``figure_style`` configuration. alpha : float Opacity of the point. Range is ``0`` (transparent) to ``1`` (opaque). Default depends on the ``figure_style`` configuration. font_size : float Font size for the text attached to the marker. Typical range is ``8`` to ``20``. Default depends on the ``figure_style`` configuration. text_color : str Color of the text attached to the marker. "same as point" uses the color of the point (prioritize edge color, then face color). Default depends on the ``figure_style`` configuration. h_align, v_align : str Horizontal and vertical alignment of the text attached to the :class:`~graphinglib.graph_elements.Point`. Horizontal alignment values include ``"left"``, ``"center"``, and ``"right"``. Vertical alignment values include ``"bottom"``, ``"baseline"``, ``"center"``, ``"center_baseline"``, and ``"top"``. Defaults to bottom left. 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, x: float, y: float, label: Optional[str] = None, face_color: Optional[str] | Inherit = INHERIT, edge_color: Optional[str] | Inherit = INHERIT, marker_size: float | Inherit = INHERIT, marker_style: str | Inherit = INHERIT, edge_width: float | Inherit = INHERIT, alpha: float | Inherit = INHERIT, font_size: int | Literal["same as figure"] = "same as figure", text_color: str | Inherit = INHERIT, h_align: str = "left", v_align: str = "bottom", ) -> None: """ This class implements a point object. The point object can be used to show important coordinates in a plot or add a label to some point. Parameters ---------- x, y : float The x and y coordinates of the :class:`~graphinglib.graph_elements.Point`. label : str, optional Label to be attached to the :class:`~graphinglib.graph_elements.Point`. face_color : str or None Face color of the marker. Default depends on the ``figure_style`` configuration. edge_color : str or None Edge color of the marker. Default depends on the ``figure_style`` configuration. marker_size : float Size of the marker. Typical range is ``10`` to ``100``. Default depends on the ``figure_style`` configuration. marker_style : str Style of the marker. Values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``, ``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, and ``"_"``. Default depends on the ``figure_style`` configuration. edge_width : float Edge width of the marker. Typical range is ``0`` to ``3``. Default depends on the ``figure_style`` configuration. alpha : float Opacity of the point. Range is ``0`` (transparent) to ``1`` (opaque). Default depends on the ``figure_style`` configuration. font_size : float Font size for the text attached to the marker. Typical range is ``8`` to ``20``. Default depends on the ``figure_style`` configuration. text_color : str Color of the text attached to the marker. "same as point" uses the color of the point (prioritize edge color, then face color). Default depends on the ``figure_style`` configuration. h_align, v_align : str Horizontal and vertical alignment of the text attached to the :class:`~graphinglib.graph_elements.Point`. Horizontal alignment values include ``"left"``, ``"center"``, and ``"right"``. Vertical alignment values include ``"bottom"``, ``"baseline"``, ``"center"``, ``"center_baseline"``, and ``"top"``. Defaults to bottom left. 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)``). """ self.x = x self.y = y self.label = label self.face_color = face_color self.edge_color = edge_color self.marker_size = marker_size self.marker_style = marker_style self.edge_width = edge_width self.alpha = alpha self.font_size = font_size self.text_color = text_color self.h_align = h_align self.v_align = v_align self._show_coordinates: bool = False
@staticmethod def _validate_coordinate(value: float) -> None: if not isinstance(value, (int, float)) or isinstance(value, bool): raise GraphingException( "The x and y coordinates for a point must be a single number each!" ) @property def x(self) -> float: return self._x @x.setter def x(self, x: float) -> None: self._validate_coordinate(x) self._x = x @property def y(self) -> float: return self._y @y.setter def y(self, y: float) -> None: self._validate_coordinate(y) self._y = y @property def label(self) -> Optional[str]: return self._label @label.setter def label(self, label: Optional[str]) -> None: self._label = label @property def face_color(self) -> str | None: return self._face_color @face_color.setter def face_color(self, face_color: str) -> None: self._face_color = face_color @property def edge_color(self) -> str | None: return self._edge_color @edge_color.setter def edge_color(self, edge_color: str) -> None: self._edge_color = edge_color @property def marker_size(self) -> float | Inherit: return self._marker_size @marker_size.setter def marker_size(self, marker_size: float | Inherit) -> None: self._marker_size = marker_size @property def marker_style(self) -> str: return self._marker_style @marker_style.setter def marker_style(self, marker_style: str) -> None: self._marker_style = marker_style @property def edge_width(self) -> float | Inherit: return self._edge_width @edge_width.setter def edge_width(self, edge_width: float | Inherit) -> None: self._edge_width = edge_width @property def alpha(self) -> float | Inherit: return self._alpha @alpha.setter def alpha(self, alpha: float | Inherit) -> None: self._alpha = alpha @property def font_size(self) -> float | Literal["same as figure"]: return self._font_size @font_size.setter def font_size(self, font_size: float | Literal["same as figure"]) -> None: self._font_size = font_size @property def text_color(self) -> str: return self._text_color @text_color.setter def text_color(self, text_color: str) -> None: self._text_color = text_color @property def h_align(self) -> str: return self._h_align @h_align.setter def h_align(self, h_align: str) -> None: self._h_align = h_align @property def v_align(self) -> str: return self._v_align @v_align.setter def v_align(self, v_align: str) -> None: self._v_align = v_align @property def show_coordinates(self) -> bool: return self._show_coordinates @show_coordinates.setter def show_coordinates(self, show_coordinates: bool) -> None: self._show_coordinates = show_coordinates @property def coordinates(self) -> tuple[float, float]: return (self._x, self._y) @coordinates.setter def coordinates(self, coordinates: tuple[float, float]) -> None: x, y = coordinates self.x = x self.y = y
[docs] def copy(self) -> Self: """ Returns a deep copy of the :class:`~graphinglib.graph_elements.Point` object. """ return deepcopy(self)
[docs] def add_coordinates(self) -> None: """ Displays the coordinates of the :class:`~graphinglib.graph_elements.Point` next to it. """ self._show_coordinates = True
def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs) -> None: """ Plots the element in the specified `Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_. """ if self._face_color is None and self._edge_color is None: raise GraphingException( "Both the face color and edge color of the point can't be None. Set at least one of them." ) size = self._font_size if self._font_size != "same as figure" else None prefix = " " if self._h_align == "left" else "" postfix = " " if self._h_align == "right" else "" if self._label is not None and not self._show_coordinates: point_label = prefix + self._label + postfix else: point_label = None params = { "c": self._face_color if self._face_color is not None else "none", "edgecolors": self._edge_color if self._edge_color is not None else "none", "s": self._marker_size, "marker": self._marker_style, "linewidths": self._edge_width, "alpha": self._alpha, } params = {k: v for k, v in params.items() if v != INHERIT} axes.scatter( self._x, self._y, zorder=z_order, **params, ) # get text color. if _text_color is "same as point", use the color of the point (prioritize edge color, then face color) if self._text_color == "same as point": if self._edge_color is not None: text_color = self._edge_color else: text_color = self._face_color else: text_color = self._text_color params = { "color": text_color, "fontsize": size, "horizontalalignment": self._h_align, "verticalalignment": self._v_align, } params = {k: v for k, v in params.items() if v != INHERIT} axes.annotate( point_label, (self._x, self._y), zorder=z_order, **params, ) if self._show_coordinates: prefix = " " if self._h_align == "left" else "" postfix = " " if self._h_align == "right" else "" if self._label is not None: point_label = ( prefix + self._label + " : " + f"({self._x:.3f}, {self._y:.3f})" + postfix ) else: point_label = prefix + f"({self._x:.3f}, {self._y:.3f})" + postfix if self._text_color == "same as point": if self._edge_color is not None: text_color = self._edge_color else: text_color = self._face_color else: text_color = self._text_color params = { "color": text_color, "fontsize": size, "horizontalalignment": self._h_align, "verticalalignment": self._v_align, } params = {k: v for k, v in params.items() if v != INHERIT} axes.annotate( point_label, (self._x, self._y), zorder=z_order, **params, )
[docs] @dataclass class Text(Plottable): """ This class allows text to be plotted. It is also possible to attach an arrow to the :class:`~graphinglib.graph_elements.Text` with the method :py:meth:`~graphinglib.graph_elements.Text.attach_arrow` to point at something of interest in the plot. Parameters ---------- x, y : float The x and y coordinates at which to plot the :class:`~graphinglib.graph_elements.Text`. text : str The text to be plotted. color : str Color of the text. Default depends on the ``figure_style`` configuration. font_size : float Font size of the text. Typical range is ``8`` to ``20``. Default depends on the ``figure_style`` configuration. alpha : float Opacity of the text. Range is ``0`` (transparent) to ``1`` (opaque). Default depends on the ``figure_style`` configuration. h_align, v_align : str Horizontal and vertical alignment of the text. Horizontal alignment values include ``"left"``, ``"center"``, and ``"right"``. Vertical alignment values include ``"bottom"``, ``"baseline"``, ``"center"``, ``"center_baseline"``, and ``"top"``. Default depends on the ``figure_style`` configuration. rotation : float Rotation angle of the text in degrees. Defaults to 0. highlight_color : str, optional Color of the background highlight box behind the text. If specified, a box will be drawn behind the text. Default is ``None`` (no highlight). highlight_alpha : float, optional Opacity of the highlight box. Range is ``0`` (transparent) to ``1`` (opaque). Defaults to 1.0. highlight_padding : float, optional Padding around the text for the highlight box. A value of 0 means no padding. Defaults to 0.1. 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)``). """ _x: float _y: float _text: str _color: str | Inherit = INHERIT _font_size: float | Literal["same as figure"] = "same as figure" _alpha: float | Inherit = INHERIT _h_align: str | Inherit = INHERIT _v_align: str | Inherit = INHERIT _rotation: float = 0.0 _highlight_color: Optional[str] = None _highlight_alpha: float = 1.0 _highlight_padding: float = 0.1 _arrow_pointing_to: Optional[tuple[float]] = field(default=None, init=False)
[docs] def __init__( self, x: float, y: float, text: str, color: str | Inherit = INHERIT, font_size: float | Literal["same as figure"] = "same as figure", alpha: float | Inherit = INHERIT, h_align: str | Inherit = INHERIT, v_align: str | Inherit = INHERIT, rotation: float = 0.0, highlight_color: Optional[str] = None, highlight_alpha: float = 1.0, highlight_padding: float = 0.1, ) -> None: """ This class allows text to be plotted. It is also possible to attach an arrow to the :class:`~graphinglib.graph_elements.Text` with the method :py:meth:`~graphinglib.graph_elements.Text.attach_arrow` to point at something of interest in the plot. Parameters ---------- x, y : float The x and y coordinates at which to plot the :class:`~graphinglib.graph_elements.Text`. text : str The text to be plotted. color : str Color of the text. Default depends on the ``figure_style`` configuration. font_size : float Font size of the text. Typical range is ``8`` to ``20``. Default depends on the ``figure_style`` configuration. alpha : float Opacity of the text. Range is ``0`` (transparent) to ``1`` (opaque). Default depends on the ``figure_style`` configuration. h_align, v_align : str Horizontal and vertical alignment of the text. Horizontal alignment values include ``"left"``, ``"center"``, and ``"right"``. Vertical alignment values include ``"bottom"``, ``"baseline"``, ``"center"``, ``"center_baseline"``, and ``"top"``. Default depends on the ``figure_style`` configuration. rotation : float Rotation angle of the text in degrees. Defaults to 0. highlight_color : str, optional Color of the background highlight box behind the text. If specified, a box will be drawn behind the text. Default is ``None`` (no highlight). highlight_alpha : float, optional Opacity of the highlight box. Range is ``0`` (transparent) to ``1`` (opaque). Defaults to 1.0. highlight_padding : float, optional Padding around the text for the highlight box. A value of 0 means no padding. Defaults to 0.1. 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)``). """ self._x = x self._y = y self._text = text self._color = color self._font_size = font_size self._alpha = alpha self._h_align = h_align self._v_align = v_align self._rotation = rotation self._highlight_color = highlight_color self._highlight_alpha = highlight_alpha self._highlight_padding = highlight_padding self._arrow_pointing_to = None
@property def x(self) -> float: return self._x @x.setter def x(self, x: float) -> None: self._x = x @property def y(self) -> float: return self._y @y.setter def y(self, y: float) -> None: self._y = y @property def text(self) -> str: return self._text @text.setter def text(self, text: str) -> None: self._text = text @property def color(self) -> str: return self._color @color.setter def color(self, color: str) -> None: self._color = color @property def font_size(self) -> float | Literal["same as figure"]: return self._font_size @font_size.setter def font_size(self, font_size: float | Literal["same as figure"]) -> None: self._font_size = font_size @property def alpha(self) -> float | Inherit: return self._alpha @alpha.setter def alpha(self, alpha: float | Inherit) -> None: self._alpha = alpha @property def h_align(self) -> str: return self._h_align @h_align.setter def h_align(self, h_align: str) -> None: self._h_align = h_align @property def v_align(self) -> str: return self._v_align @v_align.setter def v_align(self, v_align: str) -> None: self._v_align = v_align @property def rotation(self) -> float: return self._rotation @rotation.setter def rotation(self, rotation: float) -> None: self._rotation = rotation @property def highlight_color(self) -> Optional[str]: return self._highlight_color @highlight_color.setter def highlight_color(self, highlight_color: Optional[str]) -> None: self._highlight_color = highlight_color @property def highlight_alpha(self) -> float: return self._highlight_alpha @highlight_alpha.setter def highlight_alpha(self, highlight_alpha: float) -> None: self._highlight_alpha = highlight_alpha @property def highlight_padding(self) -> float: return self._highlight_padding @highlight_padding.setter def highlight_padding(self, highlight_padding: float) -> None: self._highlight_padding = highlight_padding @property def arrow_pointing_to(self) -> Optional[tuple[float]]: return self._arrow_pointing_to @arrow_pointing_to.setter def arrow_pointing_to(self, arrow_pointing_to: Optional[tuple[float]]) -> None: self._arrow_pointing_to = arrow_pointing_to
[docs] def copy(self) -> Self: """ Returns a deep copy of the :class:`~graphinglib.graph_elements.Text` object. """ return deepcopy(self)
[docs] def add_arrow( self, points_to: tuple[float, float], width: Optional[float] = None, shrink: Optional[float] = None, head_width: Optional[float] = None, head_length: Optional[float] = None, alpha: Optional[float] = None, ) -> None: """ Adds an arrow pointing from the :class:`~graphinglib.graph_elements.Text` to a specified point. Parameters ---------- points_to: tuple[float, float] Coordinates at which to point. width : float, optional Arrow width, in points. Typical range is ``0.5`` to ``3``. shrink : float, optional Fraction of the total length of the arrow to shrink from both ends. Range is ``0`` to ``0.5``. A value of ``0.5`` means the arrow is no longer visible. head_width : float, optional Width of the head of the arrow, in points. Typical range is ``3`` to ``10``. head_length : float, optional Length of the head of the arrow, in points. Typical range is ``3`` to ``10``. alpha : float, optional Opacity of the arrow. Range is ``0`` (transparent) to ``1`` (opaque). """ self._arrow_pointing_to = points_to self._arrow_properties = {} if width is not None: self._arrow_properties["width"] = width if shrink is not None: self._arrow_properties["shrink"] = shrink if head_width is not None: self._arrow_properties["headwidth"] = head_width if head_length is not None: self._arrow_properties["headlength"] = head_length if alpha is not None: self._arrow_properties["alpha"] = alpha
def _plot_element( self, target: plt.Axes | MPLFigure, z_order: int, **kwargs ) -> None: """ Plots the element in the specified target, which can be either an `Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_ or a `Figure <https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.html>`. """ size = self._font_size if self._font_size != "same as figure" else None params = { "color": self._color, "fontsize": size, "alpha": self._alpha, "horizontalalignment": self._h_align, "verticalalignment": self._v_align, "rotation": self._rotation, } # Add highlight/background box if highlight_color is specified if self._highlight_color is not None: bbox_dict = { "boxstyle": f"square,pad={self._highlight_padding}", "facecolor": self._highlight_color, "edgecolor": "none", "alpha": self._highlight_alpha, } params["bbox"] = bbox_dict params = {k: v for k, v in params.items() if v != INHERIT} target.text( self._x, self._y, self._text, zorder=z_order, **params, ) if self._arrow_pointing_to is not None and isinstance(target, plt.Axes): self._arrow_properties["color"] = self._color params = { "color": self._color, "fontsize": size, "horizontalalignment": self._h_align, "verticalalignment": self._v_align, } params = {k: v for k, v in params.items() if v != INHERIT} if self._color != INHERIT: self._arrow_properties["color"] = self._color params["arrowprops"] = self._arrow_properties target.annotate( self._text, self._arrow_pointing_to, xytext=(self._x, self._y), zorder=z_order, **params, )
[docs] @dataclass class Table(Plottable): """ This class allows to plot a table inside a Figure or MultiFigure. The Table object can be used to add raw data to a figure or add supplementary information like output parameters for a fit or anyother operation. Parameters ---------- cell_text : list[str] Text or data to be displayed in the table. The shape of the provided data determines the number of columns and rows. cell_colors : ArrayLike or str, optional Colors to apply to the cells' background. Must be a list of colors the same shape as the cells. Default depends on the ``figure_style`` configuration. cell_align : str Alignment of the cells' text. Must be one of the following: {'left', 'center', 'right'}. Default depends on the ``figure_style`` configuration. col_labels : list[str], optional List of labels for the rows of the table. If none are specified, no row labels are displayed. col_widths : list[float], optional Widths to apply to the columns. Must be a list the same length as the number of columns. col_align : str Alignment of the column labels' text. Must be one of the following: {'left', 'center', 'right'}. Default depends on the ``figure_style`` configuration. col_colors : ArrayLike or str, optional Colors to apply to the column labels' background. Must be a list of colors the same length as the number of columns. Default depends on the ``figure_style`` configuration. row_labels : list[str], optional List of labels for the rows of the table. If none are specified, no row labels are displayed. row_align : str Alignment of the row labels' text. Must be one of the following: {'left', 'center', 'right'}. Default depends on the ``figure_style`` configuration. row_colors : ArrayLike or str, optional Colors to apply to the row labels' background. Must be a list of colors the same length as the number of rows. Default depends on the ``figure_style`` configuration. edge_width : float or str, optional Width of the table's edges. Typical range is ``0.5`` to ``3``. Default depends on the ``figure_style`` configuration. edge_color : str, optional Color of the table's edges. Default depends on the ``figure_style`` configuration. text_color : str, optional Color of the text in the table. Default depends on the ``figure_style`` configuration. scaling : tuple[float], optional Horizontal and vertical scaling factors to apply to the table. Defaults to ``(1, 1.5)``. location : str Position of the table inside the axes. Values are ``"best"``, ``"bottom"``, ``"bottom left"``, ``"bottom right"``, ``"center"``, ``"center left"``, ``"center right"``, ``"left"``, ``"lower center"``, ``"lower left"``, ``"lower right"``, ``"right"``, ``"top"``, ``"top left"``, ``"top right"``, ``"upper center"``, ``"upper left"``, and ``"upper right"``. Defaults to ``"best"``. 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, cell_text: list[str], cell_colors: ArrayLike | str | Inherit = INHERIT, cell_align: str | Inherit = INHERIT, col_labels: Optional[list[str]] = None, col_widths: Optional[list[float]] = None, col_align: str | Inherit = INHERIT, col_colors: ArrayLike | str | Inherit = INHERIT, row_labels: Optional[list[str]] = None, row_align: str | Inherit = INHERIT, row_colors: ArrayLike | str | Inherit = INHERIT, edge_width: float | Inherit = INHERIT, edge_color: str | Inherit = INHERIT, text_color: str | Inherit = INHERIT, scaling: tuple[float, float] = (1.0, 1.5), location: str = "best", ) -> None: """ This class allows to plot a table inside a Figure or MultiFigure. The Table object can be used to add raw data to a figure or add supplementary information like output parameters for a fit or anyother operation. Parameters ---------- cell_text : list[str] Text or data to be displayed in the table. The shape of the provided data determines the number of columns and rows. cell_colors : ArrayLike or str, optional Colors to apply to the cells' background. Must be a list of colors the same shape as the cells. Default depends on the ``figure_style`` configuration. cell_align : str Alignment of the cells' text. Must be one of the following: {'left', 'center', 'right'}. Default depends on the ``figure_style`` configuration. col_labels : list[str], optional List of labels for the rows of the table. If none are specified, no row labels are displayed. col_widths : list[float], optional Widths to apply to the columns. Must be a list the same length as the number of columns. col_align : str Alignment of the column labels' text. Must be one of the following: {'left', 'center', 'right'}. Default depends on the ``figure_style`` configuration. col_colors : ArrayLike or str, optional Colors to apply to the column labels' background. Must be a list of colors the same length as the number of columns. Default depends on the ``figure_style`` configuration row_labels : list[str], optional List of labels for the rows of the table. If none are specified, no row labels are displayed. row_align : str Alignment of the row labels' text. Must be one of the following: {'left', 'center', 'right'}. Default depends on the ``figure_style`` configuration. row_colors : ArrayLike or str, optional Colors to apply to the row labels' background. Must be a list of colors the same length as the number of rows. Default depends on the ``figure_style`` configuration. edge_width : float or str, optional Width of the table's edges. Typical range is ``0.5`` to ``3``. Default depends on the ``figure_style`` configuration. edge_color : str, optional Color of the table's edges. Default depends on the ``figure_style`` configuration. text_color : str, optional Color of the text within the table. Default depends on the ``figure_style`` configuration. scaling : tuple[float], optional Horizontal and vertical scaling factors to apply to the table. Defaults to ``(1, 1.5)``. location : str Position of the table inside the axes. Values are ``"best"``, ``"bottom"``, ``"bottom left"``, ``"bottom right"``, ``"center"``, ``"center left"``, ``"center right"``, ``"left"``, ``"lower center"``, ``"lower left"``, ``"lower right"``, ``"right"``, ``"top"``, ``"top left"``, ``"top right"``, ``"upper center"``, ``"upper left"``, and ``"upper right"``. Defaults to ``"best"``. 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)``). """ self._cell_text = cell_text self._cell_colors = cell_colors self._cell_align = cell_align self._col_labels = col_labels self._col_widths = col_widths self._col_align = col_align self._col_colors = col_colors self._row_labels = row_labels self._row_align = row_align self._row_colors = row_colors self._edge_width = edge_width self._edge_color = edge_color self._text_color = text_color self._scaling = scaling self._location = location
@property def cell_text(self) -> list[str]: return self._cell_text @cell_text.setter def cell_text(self, cell_text: list[str]) -> None: self._cell_text = cell_text @property def cell_colors(self) -> ArrayLike | str: return self._cell_colors @cell_colors.setter def cell_colors(self, cell_colors: list) -> None: self._cell_colors = cell_colors @property def cell_align(self) -> str: return self._cell_align @cell_align.setter def cell_align(self, cell_align: str) -> None: self._cell_align = cell_align @property def col_labels(self) -> list[str]: return self._col_labels @col_labels.setter def col_labels(self, col_labels: list[str]) -> None: self._col_labels = col_labels @property def col_widths(self) -> list[float]: return self._col_widths @col_widths.setter def col_widths(self, col_widths: list[float]) -> None: self._col_widths = col_widths @property def col_align(self) -> str: return self._col_align @col_align.setter def col_align(self, col_align: str) -> None: self._col_align = col_align @property def col_colors(self) -> ArrayLike | str: return self._col_colors @col_colors.setter def col_colors(self, col_colors: list) -> None: self._col_colors = col_colors @property def row_labels(self) -> list[str]: return self._row_labels @row_labels.setter def row_labels(self, row_labels: list[str]) -> None: self._row_labels = row_labels @property def row_align(self) -> str: return self._row_align @row_align.setter def row_align(self, row_align: str) -> None: self._row_align = row_align @property def row_colors(self) -> ArrayLike | str: return self._row_colors @row_colors.setter def row_colors(self, row_colors: list) -> None: self._row_colors = row_colors @property def edge_width(self) -> float: return self._edge_width @edge_width.setter def edge_width(self, edge_width: float) -> None: self._edge_width = edge_width for (i, j), cell in self.handle.get_celld().items(): cell.set_linewidth(self._edge_width) @property def edge_color(self) -> str: return self._edge_color @edge_color.setter def edge_color(self, edge_color: str) -> None: self._edge_color = edge_color for (i, j), cell in self.handle.get_celld().items(): cell.set_edgecolor(self._edge_color) @property def text_color(self) -> str: return self._text_color @text_color.setter def text_color(self, text_color: str) -> None: self._text_color = text_color for (i, j), cell in self.handle.get_celld().items(): cell.set_text_props(color=self._text_color) @property def scaling(self) -> tuple[float]: return self._scaling @scaling.setter def scaling(self, scaling: tuple[float]) -> None: self._scaling = scaling @property def location(self) -> str: return self._location @location.setter def location(self, location: str) -> None: self._location = location
[docs] def copy(self) -> Self: """ Returns a deep copy of the :class:`~graphinglib.graph_elements.Table` object. """ return deepcopy(self)
def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs) -> None: """ Plots the element in the specified `Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_. """ params = { "cellLoc": self._cell_align, "colLoc": self._col_align, "rowLoc": self._row_align, } params = {k: v for k, v in params.items() if v != INHERIT} # Set colors to correct shape if they are strings if isinstance(self._cell_colors, str): self._cell_colors = [[self._cell_colors] * len(self._cell_text[0])] * len( self._cell_text ) if isinstance(self._col_colors, str): self._col_colors = [self._col_colors] * len(self._cell_text[0]) if isinstance(self._row_colors, str): self._row_colors = [self._row_colors] * len(self._cell_text) self.handle = axes.table( cellText=self._cell_text, cellColours=self._cell_colors, colLabels=self._col_labels, colWidths=self._col_widths, colColours=self._col_colors, rowLabels=self._row_labels, rowColours=self._row_colors, loc=self._location, zorder=z_order, **params, ) self.handle.auto_set_font_size(False) self.handle.scale(self._scaling[0], self._scaling[1]) for (i, j), cell in self.handle.get_celld().items(): cell.set_text_props(color=self._text_color) cell.set_edgecolor(self._edge_color) cell.set_linewidth(self._edge_width)
[docs] class PlottableAxMethod(Plottable): """ This experimental class allows to call any matplotlib Axes method as a plottable element in a :class:`~graphinglib.smart_figure.SmartFigure`. This object can be used to create plot types that have not yet been implemented in GraphingLib. This class only works with Axes methods that create plottable elements (e.g., ``bar`` or ``pcolormesh``). Methods that modify axes properties (e.g., ``set_facecolor``, ``set_title``) are not supported. Parameters ---------- meth : str Name of the matplotlib Axes method to call. The method will be called as ``axes.meth(*args, **kwargs)``. For example, this can be "pcolormesh" or "bar". .. warning:: The provided matplotlib Axes method must accept a ``zorder`` keyword argument to be compatible with this class. If not, an exception will be raised when attempting to plot the element. *args Positional arguments to pass to ``axes.meth``. label : str, optional Label to be attached to the :class:`~graphinglib.graph_elements.PlottableAxMethod`. **kwargs Keyword arguments to pass to ``axes.meth``. """
[docs] def __init__(self, meth: str, *args, label: Optional[str] = None, **kwargs) -> None: self.meth = meth self.args = args self.kwargs = kwargs self.label = label self.handle = None
def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs) -> None: """ Plots the element in the specified `Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_. """ try: attrs = getattr(axes, self.meth)(*self.args, zorder=z_order, **self.kwargs) if isinstance(attrs, list) and len(attrs) > 0: self.handle = attrs[0] except TypeError as e: if "zorder" in str(e): try: attrs = getattr(axes, self.meth)(*self.args, **self.kwargs) if isinstance(attrs, list) and len(attrs) > 0: self.handle = attrs[0] except Exception as e2: raise GraphingException( f"Failed to call Axes method '{self.meth}' with provided arguments. Please check that all " "provided arguments are valid for the given method." ) from e2 else: raise GraphingException( f"Failed to call Axes method '{self.meth}' with provided arguments. Please check that all " "provided arguments are valid for the given method." ) from e