Source code for graphinglib.data_plotting_1d

from __future__ import annotations

from copy import deepcopy
from dataclasses import dataclass
from types import NoneType
from typing import Callable, Literal, Optional, Protocol

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import Normalize, is_color_like, to_rgba, Colormap, to_rgba_array
from matplotlib.patches import Polygon
from numpy.typing import ArrayLike
from scipy.integrate import cumulative_trapezoid
from scipy.interpolate import interp1d

from .graph_elements import Point

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


class Fit(Protocol):
    """
    Dummy class to allow type hinting of Fit objects.
    """

    def _plot_element(self, axes: plt.Axes, z_order: int) -> None:
        """
        Plots the element in the specified axes.
        """
        pass

    def show_residual_curves(
        self,
        sigma_multiplier: float,
        color: str,
        line_width: float,
        line_style: str,
    ) -> None:
        pass

    def get_residuals(self) -> np.ndarray:
        pass


[docs] @dataclass class Curve: """ This class implements a general continuous curve. Parameters ---------- x_data, y_data : ArrayLike Arrays of x and y values to be plotted. label : str, optional Label to be displayed in the legend. color : str Color of the curve. Default depends on the ``figure_style`` configuration. line_width : float Width of the curve. Default depends on the ``figure_style`` configuration. line_style : str Style of the curve. Default depends on the ``figure_style`` configuration. """
[docs] def __init__( self, x_data: ArrayLike, y_data: ArrayLike, label: Optional[str] = None, color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", ) -> None: self.handle = None self._x_data = np.asarray(x_data) self._y_data = np.asarray(y_data) self._label = label self._color = color self._line_width = line_width self._line_style = line_style self._show_errorbars: bool = False self._errorbars_color = None self._errorbars_line_width = None self._cap_thickness = None self._cap_width = None self._show_error_curves: bool = False self._error_curves_fill_between: bool = False self._error_curves_color = None self._error_curves_line_style = None self._error_curves_line_width = None self._fill_between_bounds: Optional[tuple[float, float]] = None self._fill_between_other_curve: Optional[Self] = None self._fill_between_color: Optional[str] = None
[docs] @classmethod def from_function( cls, func: Callable[[ArrayLike], ArrayLike], x_min: float, x_max: float, label: Optional[str] = None, color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", number_of_points: int = 500, ) -> Self: """ Creates a :class:`~graphinglib.data_plotting_1d.Curve` from a function and a range of x values. Parameters ---------- func : Callable[[ArrayLike], ArrayLike] Function to be plotted. Works with regular functions and lambda functions. x_min, x_max : float The :class:`~graphinglib.data_plotting_1d.Curve` will be plotted between these two values. label : str, optional Label to be displayed in the legend. color : str Color of the curve. Default depends on the ``figure_style`` configuration. line_width : float Width of the curve. Default depends on the ``figure_style`` configuration. number_of_points : int Number of points to be used to plot the curve (resolution). Defaults to 500. Returns ------- A :class:`~graphinglib.data_plotting_1d.Curve` object created from the given function and x range. """ x_data = np.linspace(x_min, x_max, number_of_points) y_data = func(x_data) return cls(x_data, y_data, label, color, line_width, line_style)
@property def x_data(self) -> np.ndarray: return self._x_data @x_data.setter def x_data(self, x_data: ArrayLike) -> None: self._x_data = np.asarray(x_data) @property def y_data(self) -> np.ndarray: return self._y_data @y_data.setter def y_data(self, y_data: ArrayLike) -> None: self._y_data = np.asarray(y_data) @property def label(self) -> Optional[str]: return self._label @label.setter def label(self, label: Optional[str]) -> None: self._label = label @property def color(self) -> str: return self._color @color.setter def color(self, color: str) -> None: self._color = color @property def line_width(self) -> float | Literal["default"]: return self._line_width @line_width.setter def line_width(self, line_width: float | Literal["default"]) -> None: self._line_width = line_width @property def line_style(self) -> str: return self._line_style @line_style.setter def line_style(self, line_style: str) -> None: self._line_style = line_style @property def show_errorbars(self) -> bool: return self._show_errorbars @show_errorbars.setter def show_errorbars(self, show_errorbars: bool) -> None: self._show_errorbars = show_errorbars @property def errorbars_color(self) -> str: return self._errorbars_color @errorbars_color.setter def errorbars_color(self, errorbars_color: str) -> None: self._errorbars_color = errorbars_color @property def errorbars_line_width(self) -> float | Literal["default"]: return self._errorbars_line_width @errorbars_line_width.setter def errorbars_line_width( self, errorbars_line_width: float | Literal["default"] ) -> None: self._errorbars_line_width = errorbars_line_width @property def cap_thickness(self) -> float | Literal["default"]: return self._cap_thickness @cap_thickness.setter def cap_thickness(self, cap_thickness: float | Literal["default"]) -> None: self._cap_thickness = cap_thickness @property def cap_width(self) -> float | Literal["default"]: return self._cap_width @cap_width.setter def cap_width(self, cap_width: float | Literal["default"]) -> None: self._cap_width = cap_width @property def show_error_curves(self) -> bool: return self._show_error_curves @show_error_curves.setter def show_error_curves(self, show_error_curves: bool) -> None: self._show_error_curves = show_error_curves @property def error_curves_fill_between(self) -> bool: return self._error_curves_fill_between @error_curves_fill_between.setter def error_curves_fill_between(self, error_curves_fill_between: bool) -> None: self._error_curves_fill_between = error_curves_fill_between @property def error_curves_color(self) -> str: return self._error_curves_color @error_curves_color.setter def error_curves_color(self, error_curves_color: str) -> None: self._error_curves_color = error_curves_color @property def error_curves_line_style(self) -> str: return self._error_curves_line_style @error_curves_line_style.setter def error_curves_line_style(self, error_curves_line_style: str) -> None: self._error_curves_line_style = error_curves_line_style @property def error_curves_line_width(self) -> float | Literal["default"]: return self._error_curves_line_width @error_curves_line_width.setter def error_curves_line_width( self, error_curves_line_width: float | Literal["default"] ) -> None: self._error_curves_line_width = error_curves_line_width @property def fill_between_bounds(self) -> tuple[float, float]: return self._fill_between_bounds @fill_between_bounds.setter def fill_between_bounds(self, fill_between_bounds: tuple[float, float]) -> None: self._fill_between_bounds = fill_between_bounds @property def fill_between_other_curve(self) -> Self: return self._fill_between_other_curve @fill_between_other_curve.setter def fill_between_other_curve(self, fill_between_other_curve: Self) -> None: self._fill_between_other_curve = fill_between_other_curve @property def fill_between_color(self) -> str: return self._fill_between_color @fill_between_color.setter def fill_between_color(self, fill_between_color: str) -> None: self._fill_between_color = fill_between_color def __add__(self, other: Self | float) -> Self: """ Defines the addition of two curves or a curve and a number. """ if isinstance(other, Curve): if not np.array_equal(self._x_data, other._x_data): if len(self._x_data) > len(other._x_data): x_data = other._x_data y_data = interp1d(self._x_data, self._y_data)(x_data) return Curve(x_data, y_data + other._y_data) else: x_data = self._x_data y_data = interp1d(other._x_data, other._y_data)(x_data) return Curve(x_data, y_data + self._y_data) new_y_data = self._y_data + other._y_data return Curve(self._x_data, new_y_data) elif isinstance(other, (int, float)): new_y_data = self._y_data + other return Curve(self._x_data, new_y_data) else: raise TypeError("Can only add a curve to another curve or a number.") def __radd__(self, other: Self | float) -> Self: return self.__add__(other) def __iadd__(self, other: Self | float) -> Self: if isinstance(other, Curve): if not np.array_equal(self._x_data, other._x_data): if len(self._x_data) > len(other._x_data): x_data = other._x_data y_data = interp1d(self._x_data, self._y_data)(x_data) self._y_data = y_data + other._y_data return self else: x_data = self._x_data y_data = interp1d(other._x_data, other._y_data)(x_data) self._y_data = y_data + self._y_data return self self._y_data += other._y_data return self elif isinstance(other, (int, float)): self._y_data += other return self def __sub__(self, other: Self | float) -> Self: """ Defines the subtraction of two curves or a curve and a number. """ if isinstance(other, Curve): if not np.array_equal(self._x_data, other._x_data): if len(self._x_data) > len(other._x_data): x_data = other._x_data y_data = interp1d(self._x_data, self._y_data)(x_data) return Curve(x_data, y_data - other._y_data) else: x_data = self._x_data y_data = interp1d(other._x_data, other._y_data)(x_data) return Curve(x_data, self._y_data - y_data) new_y_data = self._y_data - other._y_data return Curve(self._x_data, new_y_data) elif isinstance(other, (int, float)): new_y_data = self._y_data - other return Curve(self._x_data, new_y_data) else: raise TypeError("Can only subtract a curve from another curve or a number.") def __rsub__(self, other: Self | float) -> Self: return (self * -1) + other def __isub__(self, other: Self | float) -> Self: if isinstance(other, Curve): if not np.array_equal(self._x_data, other._x_data): if len(self._x_data) > len(other._x_data): x_data = other._x_data y_data = interp1d(self._x_data, self._y_data)(x_data) self._y_data = y_data - other._y_data return self else: x_data = self._x_data y_data = interp1d(other._x_data, other._y_data)(x_data) self._y_data = self._y_data - y_data return self self._y_data -= other._y_data return self elif isinstance(other, (int, float)): self._y_data -= other return self def __mul__(self, other: Self | float) -> Self: """ Defines the multiplication of two curves or a curve and a number. """ if isinstance(other, Curve): if not np.array_equal(self._x_data, other._x_data): if len(self._x_data) > len(other._x_data): x_data = other._x_data y_data = interp1d(self._x_data, self._y_data)(x_data) return Curve(x_data, y_data * other._y_data) else: x_data = self._x_data y_data = interp1d(other._x_data, other._y_data)(x_data) return Curve(x_data, y_data * self._y_data) new_y_data = self._y_data * other._y_data return Curve(self._x_data, new_y_data) elif isinstance(other, (int, float)): new_y_data = self._y_data * other return Curve(self._x_data, new_y_data) else: raise TypeError("Can only multiply a curve by another curve or a number.") def __rmul__(self, other: Self | float) -> Self: return self.__mul__(other) def __imul__(self, other: Self | float) -> Self: if isinstance(other, Curve): if not np.array_equal(self._x_data, other._x_data): if len(self._x_data) > len(other._x_data): x_data = other._x_data y_data = interp1d(self._x_data, self._y_data)(x_data) self._y_data = y_data * other._y_data return self else: x_data = self._x_data y_data = interp1d(other._x_data, other._y_data)(x_data) self._y_data = self._y_data * y_data return self self._y_data *= other._y_data return self elif isinstance(other, (int, float)): self._y_data *= other return self def __truediv__(self, other: Self | float) -> Self: """ Defines the division of two curves or a curve and a number. """ if isinstance(other, Curve): if not np.array_equal(self._x_data, other._x_data): if len(self._x_data) > len(other._x_data): x_data = other._x_data y_data = interp1d(self._x_data, self._y_data)(x_data) return Curve(x_data, y_data / other._y_data) else: x_data = self._x_data y_data = interp1d(other._x_data, other._y_data)(x_data) return Curve(x_data, self._y_data / y_data) new_y_data = self._y_data / other._y_data return Curve(self._x_data, new_y_data) elif isinstance(other, (int, float)): new_y_data = self._y_data / other return Curve(self._x_data, new_y_data) else: raise TypeError("Can only divide a curve by another curve or a number.") def __rtruediv__(self, other: Self | float) -> Self: try: return (self**-1) * other except ZeroDivisionError: raise ZeroDivisionError("Cannot divide by zero.") def __itruediv__(self, other: Self | float) -> Self: if isinstance(other, Curve): if not np.array_equal(self._x_data, other._x_data): if len(self._x_data) > len(other._x_data): x_data = other._x_data y_data = interp1d(self._x_data, self._y_data)(x_data) self._y_data = y_data / other._y_data return self else: x_data = self._x_data y_data = interp1d(other._x_data, other._y_data)(x_data) self._y_data = self._y_data / y_data return self self._y_data /= other._y_data return self elif isinstance(other, (int, float)): self._y_data /= other return self def __pow__(self, other: float) -> Self: """ Defines the power of a curve to a number. """ if isinstance(other, (int, float)): new_y_data = self._y_data**other return Curve(self._x_data, new_y_data) else: raise TypeError("Can only raise a curve to another curve or a number.") def __ipow__(self, other: float) -> Self: self._y_data **= other return self def __iter__(self): """ Defines the iteration of a curve. Returns the y values. """ return iter(self._y_data) def __abs__(self) -> Self: """ Returns the absolute value of the curve. """ return Curve(self._x_data, np.abs(self._y_data))
[docs] def copy(self) -> Self: """ Returns a deep copy of the :class:`~graphinglib.data_plotting_1d.Curve`. """ return deepcopy(self)
[docs] def create_slice_x( self, x1: float, x2: float, label: Optional[str] = None, color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", copy_first: bool = False, ) -> Self: """ Creates a slice of the curve between two x values. Parameters ---------- x1, x2 : float The x values between which the slice is to be created. label : str, optional Label of the slice to be displayed in the legend. color : str Color of the slice. Default depends on the ``figure_style`` configuration. line_width : float Width of the slice. Default depends on the ``figure_style`` configuration. line_style : str Style of the slice. Default depends on the ``figure_style`` configuration. copy_first : bool If ``True``, a copy of the curve (with all its parameters) will be returned with the slicing applied. Any other parameters passed to this method will also be applied to the copied curve. If ``False``, a new curve will be created with the slicing applied and the parameters passed to this method. Returns ------- A :class:`~graphinglib.data_plotting_1d.Curve` object which is the slice of the original curve between the two x values. """ mask = (self._x_data >= x1) & (self._x_data <= x2) x_data = self._x_data[mask] y_data = self._y_data[mask] if copy_first: copy = self.copy() copy._x_data = x_data copy._y_data = y_data if label is not None: copy._label = label if color != "default": copy._color = color if line_width != "default": copy._line_width = line_width if line_style != "default": copy._line_style = line_style return copy else: return Curve(x_data, y_data, label, color, line_width, line_style)
[docs] def create_slice_y( self, y1: float, y2: float, label: Optional[str] = None, color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", copy_first: bool = False, ) -> Self: """ Creates a slice of the curve between two y values. Parameters ---------- y1, y2 : float The y values between which the slice is to be created. label : str, optional Label of the slice to be displayed in the legend. color : str Color of the slice. Default depends on the ``figure_style`` configuration. line_width : float Width of the slice. Default depends on the ``figure_style`` configuration. line_style : str Style of the slice. Default depends on the ``figure_style`` configuration. copy_first : bool If ``True``, a copy of the curve (with all its parameters) will be returned with the slicing applied. Any other parameters passed to this method will also be applied to the copied curve. If ``False``, a new curve will be created with the slicing applied and the parameters passed to this method. Returns ------- A :class:`~graphinglib.data_plotting_1d.Curve` object which is the slice of the original curve between the two y values. """ mask = (self._y_data >= y1) & (self._y_data <= y2) x_data = self._x_data[mask] y_data = self._y_data[mask] if copy_first: copy = self.copy() copy._x_data = x_data copy._y_data = y_data if label is not None: copy._label = label if color != "default": copy._color = color if line_width != "default": copy._line_width = line_width if line_style != "default": copy._line_style = line_style return copy else: return Curve(x_data, y_data, label, color, line_width, line_style)
[docs] def add_errorbars( self, x_error: Optional[ArrayLike] = None, y_error: Optional[ArrayLike] = None, cap_width: float | Literal["default"] = "default", errorbars_color: str = "default", errorbars_line_width: float | Literal["default"] = "default", cap_thickness: float | Literal["default"] = "default", ) -> None: """ Adds errorbars to the :class:`~graphinglib.data_plotting_1d.Curve`. Parameters ---------- x_error, y_error : ArrayLike, optional Arrays of x and y errors. Use one or both. cap_width : float Width of the errorbar caps. Default depends on the ``figure_style`` configuration. errorbars_color : str Color of the errorbars. Default depends on the ``figure_style`` configuration. errorbars_line_width : float Width of the errorbars. Default depends on the ``figure_style`` configuration. cap_thickness : float Thickness of the errorbar caps. Default depends on the ``figure_style`` configuration. """ self._show_errorbars = True self._x_error = np.array(x_error) if x_error is not None else x_error self._y_error = np.array(y_error) if y_error is not None else y_error self._errorbars_color = errorbars_color self._errorbars_line_width = errorbars_line_width self._cap_thickness = cap_thickness self._cap_width = cap_width
[docs] def add_error_curves( self, y_error: Optional[ArrayLike] = None, error_curves_color: str = "default", error_curves_line_style: str = "default", error_curves_line_width: float | Literal["default"] = "default", error_curves_fill_between: bool | Literal["default"] = "default", ) -> None: """ Adds error curves to the :class:`~graphinglib.data_plotting_1d.Curve`. Parameters ---------- y_error : ArrayLike, optional Array of y errors. error_curves_color : str Color of the error curves. Default depends on the ``figure_style`` configuration. error_curves_line_style : str Line style of the error curves. Default depends on the ``figure_style`` configuration. error_curves_line_width : float Line width of the error curves. Default depends on the ``figure_style`` configuration. error_curves_fill_between : bool Whether or not to fill the area between the two error curves. Default depends on the ``figure_style`` configuration. """ self._show_error_curves = True self._y_error = np.array(y_error) if y_error is not None else y_error self._error_curves_color = error_curves_color self._error_curves_line_style = error_curves_line_style self._error_curves_line_width = error_curves_line_width self._error_curves_fill_between = error_curves_fill_between
[docs] def get_coordinates_at_x( self, x: float, interpolation_method: str = "linear", ) -> tuple[float, float]: """ Gets the coordinates of the curve at a given x value. Parameters ---------- x : float The x value of the desired coordinates. interpolation_method : str, The type of interpolation to be used, as defined in ``scipy.interpolate.interp1d``. .. seealso:: `scipy.interpolate.interp1d <https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html>`_ Defaults to "linear". Returns ------- tuple[float, float] The coordinates of the curve at the given x value. """ return ( x, float(interp1d(self._x_data, self._y_data, kind=interpolation_method)(x)), )
[docs] def create_point_at_x( self, x: float, interpolation_method: str = "linear", label: Optional[str] = None, color: str = "default", edge_color: str = "default", marker_size: float | Literal["default"] = "default", marker_style: str = "default", line_width: float | Literal["default"] = "default", ) -> Point: """ Creates a point on the curve at a given x value. Parameters ---------- x : float The x value of the point. interpolation_method : str, The type of interpolation to be used, as defined in ``scipy.interpolate.interp1d``. .. seealso:: `scipy.interpolate.interp1d <https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html>`_ Defaults to "linear". label : str, optional Point's label to be displayed in the legend. color : str Face color of the point. Default depends on the ``figure_style`` configuration. edge_color : str Edge color of the point. Default depends on the ``figure_style`` configuration. marker_size : float Size of the point. Default depends on the ``figure_style`` configuration. marker_style : str Style of the point. Default depends on the ``figure_style`` configuration. line_width : float Width of the point edge. Default depends on the ``figure_style`` configuration. Returns ------- :class:`~graphinglib.graph_elements.Point` The point on the curve at the given x value. """ point = Point( x, self.get_coordinates_at_x(x, interpolation_method)[1], label=label, color=color, edge_color=edge_color, marker_size=marker_size, marker_style=marker_style, edge_width=line_width, ) return point
[docs] def get_coordinates_at_y( self, y: float, interpolation_method: str = "linear", ) -> list[tuple[float, float]]: """ Gets the coordinates of the curve at a given y value. Can return multiple coordinate pairs if the curve crosses the y value multiple times. Parameters ---------- y : float The y value of the desired coordinates. interpolation_method : str, The type of interpolation to be used, as defined in ``scipy.interpolate.interp1d``. .. seealso:: `scipy.interpolate.interp1d <https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html>`_ Defaults to "linear". Returns ------- list[tuple[float, float]] The coordinates of the points on the curve at the given y value. """ xs = self._x_data ys = self._y_data crossings = np.where(np.diff(np.sign(ys - y)))[0] x_vals: list[float] = [] for cross in crossings: x1, x2 = xs[cross], xs[cross + 1] y1, y2 = ys[cross], ys[cross + 1] f = interp1d([y1, y2], [x1, x2], kind=interpolation_method) x_val = f(y) x_vals.append(float(x_val)) points = [(x_val, y) for x_val in x_vals] return points
[docs] def create_points_at_y( self, y: float, interpolation_method: str = "linear", label: str | None = None, color: str = "default", edge_color: str = "default", marker_size: float | Literal["default"] = "default", marker_style: str = "default", line_width: float | Literal["default"] = "default", ) -> list[Point]: """ Gets the points on the curve at a given y value. Can return multiple Point objects if the curve crosses the y value multiple times. Parameters ---------- y : float The y value of the desired points. interpolation_method : str The type of interpolation to be used, as defined in ``scipy.interpolate.interp1d``. .. seealso:: `scipy.interpolate.interp1d <https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html>`_ Defaults to "linear". label : str, optional Point label to be displayed in the legend. color : str Face color of the point. Default depends on the ``figure_style`` configuration. edge_color : str Edge color of the point. Default depends on the ``figure_style`` configuration. marker_size : float Size of the point. Default depends on the ``figure_style`` configuration. marker_style : str Style of the point. Default depends on the ``figure_style`` configuration. line_width : float Width of the point edge. Default depends on the ``figure_style`` configuration. Returns ------- list[:class:`~graphinglib.graph_elements.Point`] The Point objects on the curve at the given y value. """ pairs = self.get_coordinates_at_y(y, interpolation_method) points = [ Point( pair[0], pair[1], label=label, color=color, edge_color=edge_color, marker_size=marker_size, marker_style=marker_style, edge_width=line_width, ) for pair in pairs ] return points
[docs] def create_derivative_curve( self, label: Optional[str] = None, color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", copy_first: bool = False, ) -> Self: """ Creates a new curve which is the derivative of the original curve. Parameters ---------- label : str, optional Label of the new curve to be displayed in the legend. color : str Color of the new curve. Default depends on the ``figure_style`` configuration. line_width : float Width of the new curve. Default depends on the ``figure_style`` configuration. line_style : str Style of the new curve. Default depends on the ``figure_style`` configuration. copy_first : bool If ``True``, a copy of the curve (with all its parameters) will be returned with the derivative applied. Any other parameters passed to this method will also be applied to the copied curve. If ``False``, a new curve will be created with the derivative applied and the parameters passed to this method. Returns ------- A :class:`~graphinglib.data_plotting_1d.Curve` object which is the derivative of the original curve. """ x_data = self._x_data y_data = np.gradient(self._y_data, x_data) if copy_first: copy = self.copy() copy._y_data = y_data if label is not None: copy._label = label if color != "default": copy._color = color if line_width != "default": copy._line_width = line_width if line_style != "default": copy._line_style = line_style return copy else: return Curve(x_data, y_data, label, color, line_width, line_style)
[docs] def create_integral_curve( self, initial_value: float = 0, label: Optional[str] = None, color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", copy_first: bool = False, ) -> Self: """ Creates a new curve which is the integral of the original curve. Parameters ---------- initial_value : float, optional The value of the integral at the first x value (initial condition). Defaults to 0. label : str, optional Label of the new curve to be displayed in the legend. color : str Color of the new curve. Default depends on the ``figure_style`` configuration. line_width : float Width of the new curve. Default depends on the ``figure_style`` configuration. line_style : str Style of the new curve. Default depends on the ``figure_style`` configuration. copy_first : bool If ``True``, a copy of the curve (with all its parameters) will be returned with the integral applied. Any other parameters passed to this method will also be applied to the copied curve. If ``False``, a new curve will be created with the integral applied and the parameters passed to this method. Returns ------- A :class:`~graphinglib.data_plotting_1d.Curve` object which is the integral of the original curve. """ # calculate the integral curve using cumulative trapezoidal integration y_data = ( cumulative_trapezoid(self._y_data, self._x_data, initial=0) + initial_value ) if copy_first: copy = self.copy() copy._y_data = y_data if label is not None: copy._label = label if color != "default": copy._color = color if line_width != "default": copy._line_width = line_width if line_style != "default": copy._line_style = line_style return copy else: return Curve(self._x_data, y_data, label, color, line_width, line_style)
[docs] def create_tangent_curve( self, x: float, label: Optional[str] = None, color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", copy_first: bool = False, ) -> Self: """ Creates a new curve which is the tangent to the original curve at a given x value. Parameters ---------- x : float The x value at which the tangent is to be calculated. label : str, optional Label of the new curve to be displayed in the legend. color : str Color of the new curve. Default depends on the ``figure_style`` configuration. line_width : float Width of the new curve. Default depends on the ``figure_style`` configuration. line_style : str Style of the new curve. Default depends on the ``figure_style`` configuration. copy_first : bool If ``True``, a copy of the curve (with all its parameters) will be returned with the tangent applied. Any other parameters passed to this method will also be applied to the copied curve. If ``False``, a new curve will be created with the tangent applied and the parameters passed to this method. Returns ------- :class:`~graphinglib.data_plotting_1d.Curve` A :class:`~graphinglib.data_plotting_1d.Curve` object which is the tangent to the original curve at a given x value. """ point = self.get_coordinates_at_x(x) gradient = self.create_derivative_curve().get_coordinates_at_x(x)[1] y_data = gradient * (self._x_data - x) + point[1] if copy_first: copy = self.copy() copy._y_data = y_data if label is not None: copy._label = label if color != "default": copy._color = color if line_width != "default": copy._line_width = line_width if line_style != "default": copy._line_style = line_style return copy else: tangent_curve = Curve( self._x_data, y_data, label, color, line_width, line_style ) return tangent_curve
[docs] def create_normal_curve( self, x: float, label: Optional[str] = None, color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", copy_first: bool = False, ) -> Self: """ Creates a new curve which is the normal to the original curve at a given x value. Parameters ---------- x : float The x value at which the normal is to be calculated. label : str, optional Label of the new curve to be displayed in the legend. color : str Color of the new curve. Default depends on the ``figure_style`` configuration. line_width : float Width of the new curve. Default depends on the ``figure_style`` configuration. line_style : str Style of the new curve. Default depends on the ``figure_style`` configuration. copy_first : bool If ``True``, a copy of the curve (with all its parameters) will be returned with the normal applied. Any other parameters passed to this method will also be applied to the copied curve. If ``False``, a new curve will be created with the normal applied and the parameters passed to this method. Returns ------- :class:`~graphinglib.data_plotting_1d.Curve` A :class:`~graphinglib.data_plotting_1d.Curve` object which is the normal to the original curve at a given x value. """ point = self.get_coordinates_at_x(x) gradient = self.create_derivative_curve().get_coordinates_at_x(x)[1] y_data = -1 / gradient * (self._x_data - x) + point[1] if copy_first: copy = self.copy() copy._y_data = y_data if label is not None: copy._label = label if color != "default": copy._color = color if line_width != "default": copy._line_width = line_width if line_style != "default": copy._line_style = line_style return copy else: normal_curve = Curve( self._x_data, y_data, label, color, line_width, line_style ) return normal_curve
[docs] def get_slope_at(self, x: float) -> float: """ Calculates the slope of the curve at a given x value. Parameters ---------- x : float The x value at which the slope is to be calculated. Returns ------- The slope of the curve (float) at the given x value. """ return self.create_derivative_curve().get_coordinates_at_x(x)[1]
[docs] def get_arc_length_between(self, x1: float, x2: float) -> float: """ Calculates the arc length of the curve between two x values. Parameters ---------- x1, x2 : float The x values between which the arc length is to be calculated. Returns ------- The arc length of the curve (float) between the two given x values. """ y_data = self._y_data x_data = self._x_data # f = interp1d(x_data, y_data) # x = np.linspace(x1, x2, 1000) # y = f(x) x = x_data[(x_data >= x1) & (x_data <= x2)] y = y_data[(x_data >= x1) & (x_data <= x2)] return np.trapz(np.sqrt(1 + np.gradient(y, x) ** 2), x)
[docs] def get_area_between( self, x1: float, x2: float, fill_between: bool = False, fill_color: str = "default", other_curve: Optional[Self] = None, ) -> float: """ Calculates the area between the curve and the x axis between two x values. This is the definite integral of the curve between the two x values. Parameters ---------- x1, x2 : float The x values between which the area is to be calculated. fill_between : bool Whether to fill the specified area between the curve and the x axis when displaying. Defaults to ``False``. fill_color : str Color of the area between the curve and the x axis when ``fill_between`` is set to ``True``. Default depends on the ``figure_style`` configuration. other_curve : :class:`~graphinglib.data_plotting_1d.Curve`, optional If specified, the area between the two curves will be calculated instead of the area between the curve and the x axis. Returns ------- The area (float) between the curve and the x axis (or between the two curves) between the two given x values. """ if other_curve is None: if fill_between: self._fill_between_bounds = (x1, x2) self._fill_between_color = fill_color y_data = self._y_data x_data = self._x_data mask = (x_data >= x1) & (x_data <= x2) y = y_data[mask] x = x_data[mask] return np.trapz(y, x) else: if fill_between: self._fill_between_bounds = (x1, x2) self._fill_between_color = fill_color self._fill_between_other_curve = other_curve if np.array_equal(self._x_data, other_curve._x_data): # No need to interpolate mask = (self._x_data >= x1) & (self._x_data <= x2) common_x = self._x_data[mask] y1 = self._y_data[mask] y2 = other_curve._y_data[mask] else: # Interpolate to get common x values density_x1 = len(self._x_data) / (self._x_data[-1] - self._x_data[0]) density_x2 = len(other_curve._x_data) / ( other_curve._x_data[-1] - other_curve._x_data[0] ) higher_density = max(density_x1, density_x2) num_of_values = int(np.ceil(higher_density * (x2 - x1))) common_x = np.linspace(x1, x2, num_of_values) y1 = np.interp(common_x, self._x_data, self._y_data) y2 = np.interp(common_x, other_curve._x_data, other_curve._y_data) difference = y1 - y2 area = np.trapz(difference, common_x) return area
[docs] def get_intersection_coordinates( self, other: Self, ) -> list[tuple[float, float]]: """ Calculates the coordinates of the intersection points between two curves. Parameters ---------- other : :class:`~graphinglib.data_plotting_1d.Curve` The other curve to calculate the intersections with. Returns ------- list[tuple[float, float]] A list of tuples of coordinates which are the intersection points between the two curves. """ y = self._y_data - other._y_data s = np.abs(np.diff(np.sign(y))).astype(bool) intersections_x = self._x_data[:-1][s] + np.diff(self._x_data)[s] / ( np.abs(y[1:][s] / y[:-1][s]) + 1 ) intersections_y = np.interp(intersections_x, self._x_data, self._y_data) points = [] for i in range(len(intersections_x)): x_val = intersections_x[i] y_val = intersections_y[i] points.append((x_val, y_val)) return points
[docs] def create_intersection_points( self, other: Self, labels: Optional[list[str] | str] = None, colors: list[str] | str = "default", edge_colors: list[str] | str = "default", marker_sizes: list[float] | float | Literal["default"] = "default", marker_styles: list[str] | str = "default", edge_widths: list[float] | float | Literal["default"] = "default", ) -> list[Point]: """ Creates the intersection Points between two curves. Parameters ---------- other : :class:`~graphinglib.data_plotting_1d.Curve` The other curve to calculate the intersections with. as_point_objects : bool Whether to return a list of :class:`~graphinglib.graph_elements.Point` objects (True) or a list of tuples of coordinates (False). Defaults to False. labels : list[str] or str, optional Labels of the intersection points to be displayed in the legend. If a single string is passed, all intersection points will have the same label. colors : list[str] or str Face colors of the intersection points. If a single string is passed, all intersection points will have the same color. Default depends on the ``figure_style`` configuration. edge_colors : list[str] or str Edge colors of the intersection points. If a single string is passed, all intersection points will have the same color. Default depends on the ``figure_style`` configuration. marker_sizes : list[float] or float Sizes of the intersection points. If a single float is passed, all intersection points will have the same size. Default depends on the ``figure_style`` configuration. marker_styles : list[str] or str Styles of the intersection points. If a single string is passed, all intersection points will have the same style. Default depends on the ``figure_style`` configuration. edge_widths : list[float] or float Widths of the intersection points. If a single float is passed, all intersection points will have the same width. Default depends on the ``figure_style`` configuration. Returns ------- list[:class:`~graphinglib.graph_elements.Point`] or list[tuple[float, float]] A list of :class:`~graphinglib.graph_elements.Point` objects which are the intersection points between the two curves. """ y = self._y_data - other._y_data s = np.abs(np.diff(np.sign(y))).astype(bool) intersections_x = self._x_data[:-1][s] + np.diff(self._x_data)[s] / ( np.abs(y[1:][s] / y[:-1][s]) + 1 ) point_coords = self.get_intersection_coordinates(other) point_objects = [] for i in range(len(intersections_x)): try: assert isinstance(labels, list) label = labels[i] except (IndexError, TypeError, AssertionError): label = labels try: assert isinstance(colors, list) color = colors[i] except (IndexError, TypeError, AssertionError): color = colors try: assert isinstance(edge_colors, list) edge_color = edge_colors[i] except (IndexError, TypeError, AssertionError): edge_color = edge_colors try: assert isinstance(marker_sizes, list) marker_size = marker_sizes[i] except (IndexError, TypeError, AssertionError): marker_size = marker_sizes try: assert isinstance(marker_styles, list) marker_style = marker_styles[i] except (IndexError, TypeError, AssertionError): marker_style = marker_styles try: assert isinstance(edge_widths, list) edge_width = edge_widths[i] except (IndexError, TypeError, AssertionError): edge_width = edge_widths point = point_coords[i] point_objects.append( Point( point[0], point[1], label=label, color=color, edge_color=edge_color, marker_size=marker_size, marker_style=marker_style, edge_width=edge_width, ) ) return point_objects
def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs) -> None: """ Plots the element in the specified axes. """ if self._show_errorbars: params = { "color": self._color, "linewidth": self._line_width, "linestyle": self._line_style, "elinewidth": ( self._errorbars_line_width if self._errorbars_line_width != "same as curve" else self._line_width ), "capsize": self._cap_width, "capthick": ( self._cap_thickness if self._cap_thickness != "same as curve" else self._line_width ), "ecolor": ( self._errorbars_color if self._errorbars_color != "same as curve" else self._color ), } params = {k: v for k, v in params.items() if v != "default"} self.handle = axes.errorbar( self._x_data, self._y_data, xerr=self._x_error, yerr=self._y_error, label=self._label, zorder=z_order, **params, ) else: params = { "color": self._color, "linewidth": self._line_width, "linestyle": self._line_style, } params = {k: v for k, v in params.items() if v != "default"} self.handle = axes.errorbar( self._x_data, self._y_data, label=self._label, zorder=z_order, **params, ) if self._show_error_curves: max_y = ( self._y_data + self._y_error if self._y_error is not None else self._y_data ) min_y = ( self._y_data - self._y_error if self._y_error is not None else self._y_data ) params = { "color": ( self._error_curves_color if self._error_curves_color != "same as curve" else self.handle[0].get_color() ), "linestyle": ( self._error_curves_line_style if self._error_curves_line_style != "same as curve" else self._line_style ), "linewidth": ( self._error_curves_line_width if self._error_curves_line_width != "same as curve" else self._line_width ), } params = {k: v for k, v in params.items() if v != "default"} axes.plot( self._x_data, min_y, **params, ) axes.plot( self._x_data, max_y, **params, ) if self._error_curves_fill_between: axes.fill_between( self._x_data, max_y, min_y, color=self.handle[0].get_color(), alpha=0.2, ) if self._fill_between_bounds: params = {"alpha": 0.2} params["color"] = ( self._fill_between_color if self._fill_between_color != "same as curve" else self.handle[0].get_color() ) params = {k: v for k, v in params.items() if v != "default"} if self._fill_between_other_curve: self_y_data = self._y_data self_x_data = self._x_data other_y_data = self._fill_between_other_curve._y_data other_x_data = self._fill_between_other_curve._x_data x_data = np.linspace( self._fill_between_bounds[0], self._fill_between_bounds[1], max(len(self_x_data), len(other_x_data)), ) self_y_data = interp1d(self_x_data, self_y_data)(x_data) other_y_data = interp1d(other_x_data, other_y_data)(x_data) params["x"] = x_data params["y1"] = self_y_data params["y2"] = other_y_data where_x_data = x_data else: params["x"] = self._x_data params["y1"] = self._y_data where_x_data = self._x_data axes.fill_between( where=np.logical_and( where_x_data >= self._fill_between_bounds[0], where_x_data <= self._fill_between_bounds[1], ), zorder=z_order - 2, **params, )
[docs] @dataclass class Scatter: """ This class implements a general scatter plot. Parameters ---------- x_data, y_data : ArrayLike Arrays of x and y values to be plotted. label : str, optional Label to be displayed in the legend. face_color : str or ArrayLike or None Face color of the points. If an array of intensities is provided, the values are mapped to the specified color map. If None, marker faces are transparent. Default depends on the ``figure_style`` configuration. edge_color : str or ArrayLike or None Edge color of the points. If an array of intensities is provided, the values are mapped to the specified color map. If None, marker edges are transparent. Default depends on the ``figure_style`` configuration. color_map : str or Colormap Color map of the stream lines, to be used in combination with the color parameter to specify intensity. Default depends on the ``figure_style`` configuration. show_color_bar : bool Whether or not to display the color bar next to the plot. Default depends on the ``figure_style`` configuration. marker_size : float Size of the points. Default depends on the ``figure_style`` configuration. marker_style : str Style of the points. Default depends on the ``figure_style`` configuration. """
[docs] def __init__( self, x_data: ArrayLike, y_data: ArrayLike, label: Optional[str] = None, face_color: str | ArrayLike | NoneType = "default", edge_color: str | ArrayLike | NoneType = "default", color_map: str | Colormap = "default", show_color_bar: bool | Literal["default"] = "default", marker_size: float | Literal["default"] = "default", marker_style: str = "default", ) -> None: """ This class implements a general scatter plot. Parameters ---------- x_data, y_data : ArrayLike Arrays of x and y values to be plotted. label : str, optional Label to be displayed in the legend. face_color : str or ArrayLike or None Face color of the points. If an array of intensities is provided, the values are mapped to the specified color map. If None, marker faces are transparent. Default depends on the ``figure_style`` configuration. edge_color : str or ArrayLike or None Edge color of the points. If an array of intensities is provided, the values are mapped to the specified color map. If None, marker edges are transparent. Default depends on the ``figure_style`` configuration. color_map : str or Colormap Color map of the stream lines, to be used in combination with the color parameter to specify intensity. Default depends on the ``figure_style`` configuration. show_color_bar : bool Whether or not to display the color bar next to the plot. Default depends on the ``figure_style`` configuration. marker_size : float Size of the points. Default depends on the ``figure_style`` configuration. marker_style : str Style of the points. Default depends on the ``figure_style`` configuration. """ self.handle = None self.errorbars_handle = None self._x_data = np.asarray(x_data) self._y_data = np.asarray(y_data) self._label = label self._face_color = face_color self._edge_color = edge_color self._color_map = color_map self._show_color_bar = show_color_bar self._marker_size = marker_size self._marker_style = marker_style self._show_errorbars: bool = False self._errorbars_line_width: float = 1.0 self._cap_width: float = 3.0 self._cap_thickness: float = 1.0 self._errorbars_color: Optional[str] = None
[docs] @classmethod def from_function( cls, func: Callable[[ArrayLike], ArrayLike], x_min: float, x_max: float, label: Optional[str] = None, face_color: str | ArrayLike | NoneType = "default", edge_color: str | ArrayLike | NoneType = "default", color_map: str | Colormap = "default", show_color_bar: bool | Literal["default"] = "default", marker_size: int | Literal["default"] = "default", marker_style: str = "default", number_of_points: int = 30, ) -> Self: """ Creates a scatter plot from a function and a range of x values. Parameters ---------- func : Callable[[ArrayLike], ArrayLike] The function to be plotted. x_min, x_max : float The scatter plot will be created for x values between x_min and x_max. label : str, optional Label to be displayed in the legend. face_color : str or ArrayLike or None Face color of the points. If an array of intensities is provided, the values are mapped to the specified color map. If None, marker faces are transparent. edge_color : str or ArrayLike or None Edge color of the points. If an array of intensities is provided, the values are mapped to the specified color map. If None, marker edges are transparent. Default depends on the ``figure_style`` configuration. color_map : str or Colormap Color map of the stream lines, to be used in combination with the color parameter to specify intensity. Default depends on the ``figure_style`` configuration. show_color_bar : bool Whether or not to display the color bar next to the plot. Default depends on the ``figure_style`` configuration. marker_size : int Size of the points. Default depends on the ``figure_style`` configuration. marker_style : str Style of the points. Default depends on the ``figure_style`` configuration. number_of_points : int Number of points to be plotted. Defaults to 30. Returns ------- A :class:`~graphinglib.data_plotting_1d.Scatter` object created from a function and a range of x values. """ x_data = np.linspace(x_min, x_max, number_of_points) y_data = func(x_data) return cls( x_data, y_data, label, face_color, edge_color, color_map, show_color_bar, marker_size, marker_style, )
@property def x_data(self) -> np.ndarray: return self._x_data @x_data.setter def x_data(self, x_data: ArrayLike) -> None: self._x_data = np.asarray(x_data) @property def y_data(self) -> np.ndarray: return self._y_data @y_data.setter def y_data(self, y_data: ArrayLike) -> None: self._y_data = np.asarray(y_data) @property def label(self) -> str | None: return self._label @label.setter def label(self, label: str | None) -> None: self._label = label @property def face_color(self) -> str | ArrayLike: return self._face_color @face_color.setter def face_color(self, face_color: str | ArrayLike) -> None: self._face_color = face_color @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 @property def color_map(self) -> str | Colormap: return self._color_map @color_map.setter def color_map(self, color_map: str | Colormap) -> None: self._color_map = color_map @property def show_color_bar(self) -> bool: return self._show_color_bar @show_color_bar.setter def show_color_bar(self, show_color_bar: bool) -> None: self._show_color_bar = show_color_bar @property def marker_size(self) -> float | Literal["default"]: return self._marker_size @marker_size.setter def marker_size(self, marker_size: float | Literal["default"]) -> 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 show_errorbars(self) -> bool: return self._show_errorbars @show_errorbars.setter def show_errorbars(self, show_errorbars: bool) -> None: self._show_errorbars = show_errorbars @property def errorbars_line_width(self) -> float: return self._errorbars_line_width @errorbars_line_width.setter def errorbars_line_width(self, errorbars_line_width: float) -> None: self._errorbars_line_width = errorbars_line_width @property def cap_width(self) -> float: return self._cap_width @cap_width.setter def cap_width(self, cap_width: float) -> None: self._cap_width = cap_width @property def cap_thickness(self) -> float: return self._cap_thickness @cap_thickness.setter def cap_thickness(self, cap_thickness: float) -> None: self._cap_thickness = cap_thickness @property def errorbars_color(self) -> str: return self._errorbars_color @errorbars_color.setter def errorbars_color(self, errorbars_color: str) -> None: self._errorbars_color = errorbars_color def __add__(self, other: Self | float) -> Self: """ Defines the addition of two scatter plots or a scatter plot and a number. """ if isinstance(other, Scatter): try: assert np.array_equal(self._x_data, other._x_data) except AssertionError: raise ValueError( "Cannot add two scatter plots with different x values." ) new_y_data = self._y_data + other._y_data return Scatter(self._x_data, new_y_data) elif isinstance(other, (int, float)): new_y_data = self._y_data + other return Scatter(self._x_data, new_y_data) else: raise TypeError( "Can only add a scatter plot to another scatter plot or a number." ) def __radd__(self, other: Self | float) -> Self: """ Defines the reverse addition of a scatter plot and a number. """ return self.__add__(other) def __iadd__(self, other: Self | float) -> Self: if isinstance(other, Scatter): try: assert np.array_equal(self._x_data, other._x_data) except AssertionError: raise ValueError( "Cannot add two scatter plots with different x values." ) self._y_data += other._y_data return self elif isinstance(other, (int, float)): self._y_data += other return self def __sub__(self, other: Self | float) -> Self: """ Defines the subtraction of two scatter plots or a scatter plot and a number. """ if isinstance(other, Scatter): try: assert np.array_equal(self._x_data, other._x_data) except AssertionError: raise ValueError( "Cannot subtract two scatter plots with different x values." ) new_y_data = self._y_data - other._y_data return Scatter(self._x_data, new_y_data) elif isinstance(other, (int, float)): new_y_data = self._y_data - other return Scatter(self._x_data, new_y_data) else: raise TypeError( "Can only subtract a scatter plot from another scatter plot or a number." ) def __rsub__(self, other: Self | float) -> Self: """ Defines the reverse subtraction of a scatter plot and a number. """ return (self * -1) + other def __isub__(self, other: Self | float) -> Self: if isinstance(other, Scatter): try: assert np.array_equal(self._x_data, other._x_data) except AssertionError: raise ValueError( "Cannot subtract two scatter plots with different x values." ) self._y_data -= other._y_data return self elif isinstance(other, (int, float)): self._y_data -= other return self def __mul__(self, other: Self | float) -> Self: """ Defines the multiplication of two scatter plots or a scatter plot and a number. """ if isinstance(other, Scatter): try: assert np.array_equal(self._x_data, other._x_data) except AssertionError: raise ValueError( "Cannot multiply two scatter plots with different x values." ) new_y_data = self._y_data * other._y_data return Scatter(self._x_data, new_y_data) elif isinstance(other, (int, float)): new_y_data = self._y_data * other return Scatter(self._x_data, new_y_data) else: raise TypeError( "Can only multiply a scatter plot by another scatter plot or a number." ) def __rmul__(self, other: Self | float) -> Self: """ Defines the reverse multiplication of a scatter plot and a number. """ return self.__mul__(other) def __imul__(self, other: Self | float) -> Self: if isinstance(other, Scatter): try: assert np.array_equal(self._x_data, other._x_data) except AssertionError: raise ValueError( "Cannot multiply two scatter plots with different x values." ) self._y_data *= other._y_data return self elif isinstance(other, (int, float)): self._y_data *= other return self def __truediv__(self, other: Self | float) -> Self: """ Defines the division of two scatter plots or a scatter plot and a number. """ if isinstance(other, Scatter): try: assert np.array_equal(self._x_data, other._x_data) except AssertionError: raise ValueError( "Cannot divide two scatter plots with different x values." ) new_y_data = self._y_data / other._y_data return Scatter(self._x_data, new_y_data) elif isinstance(other, (int, float)): new_y_data = self._y_data / other return Scatter(self._x_data, new_y_data) else: raise TypeError( "Can only divide a scatter plot by another scatter plot or a number." ) def __rtruediv__(self, other: Self | float) -> Self: """ Defines the division of two scatter plots or a scatter plot and a number. """ try: return (self**-1) * other except ZeroDivisionError: raise ZeroDivisionError("Cannot divide by zero.") def __itruediv__(self, other: Self | float) -> Self: if isinstance(other, Scatter): try: assert np.array_equal(self._x_data, other._x_data) except AssertionError: raise ValueError( "Cannot divide two scatter plots with different x values." ) self._y_data /= other._y_data return self elif isinstance(other, (int, float)): self._y_data /= other return self def __pow__(self, other: float) -> Self: """ Defines the power of a scatter plot to a number. """ if isinstance(other, (int, float)): new_y_data = self._y_data**other return Scatter(self._x_data, new_y_data) else: raise TypeError( "Can only raise a scatter plot to another scatter plot or a number." ) def __ipow__(self, other: float) -> Self: self._y_data **= other return self def __iter__(self): """ Defines the iteration of a scatter plot. Returns the y values. """ return iter(self._y_data) def __abs__(self) -> Self: """ Defines the absolute value of a scatter plot. """ new_y_data = np.abs(self._y_data) return Scatter(self._x_data, new_y_data)
[docs] def copy(self) -> Self: """ Returns a deep copy of the :class:`~graphinglib.data_plotting_1d.Scatter` object. """ return deepcopy(self)
[docs] def create_slice_x( self, x_min: float, x_max: float, label: Optional[str] = None, face_color: str | ArrayLike | NoneType = "default", edge_color: str | ArrayLike | NoneType = "default", color_map: str | Colormap = "default", show_color_bar: bool | Literal["default"] = "default", marker_size: float | Literal["default"] = "default", marker_style: str = "default", copy_first: bool = False, ) -> Self: """ Creates a slice of the scatter plot between two x values. Parameters ---------- x_min, x_max : float The slice will be created between x_min and x_max. label : str, optional Label to be displayed in the legend. face_color : str or ArrayLike or None Face color of the points. If an array of intensities is provided, the values are mapped to the specified color map. If None, marker faces are transparent. Default depends on the ``figure_style`` configuration. edge_color : str or ArrayLike or None Edge color of the points. If an array of intensities is provided, the values are mapped to the specified color map. If None, marker edges are transparent. Default depends on the ``figure_style`` configuration. color_map : str or Colormap Color map of the stream lines, to be used in combination with the color parameter to specify intensity. Default depends on the ``figure_style`` configuration. show_color_bar : bool Whether or not to display the color bar next to the plot. Default depends on the ``figure_style`` configuration. marker_size : float Size of the points. Default depends on the ``figure_style`` configuration. marker_style : str Style of the points. Default depends on the ``figure_style`` configuration. copy_first : bool If ``True``, a copy of the scatter plot (with all its parameters) will be returned with the slice applied. Any other parameters passed to this method will also be applied to the copied scatter plot. If ``False``, a new scatter plot will be created with the slice applied and the parameters passed to this method. Returns ------- :class:`~graphinglib.data_plotting_1d.Scatter` A new :class:`~graphinglib.data_plotting_1d.Scatter` object which is a slice of the original scatter plot. """ mask = (self._x_data >= x_min) & (self._x_data <= x_max) if copy_first: copy = self.copy() copy._x_data = self._x_data[mask] copy._y_data = self._y_data[mask] if label is not None: copy._label = label if face_color != "default": copy._face_color = face_color if edge_color != "default": copy._edge_color = edge_color if color_map != "default": copy._color_map = color_map if show_color_bar != "default": copy._show_color_bar = show_color_bar if marker_size != "default": copy._marker_size = marker_size if marker_style != "default": copy._marker_style = marker_style return copy else: return Scatter( self._x_data[mask], self._y_data[mask], label, face_color, edge_color, color_map, show_color_bar, marker_size, marker_style, )
[docs] def create_slice_y( self, y_min: float, y_max: float, label: Optional[str] = None, face_color: str | ArrayLike | NoneType = "default", edge_color: str | ArrayLike | NoneType = "default", color_map: str | Colormap | Literal["default"] = "default", show_color_bar: bool | Literal["default"] = "default", marker_size: float | Literal["default"] = "default", marker_style: str = "default", copy_first: bool = False, ) -> Self: """ Creates a slice of the scatter plot between two y values. Parameters ---------- y_min, y_max : float The slice will be created between y_min and y_max. label : str, optional Label to be displayed in the legend. face_color : str or ArrayLike or None Face color of the points. If an array of intensities is provided, the values are mapped to the specified color map. If None, marker faces are transparent. Default depends on the ``figure_style`` configuration. edge_color : str or ArrayLike or None Edge color of the points. If an array of intensities is provided, the values are mapped to the specified color map. If None, marker edges are transparent. Default depends on the ``figure_style`` configuration. color_map : str or Colormap Color map of the stream lines, to be used in combination with the color parameter to specify intensity. Default depends on the ``figure_style`` configuration. show_color_bar : bool Whether or not to display the color bar next to the plot. Default depends on the ``figure_style`` configuration. marker_size : float Size of the points. Default depends on the ``figure_style`` configuration. marker_style : str Style of the points. Default depends on the ``figure_style`` configuration. copy_first : bool If ``True``, a copy of the scatter plot (with all its parameters) will be returned with the slice applied. Any other parameters passed to this method will also be applied to the copied scatter plot. If ``False``, a new scatter plot will be created with the slice applied and the parameters passed to this method. Returns ------- :class:`~graphinglib.data_plotting_1d.Scatter` A new :class:`~graphinglib.data_plotting_1d.Scatter` object which is a slice of the original scatter plot. """ mask = (self._y_data >= y_min) & (self._y_data <= y_max) if copy_first: copy = self.copy() copy._x_data = self._x_data[mask] copy._y_data = self._y_data[mask] if label is not None: copy._label = label if face_color != "default": copy._face_color = face_color if edge_color != "default": copy._edge_color = edge_color if color_map != "default": copy._color_map = color_map if show_color_bar != "default": copy._show_color_bar = show_color_bar if marker_size != "default": copy._marker_size = marker_size if marker_style != "default": copy._marker_style = marker_style return copy else: return Scatter( self._x_data[mask], self._y_data[mask], label, face_color, edge_color, color_map, show_color_bar, marker_size, marker_style, )
[docs] def add_errorbars( self, x_error: Optional[ArrayLike] = None, y_error: Optional[ArrayLike] = None, cap_width: float | Literal["default"] = "default", errorbars_color: str = "default", errorbars_line_width: float | Literal["default"] = "default", cap_thickness: float | Literal["default"] = "default", ) -> None: """ Adds errorbars to the scatter plot. Parameters ---------- x_error, y_error : ArrayLike, optional Arrays of x and y errors. Use one or both. cap_width : float Width of the errorbar caps. Default depends on the ``figure_style`` configuration. errorbars_color : str Color of the errorbars. Default depends on the ``figure_style`` configuration. errorbars_line_width : float Width of the errorbars. Default depends on the ``figure_style`` configuration. cap_thickness : float Thickness of the errorbar caps. Default depends on the ``figure_style`` configuration. """ self._show_errorbars = True self._x_error = np.array(x_error) if x_error is not None else x_error self._y_error = np.array(y_error) if y_error is not None else y_error self._errorbars_color = errorbars_color self._errorbars_line_width = errorbars_line_width self._cap_thickness = cap_thickness self._cap_width = cap_width
[docs] def get_coordinates_at_x( self, x: float, interpolation_method: str = "linear", ) -> tuple[float, float]: """ Gets the coordinates of the point on the curve at a given x value. Parameters ---------- x : float The x value of the point. interpolation_method : str, The type of interpolation to be used, as defined in ``scipy.interpolate.interp1d``. .. seealso:: `scipy.interpolate.interp1d <https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html>`_ Defaults to "linear". Returns ------- tuple[float, float] The coordinates of the point on the curve at the given x value. """ return ( x, float(interp1d(self._x_data, self._y_data, kind=interpolation_method)(x)), )
[docs] def create_point_at_x( self, x: float, interpolation_method: str = "linear", label: Optional[str] = None, color: str = "default", edge_color: str = "default", marker_size: float | Literal["default"] = "default", marker_style: str = "default", line_width: float | Literal["default"] = "default", ) -> Point: """ Creates a Point on the curve at a given x value. Parameters ---------- x : float The x value of the point. interpolation_method : str, The type of interpolation to be used, as defined in ``scipy.interpolate.interp1d``. .. seealso:: `scipy.interpolate.interp1d <https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html>`_ Defaults to "linear". label : str, optional Label to be displayed in the legend. color : str Face color of the point. Default depends on the ``figure_style`` configuration. edge_color : str Edge color of the point. Default depends on the ``figure_style`` configuration. marker_size : float Size of the point. Default depends on the ``figure_style`` configuration. marker_style : str Style of the point. Default depends on the ``figure_style`` configuration. line_width : float Width of the point edge. Default depends on the ``figure_style`` configuration. Returns ------- :class:`~graphinglib.graph_elements.Point` The Point on the curve at the given x value. """ point = Point( x, self.get_coordinates_at_x(x, interpolation_method)[1], label=label, color=color, edge_color=edge_color, marker_size=marker_size, marker_style=marker_style, edge_width=line_width, ) return point
[docs] def get_coordinates_at_y( self, y: float, interpolation_method: str = "linear", ) -> list[tuple[float, float]]: """ Gets the coordinates the curve at a given y value. Can return multiple coordinate pairs if the curve crosses the y value multiple times. Parameters ---------- y : float The y value of the point. interpolation_method : str, The type of interpolation to be used, as defined in ``scipy.interpolate.interp1d``. .. seealso:: `scipy.interpolate.interp1d <https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html>`_ Defaults to "linear". Returns ------- list[tuple[float, float]] The coordinates of the points on the curve at the given y value. """ xs = self._x_data ys = self._y_data assert isinstance(xs, np.ndarray) and isinstance(ys, np.ndarray) crossings = np.where(np.diff(np.sign(ys - y)))[0] x_vals: list[float] = [] for cross in crossings: x1, x2 = xs[cross], xs[cross + 1] y1, y2 = ys[cross], ys[cross + 1] f = interp1d([y1, y2], [x1, x2], kind=interpolation_method) x_val = f(y) x_vals.append(float(x_val)) points = [(x_val, y) for x_val in x_vals] return points
[docs] def create_points_at_y( self, y: float, interpolation_method: str = "linear", label: Optional[str] = None, color: str = "default", edge_color: str = "default", marker_size: float | Literal["default"] = "default", marker_style: str = "default", line_width: float | Literal["default"] = "default", ) -> list[Point]: """ Creates the Points on the curve at a given y value. Can return multiple Points if the curve crosses the y value multiple times. Parameters ---------- y : float The y value of the desired points. interpolation_method : str The type of interpolation to be used, as defined in ``scipy.interpolate.interp1d``. .. seealso:: `scipy.interpolate.interp1d <https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html>`_ Defaults to "linear". label : str, optional Label to be displayed in the legend. color : str Face color of the point. Default depends on the ``figure_style`` configuration. edge_color : str Edge color of the point. Default depends on the ``figure_style`` configuration. marker_size : float Size of the point. Default depends on the ``figure_style`` configuration. marker_style : str Style of the point. Default depends on the ``figure_style`` configuration. line_width : float Width of the point edge. Default depends on the ``figure_style`` configuration. Returns ------- list[:class:`~graphinglib.graph_elements.Point`] The Point objects on the curve at the given y value. """ coords = self.get_coordinates_at_y(y, interpolation_method) points = [ Point( coord[0], coord[1], label=label, color=color, edge_color=edge_color, marker_size=marker_size, marker_style=marker_style, edge_width=line_width, ) for coord in coords ] return points
def _get_contrasting_shade(self, color: str | tuple[int, int, int]) -> str: """ Gives the most contrasting shade (black/white) for a given color. The algorithm used comes from this Stack Exchange answer : https://ux.stackexchange.com/a/82068. Parameters ---------- color : str or tuple[int, int, int] Color that needs to be contrasted. This can either be a known matplotlib color string or a RGB code, given as a tuple of integers that take 0-255. Returns ------- shade : str Shade (black/white) that contrasts the most with the given color. """ if isinstance(color, str): color = to_rgba_array(color)[0, :3] * 255 R, G, B = color if R <= 10: Rg = R / 3294 else: Rg = (R / 269 + 0.0513) ** 2.4 if G <= 10: Gg = G / 3294 else: Gg = (G / 269 + 0.0513) ** 2.4 if B <= 10: Bg = B / 3294 else: Bg = (B / 269 + 0.0513) ** 2.4 L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg if L < 0.5: return "white" else: return "black" def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs) -> None: """ Plots the element in the specified axes. """ # Check that either face color or edge color is not None if self._face_color is None and self._edge_color is None: raise ValueError( "Both face color and edge color cannot be None. Please set at least one of them to a valid color." ) # Check that either face color or edge color is not a list/array/tuple of intensities if isinstance(self._face_color, (list, tuple, np.ndarray)) and isinstance( self._edge_color, (list, tuple, np.ndarray) ): # If they're 3 or 4 element arrays, assume they're RGB or RGBA values if len(self._face_color) in [3, 4] or len(self._edge_color) in [3, 4]: pass else: raise ValueError( "Both face color and edge color cannot be lists/arrays/tuples of intensities or colors. Please set at least one of them to a valid color or set one of them to None." ) # Convert face color to matplotlib notation if self._face_color is None: # Set to transparent mpl_face_color = "none" elif isinstance(self._face_color, str) and self._face_color == "default": # Use color cycle (figure uses a matplotlib style) mpl_face_color = None elif isinstance(self._face_color, str) and self._face_color == "color cycle": # Use color cycle mpl_face_color = None else: # Use specified color mpl_face_color = self._face_color # Convert edge color to matplotlib notation if self._edge_color is None: # Set to transparent mpl_edge_color = "none" elif isinstance(self._edge_color, str) and self._edge_color == "default": # Use color cycle (figure uses a matplotlib style) mpl_edge_color = None elif isinstance(self._edge_color, str) and self._edge_color == "color cycle": # Use color cycle mpl_edge_color = kwargs["cycle_color"] else: # Use specified color mpl_edge_color = self._edge_color # Check whether to use color map (one of the colors is an array of intensities) if isinstance(self._face_color, (list, tuple, np.ndarray)): if all(isinstance(i, (int, float)) for i in self._face_color): color_map = plt.get_cmap(self._color_map) norm = Normalize(vmin=min(self._face_color), vmax=max(self._face_color)) mpl_face_color = [color_map(norm(i)) for i in self._face_color] elif isinstance(self._edge_color, (list, tuple, np.ndarray)): if all(isinstance(i, (int, float)) for i in self._edge_color): color_map = plt.get_cmap(self._color_map) norm = Normalize(vmin=min(self._edge_color), vmax=max(self._edge_color)) mpl_edge_color = [color_map(norm(i)) for i in self._edge_color] params = { "s": self._marker_size, "marker": self._marker_style if self._marker_style != "default" else "o", } params = {k: v for k, v in params.items() if v != "default"} params["facecolors"] = mpl_face_color params["edgecolors"] = mpl_edge_color self.handle = axes.scatter( self._x_data, self._y_data, label=self._label, zorder=z_order, **params, ) if self._show_errorbars: # Convert errorbars color to matplotlib notation if self._errorbars_color is None: raise ValueError( "Errorbars color cannot be None. Please set the errorbars color to a valid color." ) elif isinstance(self._errorbars_color, str) and ( self._errorbars_color == "default" ): # Use color cycle mpl_errorbars_color = None elif ( isinstance(self._errorbars_color, str) and self._errorbars_color == "same as scatter" ): marker_edge_color = self.handle.get_edgecolor() marker_face_color = self.handle.get_facecolor() if is_color_like(marker_edge_color): mpl_errorbars_color = marker_edge_color elif is_color_like(marker_face_color): mpl_errorbars_color = marker_face_color else: ax_face_color = plt.rcParams["axes.facecolor"] mpl_errorbars_color = self._get_contrasting_shade(ax_face_color) elif isinstance(self._errorbars_color, str): # Use specified color mpl_errorbars_color = self._errorbars_color else: raise ValueError("Errorbars color must be a string.") errorbar_params = { "markerfacecolor": None, "markeredgecolor": None, "elinewidth": self._errorbars_line_width, "capsize": self._cap_width, "capthick": self._cap_thickness, "linestyle": "none", } errorbar_params = { k: v for k, v in errorbar_params.items() if v != "default" } errorbar_params["ecolor"] = mpl_errorbars_color self.errorbars_handle = axes.errorbar( self._x_data, self._y_data, xerr=self._x_error, yerr=self._y_error, zorder=z_order - 1, **errorbar_params, ) if ( self._show_color_bar and self._face_color is not None and not isinstance(self.face_color, str) ): # Create color bar from face color intensities color_map = plt.get_cmap(self._color_map) norm = Normalize(vmin=min(self._face_color), vmax=max(self._face_color)) sm = plt.cm.ScalarMappable(cmap=color_map, norm=norm) sm.set_array([]) plt.colorbar(sm, ax=axes) if ( self._show_color_bar and self._edge_color is not None and not isinstance(self.edge_color, str) ): # Create color bar from edge color intensities color_map = plt.get_cmap(self._color_map) norm = Normalize(vmin=min(self._edge_color), vmax=max(self._edge_color)) sm = plt.cm.ScalarMappable(cmap=color_map, norm=norm) sm.set_array([]) plt.colorbar(sm, ax=axes)
[docs] @dataclass class Histogram: """ This class implements a general histogram. Parameters ---------- data : ArrayLike Array of values to be plotted. number_of_bins : int Number of bins to be used in the histogram. label : str, optional Label to be displayed in the legend. face_color : str Face color of the histogram. Default depends on the ``figure_style`` configuration. edge_color : str Edge color of the histogram. Default depends on the ``figure_style`` configuration. hist_type : str Type of the histogram. Can be "bar", "barstacked", "step", "stepfilled". Default depends on the ``figure_style`` configuration. alpha : float Transparency of the histogram. Default depends on the ``figure_style`` configuration. line_width : float Width of the histogram edge. Default depends on the ``figure_style`` configuration. normalize : bool Whether or not to normalize the histogram. Default depends on the ``figure_style`` configuration. show_pdf : str Whether or not to show the probability density function. Can be "normal" or "gaussian". Default depends on the ``figure_style`` configuration. show_params : bool Whether or not to show the mean and standard deviation of the data. Default depends on the ``figure_style`` configuration. """
[docs] def __init__( self, data: ArrayLike, number_of_bins: int, label: Optional[str] = None, face_color: str = "default", edge_color: str = "default", hist_type: str = "default", alpha: float | Literal["default"] = "default", line_width: float | Literal["default"] = "default", normalize: bool | Literal["default"] = "default", show_params: bool | Literal["default"] = "default", ) -> None: """ This class implements a general histogram. Parameters ---------- data : ArrayLike Array of values to be plotted. number_of_bins : int Number of bins to be used in the histogram. label : str, optional Label to be displayed in the legend. face_color : str Face color of the histogram. Default depends on the ``figure_style`` configuration. edge_color : str Edge color of the histogram. Default depends on the ``figure_style`` configuration. hist_type : str Type of the histogram. Can be "bar", "barstacked", "step", "stepfilled". Default depends on the ``figure_style`` configuration. alpha : float Transparency of the histogram. Default depends on the ``figure_style`` configuration. line_width : float Width of the histogram edge. Default depends on the ``figure_style`` configuration. normalize : bool Whether or not to normalize the histogram. Default depends on the ``figure_style`` configuration. show_pdf : str Whether or not to show the probability density function. Can be "normal" or "gaussian". Default depends on the ``figure_style`` configuration. show_params : bool Whether or not to show the mean and standard deviation of the data. Default depends on the ``figure_style`` configuration. """ self._data = np.asarray(data) self._number_of_bins = number_of_bins self._label = label self._face_color = face_color self._edge_color = edge_color self._hist_type = hist_type self._alpha = alpha self._line_width = line_width self._normalize = normalize self._show_params = show_params self._show_pdf = False self._pdf_type = None self._pdf_show_mean = None self._pdf_show_std = None self._pdf_curve_color = None self._pdf_mean_color = None self._pdf_std_color = None self._mean = np.mean(self._data) self._standard_deviation = np.std(self._data) _parameters = np.histogram( self._data, bins=self._number_of_bins, density=self._normalize ) self._bin_heights, bin_edges = _parameters[0], _parameters[1] bin_width = bin_edges[1] - bin_edges[0] bin_centers = bin_edges[1:] - bin_width / 2 self._bin_width = bin_width self._bin_centers = bin_centers self._bin_edges = bin_edges
[docs] @classmethod def from_fit_residuals( cls, fit: Fit, number_of_bins: int, label: Optional[str] = None, face_color: str = "default", edge_color: str = "default", hist_type: str = "default", alpha: int | Literal["default"] = "default", line_width: int | Literal["default"] = "default", normalize: bool | Literal["default"] = "default", show_params: bool | Literal["default"] = "default", ) -> Self: """ Calculates the residuals of a fit and plots them as a histogram. Parameters ---------- fit : Fit The fit from which the residuals are to be calculated. number_of_bins : int Number of bins to be used in the histogram. label : str, optional Label to be displayed in the legend. face_color : str Face color of the histogram. Default depends on the ``figure_style`` configuration. edge_color : str Edge color of the histogram. Default depends on the ``figure_style`` configuration. hist_type : str Type of the histogram. Can be "bar", "barstacked", "step", "stepfilled". Default depends on the ``figure_style`` configuration. alpha : float Transparency of the histogram. Default depends on the ``figure_style`` configuration. line_width : float Width of the histogram edge. Default depends on the ``figure_style`` configuration. normalize : bool Whether or not to normalize the histogram. Default depends on the ``figure_style`` configuration. show_pdf : str Whether or not to show the probability density function. Can be "normal" or "gaussian". Default depends on the ``figure_style`` configuration. show_params : bool Whether or not to show the mean and standard deviation of the data. Default depends on the ``figure_style`` configuration. Returns ------- A :class:`~graphinglib.data_plotting_1d.Histogram` object created from the residuals of a fit. """ residuals = fit.get_residuals() return cls( residuals, number_of_bins, label, face_color, edge_color, hist_type, alpha, line_width, normalize, show_params, )
@property def data(self) -> np.ndarray: return self._data @data.setter def data(self, data: ArrayLike) -> None: self._data = np.array(data) self._mean = np.mean(self._data) self._standard_deviation = np.std(self._data) _parameters = np.histogram( self._data, bins=self._number_of_bins, density=self._normalize ) self._bin_heights, bin_edges = _parameters[0], _parameters[1] bin_width = bin_edges[1] - bin_edges[0] bin_centers = bin_edges[1:] - bin_width / 2 self._bin_width = bin_width self._bin_centers = bin_centers self._bin_edges = bin_edges @property def number_of_bins(self) -> int: return self._number_of_bins @number_of_bins.setter def number_of_bins(self, number_of_bins: int) -> None: self._number_of_bins = number_of_bins _parameters = np.histogram( self._data, bins=self._number_of_bins, density=self._normalize ) self._bin_heights, bin_edges = _parameters[0], _parameters[1] bin_width = bin_edges[1] - bin_edges[0] bin_centers = bin_edges[1:] - bin_width / 2 self._bin_width = bin_width self._bin_centers = bin_centers self._bin_edges = bin_edges @property def label(self) -> str: return self._label @label.setter def label(self, label: str) -> None: self._label = label @property def face_color(self) -> str: 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: return self._edge_color @edge_color.setter def edge_color(self, edge_color: str) -> None: self._edge_color = edge_color @property def hist_type(self) -> str: return self._hist_type @hist_type.setter def hist_type(self, hist_type: str) -> None: self._hist_type = hist_type @property def alpha(self) -> float: return self._alpha @alpha.setter def alpha(self, alpha: float) -> None: self._alpha = alpha @property def line_width(self) -> float: return self._line_width @line_width.setter def line_width(self, line_width: float) -> None: self._line_width = line_width @property def normalize(self) -> bool: return self._normalize @normalize.setter def normalize(self, normalize: bool) -> None: self._normalize = normalize @property def show_params(self) -> bool: return self._show_params @show_params.setter def show_params(self, show_params: bool) -> None: self._show_params = show_params @property def mean(self) -> float: return self._mean @property def standard_deviation(self) -> float: return self._standard_deviation @property def parameters(self) -> tuple[float, float]: return self._mean, self._standard_deviation @property def bin_heights(self) -> np.ndarray: return self._bin_heights @property def bin_centers(self) -> np.ndarray: return self._bin_centers @property def bin_edges(self) -> np.ndarray: return self._bin_edges def _create_label(self) -> None: """ Creates the label of the histogram (with or without parameters). """ lab = self._label if lab and self._show_params: lab += ( " :\n" + f"$\mu$ = {0 if abs(self._mean) < 1e-3 else self._mean:.3f}, $\sigma$ = {self._standard_deviation:.3f}" ) elif self._show_params: lab = f"$\mu$ = {0 if abs(self._mean) < 1e-3 else self._mean:.3f}, $\sigma$ = {self._standard_deviation:.3f}" self._label = lab
[docs] def copy(self) -> Self: """ Returns a deep copy of the :class:`~graphinglib.data_plotting_1d.Histogram` object. """ return deepcopy(self)
def _normal_normalized(self, x: ArrayLike) -> ArrayLike: """ Calculates the normalized gaussian curve from the mean and standard deviation of the data. Parameters ---------- x : ArrayLike The x values at which the gaussian curve is to be calculated. Returns ------- The corresponding array of y values of the gaussian curve. """ x = np.array(x) return (1 / (self._standard_deviation * np.sqrt(2 * np.pi))) * np.exp( -0.5 * (((x - self._mean) / self._standard_deviation) ** 2) ) def _normal_not_normalized(self, x: ArrayLike) -> ArrayLike: """ Calculates the (not normalized) gaussian curve from the mean and standard deviation of the data. Parameters ---------- x : ArrayLike The x values at which the gaussian curve is to be calculated. Returns ------- The corresponding array of y values of the gaussian curve. """ x = np.array(x) return sum(self._bin_heights) * self._bin_width * self._normal_normalized(x)
[docs] def add_pdf( self, type: str = "normal", show_mean: bool | Literal["default"] = "default", show_std: bool | Literal["default"] = "default", curve_color: str | Literal["default"] = "default", mean_color: str | Literal["default"] = "default", std_color: str | Literal["default"] = "default", ) -> None: """ Shows the probability density function of the histogram. Parameters ---------- type : str The type of probability density function to be shown. Currently only "normal" is supported. Defaults to "normal". show_mean : bool Whether or not to show the mean of the data. Default depends on the ``figure_style`` configuration. show_std : bool Whether or not to show the standard deviation of the data. Default depends on the ``figure_style`` configuration. curve_color : str Color of the probability density function curve. Default depends on the ``figure_style`` configuration. mean_color : str Color of the mean line. Default depends on the ``figure_style`` configuration. std_color : str Color of the standard deviation lines. Default depends on the ``figure_style`` configuration. """ if type != "normal": raise ValueError("Currently, only 'normal' distribution is supported.") self._show_pdf = True self._pdf_type = type self._pdf_show_mean = show_mean self._pdf_show_std = show_std self._pdf_curve_color = curve_color self._pdf_mean_color = mean_color self._pdf_std_color = std_color
def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs) -> None: """ Plots the element in the specified axes. """ self._create_label() params = { "facecolor": ( to_rgba(self._face_color, self._alpha) if self._face_color != "default" and self._alpha != "default" else "default" ), "edgecolor": ( to_rgba(self._edge_color, 1) if self._edge_color != "default" else self._edge_color ), "linewidth": self._line_width, } params = {k: v for k, v in params.items() if v != "default"} self.handle = Polygon( np.array([[0, 2, 2, 3, 3, 1, 1, 0, 0], [0, 0, 1, 1, 2, 2, 3, 3, 0]]).T, **params, ) params = { "facecolor": ( to_rgba(self._face_color, self._alpha) if self._face_color != "default" and self._alpha != "default" else "default" ), "edgecolor": ( to_rgba(self._edge_color, 1) if self._edge_color != "default" else self._edge_color ), "histtype": self._hist_type, "linewidth": self._line_width, "density": self._normalize, } params = {k: v for k, v in params.items() if v != "default"} axes.hist( self._data, bins=self._number_of_bins, label=self._label, zorder=z_order - 1, **params, ) if self._show_pdf: normal = ( self._normal_normalized if self._normalize else self._normal_not_normalized ) num_of_points = 500 x_data = np.linspace(self._bin_edges[0], self._bin_edges[-1], num_of_points) y_data = normal(x_data) params = { "color": self._pdf_curve_color, } params = {k: v for k, v in params.items() if v != "default"} axes.plot( x_data, y_data, zorder=z_order, **params, ) curve_max_y = normal(self._mean) curve_std_y = normal(self._mean + self._standard_deviation) if self._pdf_show_std: params = {} if self._pdf_std_color != "default": params["colors"] = [self._pdf_std_color, self._pdf_std_color] plt.vlines( [ self._mean - self._standard_deviation, self._mean + self._standard_deviation, ], [0, 0], [curve_std_y, curve_std_y], linestyles=["dashed"], zorder=z_order - 1, **params, ) if self._pdf_show_mean: params = {} if self._pdf_mean_color != "default": params["colors"] = [self._pdf_mean_color] plt.vlines( self._mean, 0, curve_max_y, linestyles=["dashed"], zorder=z_order - 1, **params, )