from __future__ import annotations
from .inherit import INHERIT, Inherit, is_inherit
from copy import deepcopy
from dataclasses import dataclass
from types import NoneType
from typing import Callable, Optional, Protocol, runtime_checkable
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import Colormap, Normalize, is_color_like, to_rgba
from matplotlib.patches import Polygon
from numpy.typing import ArrayLike
from pyperclip import copy as copy_to_clipboard
from scipy.integrate import cumulative_trapezoid
from scipy.interpolate import interp1d
from .graph_elements import Plottable, Point
from .tools import MathematicalObject, get_contrasting_shade
try:
from typing import Self
except ImportError:
from typing_extensions import Self
@runtime_checkable
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
@runtime_checkable
class Plottable1D(Plottable, Protocol):
"""
Dummy class to allow type hinting of Plottable1D objects.
"""
@staticmethod
def to_desmos(
x_data: ArrayLike, y_data: ArrayLike, decimal_precision: int = 2
) -> str:
"""
Gives the data points in a Desmos-readable format. The outputted string can then be pasted into a single Desmos
cell and the object's data will be displayed.
.. note::
NaN values are ignored.
Parameters
----------
x_data, y_data : ArrayLike
Arrays of x and y values to be plotted.
decimal_precision : int, optional
Specifies the number of decimals of the formatted points.
Defaults to 2.
Returns
-------
formatted points : str
A list of tuples representing every data point.
"""
sorted_indices = np.argsort(x_data)
sorted_x_data = x_data[sorted_indices]
sorted_y_data = y_data[sorted_indices]
# Change exponential formatting to be interpretable by Desmos
def format_tex(num: str, exponent: str):
num = num.rstrip("0")
if exponent == "+00":
return str(num)
else:
return rf"{num}\cdot10^" + "{" + str(int(exponent)) + "}"
formatted_points = "["
for x, y in zip(sorted_x_data, sorted_y_data):
if np.isnan(x) or np.isnan(y):
continue
x_num, x_exponent = f"{x:.{decimal_precision:d}e}".split("e")
y_num, y_exponent = f"{y:.{decimal_precision:d}e}".split("e")
formatted_points += (
f"({format_tex(x_num, x_exponent)},{format_tex(y_num, y_exponent)}),"
)
formatted_points = formatted_points[:-1] + "]"
return formatted_points
[docs]
@dataclass
class Curve(Plottable1D, MathematicalObject):
"""
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.
Typical range is ``0.5`` to ``4``.
Default depends on the ``figure_style`` configuration.
line_style : str
Style of the curve.
Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and
``"dotted"``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the curve.
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_data: ArrayLike,
y_data: ArrayLike,
label: Optional[str] = None,
color: str | Inherit = INHERIT,
line_width: float | Inherit = INHERIT,
line_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
) -> 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._alpha = alpha
self._x_error = None
self._y_error = None
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 | Inherit = INHERIT,
line_width: float | Inherit = INHERIT,
line_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
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.
Typical range is ``0.5`` to ``4``.
Default depends on the ``figure_style`` configuration.
line_style : str
Style of the curve.
Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and
``"dotted"``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the curve.
Range is ``0`` (transparent) to ``1`` (opaque).
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.
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)``).
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, alpha)
@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 x_error(self) -> np.ndarray | None:
return self._x_error
@x_error.setter
def x_error(self, x_error: ArrayLike) -> None:
self._x_error = np.asarray(x_error)
@property
def y_error(self) -> np.ndarray | None:
return self._y_error
@y_error.setter
def y_error(self, y_error: ArrayLike) -> None:
self._y_error = np.asarray(y_error)
@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 | Inherit:
return self._line_width
@line_width.setter
def line_width(self, line_width: float | Inherit) -> 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 alpha(self) -> float | Inherit:
return self._alpha
@alpha.setter
def alpha(self, alpha: float | Inherit) -> None:
self._alpha = alpha
@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 | Inherit:
return self._errorbars_line_width
@errorbars_line_width.setter
def errorbars_line_width(self, errorbars_line_width: float | Inherit) -> None:
self._errorbars_line_width = errorbars_line_width
@property
def cap_thickness(self) -> float | Inherit:
return self._cap_thickness
@cap_thickness.setter
def cap_thickness(self, cap_thickness: float | Inherit) -> None:
self._cap_thickness = cap_thickness
@property
def cap_width(self) -> float | Inherit:
return self._cap_width
@cap_width.setter
def cap_width(self, cap_width: float | Inherit) -> 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 | Inherit:
return self._error_curves_line_width
@error_curves_line_width.setter
def error_curves_line_width(self, error_curves_line_width: float | Inherit) -> 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 __eq__(self, other: Self) -> bool:
"""
Defines the equality between two curves.
"""
return (
np.equal(self.x_data, other.x_data).all()
and np.equal(self.y_data, other.y_data).all()
)
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 __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 __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 __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 __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 __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 | Inherit = INHERIT,
line_width: float | Inherit = INHERIT,
line_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
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.
Typical range is ``0.5`` to ``4``.
Default depends on the ``figure_style`` configuration.
line_style : str
Style of the slice.
Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and
``"dotted"``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the slice.
Range is ``0`` (transparent) to ``1`` (opaque).
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.
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)``).
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 != INHERIT:
copy._color = color
if line_width != INHERIT:
copy._line_width = line_width
if line_style != INHERIT:
copy._line_style = line_style
if alpha != INHERIT:
copy._alpha = alpha
return copy
else:
return Curve(x_data, y_data, label, color, line_width, line_style, alpha)
[docs]
def create_slice_y(
self,
y1: float,
y2: float,
label: Optional[str] = None,
color: str | Inherit = INHERIT,
line_width: float | Inherit = INHERIT,
line_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
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.
Typical range is ``0.5`` to ``4``.
Default depends on the ``figure_style`` configuration.
line_style : str
Style of the slice.
Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and
``"dotted"``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the slice.
Range is ``0`` (transparent) to ``1`` (opaque).
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.
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)``).
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 != INHERIT:
copy._color = color
if line_width != INHERIT:
copy._line_width = line_width
if line_style != INHERIT:
copy._line_style = line_style
if alpha != INHERIT:
copy._alpha = alpha
return copy
else:
return Curve(x_data, y_data, label, color, line_width, line_style, alpha)
[docs]
def add_errorbars(
self,
x_error: Optional[ArrayLike] = None,
y_error: Optional[ArrayLike] = None,
cap_width: float | Inherit = INHERIT,
errorbars_color: str | Inherit = INHERIT,
errorbars_line_width: float | Inherit = INHERIT,
cap_thickness: float | Inherit = INHERIT,
) -> 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.
Values must be non-negative.
cap_width : float
Width of the errorbar caps.
Typical range is ``0`` to ``10`` points.
Default depends on the ``figure_style`` configuration.
errorbars_color : str
Color of the errorbars.
``"same as curve"`` uses the curve color.
Default depends on the ``figure_style`` configuration.
errorbars_line_width : float
Width of the errorbars.
Typical range is ``0.5`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
cap_thickness : float
Thickness of the errorbar caps.
Typical range is ``0.5`` to ``3`` points.
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)``).
"""
self._show_errorbars = True
if x_error is not None:
self._x_error = np.array(x_error)
if y_error is not None:
self._y_error = np.array(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 | Inherit = INHERIT,
error_curves_line_style: str | Inherit = INHERIT,
error_curves_line_width: float | Inherit = INHERIT,
error_curves_fill_between: bool | Inherit = INHERIT,
) -> 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.
``"same as curve"`` uses the curve color.
Default depends on the ``figure_style`` configuration.
error_curves_line_style : str
Line style of the error curves.
Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and
``"dotted"``.
Default depends on the ``figure_style`` configuration.
error_curves_line_width : float
Line width of the error curves.
Typical range is ``0.5`` to ``3`` points.
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.
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._show_error_curves = True
if y_error is not None:
self._y_error = np.array(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,
face_color: str | Inherit = INHERIT,
edge_color: str | Inherit = INHERIT,
marker_size: float | Inherit = INHERIT,
marker_style: str | Inherit = INHERIT,
line_width: float | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
) -> 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.
face_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.
Typical range is ``10`` to ``100``.
Default depends on the ``figure_style`` configuration.
marker_style : str
Style of the point.
Common values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``,
``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, ``"_"``,
``"P"``, ``"X"``, ``"None"``, ``" "``, and ``""``.
Default depends on the ``figure_style`` configuration.
line_width : float
Width of the point edge.
Typical range is ``0`` to ``3`` points.
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.
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)``).
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,
face_color=face_color,
edge_color=edge_color,
marker_size=marker_size,
marker_style=marker_style,
edge_width=line_width,
alpha=alpha,
)
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,
face_color: str | Inherit = INHERIT,
edge_color: str | Inherit = INHERIT,
marker_size: float | Inherit = INHERIT,
marker_style: str | Inherit = INHERIT,
line_width: float | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
) -> 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.
face_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.
Typical range is ``10`` to ``100``.
Default depends on the ``figure_style`` configuration.
marker_style : str
Style of the point.
Common values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``,
``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, ``"_"``,
``"P"``, ``"X"``, ``"None"``, ``" "``, and ``""``.
Default depends on the ``figure_style`` configuration.
line_width : float
Width of the point edge.
Typical range is ``0`` to ``3`` points.
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.
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)``).
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,
face_color=face_color,
edge_color=edge_color,
marker_size=marker_size,
marker_style=marker_style,
edge_width=line_width,
alpha=alpha,
)
for pair in pairs
]
return points
[docs]
def create_derivative_curve(
self,
label: Optional[str] = None,
color: str | Inherit = INHERIT,
line_width: float | Inherit = INHERIT,
line_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
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.
Typical range is ``0.5`` to ``4``.
Default depends on the ``figure_style`` configuration.
line_style : str
Style of the new curve.
Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and
``"dotted"``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the new curve.
Range is ``0`` (transparent) to ``1`` (opaque).
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.
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)``).
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 != INHERIT:
copy._color = color
if line_width != INHERIT:
copy._line_width = line_width
if line_style != INHERIT:
copy._line_style = line_style
if alpha != INHERIT:
copy._alpha = alpha
return copy
else:
return Curve(x_data, y_data, label, color, line_width, line_style, alpha)
[docs]
def create_integral_curve(
self,
initial_value: float = 0,
label: Optional[str] = None,
color: str | Inherit = INHERIT,
line_width: float | Inherit = INHERIT,
line_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
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.
Typical range is ``0.5`` to ``4``.
Default depends on the ``figure_style`` configuration.
line_style : str
Style of the new curve.
Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and
``"dotted"``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the new curve.
Range is ``0`` (transparent) to ``1`` (opaque).
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.
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)``).
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 != INHERIT:
copy._color = color
if line_width != INHERIT:
copy._line_width = line_width
if line_style != INHERIT:
copy._line_style = line_style
if alpha != INHERIT:
copy._alpha = alpha
return copy
else:
return Curve(
self._x_data, y_data, label, color, line_width, line_style, alpha
)
[docs]
def create_tangent_curve(
self,
x: float,
label: Optional[str] = None,
color: str | Inherit = INHERIT,
line_width: float | Inherit = INHERIT,
line_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
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.
Typical range is ``0.5`` to ``4``.
Default depends on the ``figure_style`` configuration.
line_style : str
Style of the new curve.
Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and
``"dotted"``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the new curve.
Range is ``0`` (transparent) to ``1`` (opaque).
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.
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)``).
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 != INHERIT:
copy._color = color
if line_width != INHERIT:
copy._line_width = line_width
if line_style != INHERIT:
copy._line_style = line_style
if alpha != INHERIT:
copy._alpha = alpha
return copy
else:
tangent_curve = Curve(
self._x_data, y_data, label, color, line_width, line_style, alpha
)
return tangent_curve
[docs]
def create_normal_curve(
self,
x: float,
label: Optional[str] = None,
color: str | Inherit = INHERIT,
line_width: float | Inherit = INHERIT,
line_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
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.
Typical range is ``0.5`` to ``4``.
Default depends on the ``figure_style`` configuration.
line_style : str
Style of the new curve.
Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and
``"dotted"``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the new curve.
Range is ``0`` (transparent) to ``1`` (opaque).
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.
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)``).
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 != INHERIT:
copy._color = color
if line_width != INHERIT:
copy._line_width = line_width
if line_style != INHERIT:
copy._line_style = line_style
if alpha != INHERIT:
copy._alpha = alpha
return copy
else:
normal_curve = Curve(
self._x_data, y_data, label, color, line_width, line_style, alpha
)
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.trapezoid(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 | Inherit = INHERIT,
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``.
``"same as curve"`` uses the curve color.
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.
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)``).
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.trapezoid(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.trapezoid(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 to_desmos(self, decimal_precision: int = 2, to_clipboard: bool = False) -> str:
"""
Gives the data points in a Desmos-readable format. The outputted string can then be pasted into a single Desmos
cell and the object's data will be displayed.
Parameters
----------
decimal_precision : int, optional
Specifies the number of decimals of the formatted points.
Defaults to 2.
to_clipboard : bool, optional
Specifies whether the points should be directly copied to the user's clipboard in addition to being
returned as a string.
Defaults to False.
Returns
-------
formatted points : str
A list of tuples representing every data point.
"""
formatted_points = super().to_desmos(
self._x_data, self._y_data, decimal_precision
)
if to_clipboard:
copy_to_clipboard(formatted_points)
return formatted_points
[docs]
def create_intersection_points(
self,
other: Self,
labels: Optional[list[str] | str] = None,
face_colors: list[str] | str | Inherit = INHERIT,
edge_colors: list[str] | str | Inherit = INHERIT,
marker_sizes: list[float] | float | Inherit = INHERIT,
marker_styles: list[str] | str | Inherit = INHERIT,
edge_widths: list[float] | float | Inherit = INHERIT,
alphas: list[float] | float | Inherit = INHERIT,
) -> 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.
face_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.
Typical range is ``10`` to ``100``.
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.
Common values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``,
``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, ``"_"``,
``"P"``, ``"X"``, ``"None"``, ``" "``, and ``""``.
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.
Typical range is ``0`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
alphas : list[float] or float
Opacities of the intersection points.
If a single float is passed, all intersection points will have the same opacity.
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)``).
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(face_colors, list)
face_color = face_colors[i]
except (IndexError, TypeError, AssertionError):
face_color = face_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
try:
assert isinstance(alphas, list)
alpha = alphas[i]
except (IndexError, TypeError, AssertionError):
alpha = alphas
point = point_coords[i]
point_objects.append(
Point(
point[0],
point[1],
label=label,
face_color=face_color,
edge_color=edge_color,
marker_size=marker_size,
marker_style=marker_style,
edge_width=edge_width,
alpha=alpha,
)
)
return point_objects
def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs) -> None:
"""
Plots the element in the specified axes.
"""
params = {
"color": self._color,
"linewidth": self._line_width,
"linestyle": self._line_style,
"alpha": self._alpha,
}
if self._show_errorbars:
params.update(
{
"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 != INHERIT}
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 = {k: v for k, v in params.items() if v != INHERIT}
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 != INHERIT}
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,
facecolor=self.handle[0].get_color(),
alpha=0.2,
)
if self._fill_between_bounds:
params = {"alpha": 0.2}
params["facecolor"] = (
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 != INHERIT}
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(Plottable1D, MathematicalObject):
"""
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 used when ``face_color`` or ``edge_color`` is an array of intensity values.
Examples include ``"viridis"``, ``"plasma"``, and ``"coolwarm"``.
Default depends on the ``figure_style`` configuration.
color_map_range: tuple[float, float], optional
The data range covered by the color map, given as ``(minimum, maximum)``.
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.
Typical range is ``10`` to ``100``.
Default depends on the ``figure_style`` configuration.
marker_edge_width: float
Line width of the marker edges.
Typical range is ``0`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
marker_style : str
Style of the points.
Common values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``,
``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, ``"_"``,
``"P"``, ``"X"``, ``"None"``, ``" "``, and ``""``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the scatter plot.
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)``). They may also be arrays of
intensity values, which are mapped through ``color_map``.
"""
[docs]
def __init__(
self,
x_data: ArrayLike,
y_data: ArrayLike,
label: Optional[str] = None,
face_color: str | ArrayLike | NoneType | Inherit = INHERIT,
edge_color: str | ArrayLike | NoneType | Inherit = INHERIT,
color_map: str | Colormap | Inherit = INHERIT,
color_map_range: Optional[tuple[float, float]] = None,
show_color_bar: bool | Inherit = INHERIT,
marker_size: float | Inherit = INHERIT,
marker_edge_width: float | Inherit = INHERIT,
marker_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
) -> 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 used when ``face_color`` or ``edge_color`` is an array of intensity values.
Examples include ``"viridis"``, ``"plasma"``, and ``"coolwarm"``.
Default depends on the ``figure_style`` configuration.
color_map_range: tuple[float, float], optional
The data range covered by the color map, given as ``(minimum, maximum)``.
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.
Typical range is ``10`` to ``100``.
Default depends on the ``figure_style`` configuration.
marker_edge_width: float
Line width of the marker edges.
Typical range is ``0`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
marker_style : str
Style of the points.
Common values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``,
``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, ``"_"``,
``"P"``, ``"X"``, ``"None"``, ``" "``, and ``""``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the scatter plot.
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)``). They may also be arrays of
intensity values, which are mapped through ``color_map``.
"""
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._color_map_range = color_map_range
self._show_color_bar = show_color_bar
self._marker_size = marker_size
self._marker_edge_width = marker_edge_width
self._marker_style = marker_style
self._alpha = alpha
self._x_error = None
self._y_error = None
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
self._color_bar_params: dict = {}
[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 | Inherit = INHERIT,
edge_color: str | ArrayLike | NoneType | Inherit = INHERIT,
color_map: str | Colormap | Inherit = INHERIT,
color_map_range: Optional[tuple[float, float]] = None,
show_color_bar: bool | Inherit = INHERIT,
marker_size: int | Inherit = INHERIT,
marker_edge_width: float | Inherit = INHERIT,
marker_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
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 used when ``face_color`` or ``edge_color`` is an array of intensity values.
Examples include ``"viridis"``, ``"plasma"``, and ``"coolwarm"``.
Default depends on the ``figure_style`` configuration.
color_map_range: tuple[float, float], optional
The data range covered by the color map, given as ``(minimum, maximum)``.
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.
Typical range is ``10`` to ``100``.
Default depends on the ``figure_style`` configuration.
marker_style : str
Style of the points.
Common values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``,
``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, ``"_"``,
``"P"``, ``"X"``, ``"None"``, ``" "``, and ``""``.
Default depends on the ``figure_style`` configuration.
marker_edge_width: float
Line width of the marker edges.
Typical range is ``0`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the scatter plot.
Range is ``0`` (transparent) to ``1`` (opaque).
Default depends on the ``figure_style`` configuration.
number_of_points : int
Number of points to be plotted.
Defaults to 30.
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)``). They may also be arrays of
intensity values, which are mapped through ``color_map``.
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,
color_map_range,
show_color_bar,
marker_size,
marker_edge_width,
marker_style,
alpha,
)
@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 x_error(self) -> np.ndarray | None:
return self._x_error
@x_error.setter
def x_error(self, x_error: ArrayLike) -> None:
self._x_error = np.asarray(x_error)
@property
def y_error(self) -> np.ndarray | None:
return self._y_error
@y_error.setter
def y_error(self, y_error: ArrayLike) -> None:
self._y_error = np.asarray(y_error)
@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 color_map_range(self) -> tuple[float, float]:
return self._color_map_range
@color_map_range.setter
def color_map_range(self, color_map_range: tuple[float, float]) -> None:
self._color_map_range = color_map_range
@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 | 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_edge_width(self) -> float:
return self._marker_edge_width
@marker_edge_width.setter
def marker_edge_width(self, value: float):
self._marker_edge_width = value
@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 alpha(self) -> float | Inherit:
return self._alpha
@alpha.setter
def alpha(self, alpha: float | Inherit) -> None:
self._alpha = alpha
@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
@property
def color_bar_params(self) -> dict:
return self._color_bar_params
def __eq__(self, other: Self) -> bool:
"""
Defines the equality between two scatters.
"""
return (
np.equal(self.x_data, other.x_data).all()
and np.equal(self.y_data, other.y_data).all()
)
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 __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 __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 __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 __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 __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 | Inherit = INHERIT,
edge_color: str | ArrayLike | NoneType | Inherit = INHERIT,
color_map: str | Colormap | Inherit = INHERIT,
color_map_range: Optional[tuple[float, float]] = None,
show_color_bar: bool | Inherit = INHERIT,
marker_size: float | Inherit = INHERIT,
marker_edge_width: float | Inherit = INHERIT,
marker_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
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 used when ``face_color`` or ``edge_color`` is an array of intensity values.
Examples include ``"viridis"``, ``"plasma"``, and ``"coolwarm"``.
Default depends on the ``figure_style`` configuration.
color_map_range: tuple[float, float], optional
The data range covered by the color map, given as ``(minimum, maximum)``.
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.
Typical range is ``10`` to ``100``.
Default depends on the ``figure_style`` configuration.
marker_edge_width: float
Line width of the marker edges.
Typical range is ``0`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
marker_style : str
Style of the points.
Common values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``,
``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, ``"_"``,
``"P"``, ``"X"``, ``"None"``, ``" "``, and ``""``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacities of the points.
Range is ``0`` (transparent) to ``1`` (opaque).
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.
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)``). They may also be arrays of
intensity values, which are mapped through ``color_map``.
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 != INHERIT:
copy._face_color = face_color
if edge_color != INHERIT:
copy._edge_color = edge_color
if color_map != INHERIT:
copy._color_map = color_map
if color_map_range:
copy._color_map_range = color_map_range
if show_color_bar != INHERIT:
copy._show_color_bar = show_color_bar
if marker_size != INHERIT:
copy._marker_size = marker_size
if marker_edge_width != INHERIT:
copy._marker_edge_width = marker_edge_width
if marker_style != INHERIT:
copy._marker_style = marker_style
if alpha != INHERIT:
copy._alpha = alpha
return copy
else:
return Scatter(
self._x_data[mask],
self._y_data[mask],
label,
face_color,
edge_color,
color_map,
color_map_range,
show_color_bar,
marker_size,
marker_edge_width,
marker_style,
alpha,
)
[docs]
def create_slice_y(
self,
y_min: float,
y_max: float,
label: Optional[str] = None,
face_color: str | ArrayLike | NoneType | Inherit = INHERIT,
edge_color: str | ArrayLike | NoneType | Inherit = INHERIT,
color_map: str | Colormap | Inherit = INHERIT,
color_map_range: Optional[tuple[float, float]] = None,
show_color_bar: bool | Inherit = INHERIT,
marker_size: float | Inherit = INHERIT,
marker_edge_width: float | Inherit = INHERIT,
marker_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
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 used when ``face_color`` or ``edge_color`` is an array of intensity values.
Examples include ``"viridis"``, ``"plasma"``, and ``"coolwarm"``.
Default depends on the ``figure_style`` configuration.
color_map_range: tuple[float, float], optional
The data range covered by the color map, given as ``(minimum, maximum)``.
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.
Typical range is ``10`` to ``100``.
Default depends on the ``figure_style`` configuration.
marker_edge_width: float
Line width of the marker edges.
Typical range is ``0`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
marker_style : str
Style of the points.
Common values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``,
``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, ``"_"``,
``"P"``, ``"X"``, ``"None"``, ``" "``, and ``""``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacities of the points.
Range is ``0`` (transparent) to ``1`` (opaque).
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.
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)``). They may also be arrays of
intensity values, which are mapped through ``color_map``.
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 != INHERIT:
copy._face_color = face_color
if edge_color != INHERIT:
copy._edge_color = edge_color
if color_map != INHERIT:
copy._color_map = color_map
if color_map_range:
copy._color_map_range = color_map_range
if show_color_bar != INHERIT:
copy._show_color_bar = show_color_bar
if marker_size != INHERIT:
copy._marker_size = marker_size
if marker_edge_width != INHERIT:
copy._marker_edge_width = marker_edge_width
if marker_style != INHERIT:
copy._marker_style = marker_style
if alpha != INHERIT:
copy._alpha = alpha
return copy
else:
return Scatter(
self._x_data[mask],
self._y_data[mask],
label,
face_color,
edge_color,
color_map,
color_map_range,
show_color_bar,
marker_size,
marker_edge_width,
marker_style,
alpha,
)
[docs]
def add_errorbars(
self,
x_error: Optional[ArrayLike] = None,
y_error: Optional[ArrayLike] = None,
cap_width: float | Inherit = INHERIT,
errorbars_color: str | Inherit = INHERIT,
errorbars_line_width: float | Inherit = INHERIT,
cap_thickness: float | Inherit = INHERIT,
) -> None:
"""
Adds errorbars to the scatter plot.
Parameters
----------
x_error, y_error : ArrayLike, optional
Arrays of x and y errors. Use one or both.
Values must be non-negative.
cap_width : float
Width of the errorbar caps.
Typical range is ``0`` to ``10`` points.
Default depends on the ``figure_style`` configuration.
errorbars_color : str
Color of the errorbars.
``"same as scatter"`` uses the scatter marker color.
Default depends on the ``figure_style`` configuration.
errorbars_line_width : float
Width of the errorbars.
Typical range is ``0.5`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
cap_thickness : float
Thickness of the errorbar caps.
Typical range is ``0.5`` to ``3`` points.
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)``).
"""
self._show_errorbars = True
if x_error is not None:
self._x_error = np.array(x_error)
if y_error is not None:
self._y_error = np.array(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 set_color_bar_params(
self,
label: Optional[str] = None,
position: Optional[str] = None,
**color_bar_params,
) -> None:
"""
Sets the color bar parameters.
Parameters
----------
label : str, optional
Label of the color bar.
position : str, optional
Position of the color bar relative to the ``Figure``. It can be "left",
"right", "top" or "bottom". This also determines the orientation of the
color bar (vertical if the color bar is plotted on the "left" or "right",
horizontal otherwise). If None, the color bar is plotted on the right
side of the ``Figure``.
Values are ``"left"``, ``"right"``, ``"top"``, and ``"bottom"``.
**color_bar_params:
Additional keyword arguments are passed to ``plt.colorbar`` call.
"""
self._color_bar_params = color_bar_params
if label is not None:
self._color_bar_params["label"] = label
if position is not None:
self._color_bar_params["location"] = position
[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,
face_color: str | Inherit = INHERIT,
edge_color: str | Inherit = INHERIT,
marker_size: float | Inherit = INHERIT,
marker_edge_width: float | Inherit = INHERIT,
marker_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
) -> 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.
face_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.
Typical range is ``10`` to ``100``.
Default depends on the ``figure_style`` configuration.
marker_edge_width : float
Width of the point edge.
Typical range is ``0`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
marker_style : str
Style of the point.
Common values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``,
``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, ``"_"``,
``"P"``, ``"X"``, ``"None"``, ``" "``, and ``""``.
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.
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)``).
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,
face_color=face_color,
edge_color=edge_color,
marker_size=marker_size,
marker_style=marker_style,
edge_width=marker_edge_width,
alpha=alpha,
)
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,
face_color: str | Inherit = INHERIT,
edge_color: str | Inherit = INHERIT,
marker_size: float | Inherit = INHERIT,
marker_edge_width: float | Inherit = INHERIT,
marker_style: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
) -> 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.
face_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.
Typical range is ``10`` to ``100``.
Default depends on the ``figure_style`` configuration.
marker_edge_width : float
Width of the point edge.
Typical range is ``0`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
marker_style : str
Style of the point.
Common values include ``"."``, ``","``, ``"o"``, ``"v"``, ``"^"``, ``"<"``, ``">"``, ``"s"``,
``"p"``, ``"*"``, ``"h"``, ``"H"``, ``"+"``, ``"x"``, ``"D"``, ``"d"``, ``"|"``, ``"_"``,
``"P"``, ``"X"``, ``"None"``, ``" "``, and ``""``.
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.
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)``).
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,
face_color=face_color,
edge_color=edge_color,
marker_size=marker_size,
marker_style=marker_style,
edge_width=marker_edge_width,
alpha=alpha,
)
for coord in coords
]
return points
[docs]
def to_desmos(self, decimal_precision: int = 2, to_clipboard: bool = False) -> str:
"""
Gives the data points in a Desmos-readable format. The outputted string can then be pasted into a single Desmos
cell and the object's data will be displayed.
Parameters
----------
decimal_precision : int, optional
Specifies the number of decimals of the formatted points.
Defaults to 2.
to_clipboard : bool, optional
Specifies whether the points should be directly copied to the user's clipboard in addition to being
returned as a string.
Defaults to False.
Returns
-------
formatted points : str
A list of tuples representing every data point.
"""
formatted_points = super().to_desmos(
self._x_data, self._y_data, decimal_precision
)
if to_clipboard:
copy_to_clipboard(formatted_points)
return formatted_points
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 is_inherit(self._face_color):
# 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 is_inherit(self._edge_color):
# 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)
# Sets the data range that the color map will cover.
if self._color_map_range:
norm = Normalize(
vmin=min(self._color_map_range), vmax=max(self._color_map_range)
)
else: # Calculate from the array of intensities
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)
# Sets the data range that the color map will cover.
if self._color_map_range:
norm = Normalize(
vmin=min(self._color_map_range), vmax=max(self._color_map_range)
)
else: # Calculate from the array of intensities
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,
"linewidth": self._marker_edge_width,
"alpha": self._alpha,
}
params = {k: v for k, v in params.items() if v != INHERIT}
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 is_inherit(self._errorbars_color):
# 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 = 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 != INHERIT}
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 is_inherit(self._face_color)
and not isinstance(self.face_color, str)
):
# Create color bar from face color intensities
color_map_name = (
plt.rcParams["image.cmap"]
if is_inherit(self._color_map)
else self._color_map
)
color_map = plt.get_cmap(color_map_name)
# Sets the data range that the color map on the color bar will cover.
# Otherwise, it will be calculated from the array of intensities.
if self._color_map_range:
norm = Normalize(
vmin=min(self._color_map_range), vmax=max(self._color_map_range)
)
else:
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, **self._color_bar_params)
if (
self._show_color_bar
and self._edge_color is not None
and not is_inherit(self._edge_color)
and not isinstance(self.edge_color, str)
):
# Create color bar from edge color intensities
color_map_name = (
plt.rcParams["image.cmap"]
if is_inherit(self._color_map)
else self._color_map
)
color_map = plt.get_cmap(color_map_name)
# Sets the data range that the color map on the color bar will cover.
# Otherwise, it will be calculated from the array of intensities.
if self._color_map_range:
norm = Normalize(
vmin=min(self._color_map_range), vmax=max(self._color_map_range)
)
else:
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, **self._color_bar_params)
[docs]
@dataclass
class Histogram(Plottable1D):
"""
This class implements a general histogram.
Parameters
----------
data : ArrayLike
Array of values to be plotted.
bins : int | ArrayLike
If `bins` is an integer, it defines the number of equal_width bins to be used in the histogram.
If `bins` is an array, it defines the bin edges to be used in the histogram, including the left edge of the
first bin and the right edge of the last bin.
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.
Values are ``"bar"``, ``"barstacked"``, ``"step"``, and ``"stepfilled"``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the histogram.
Range is ``0`` (transparent) to ``1`` (opaque).
Default depends on the ``figure_style`` configuration.
line_width : float
Width of the histogram edge.
Typical range is ``0.5`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
normalize : bool
Whether or not to normalize the histogram.
Default depends on the ``figure_style`` configuration.
orientation: str
Whether to plot the histogram on x-axis or on y-axis.
Values are ``"vertical"`` and ``"horizontal"``.
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.
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,
data: ArrayLike,
bins: int,
label: Optional[str] = None,
face_color: str | Inherit = INHERIT,
edge_color: str | Inherit = INHERIT,
hist_type: str | Inherit = INHERIT,
alpha: float | Inherit = INHERIT,
line_width: float | Inherit = INHERIT,
normalize: bool | Inherit = INHERIT,
orientation: str | Inherit = INHERIT,
show_params: bool | Inherit = INHERIT,
) -> None:
"""
This class implements a general histogram.
Parameters
----------
data : ArrayLike
Array of values to be plotted.
bins : int | ArrayLike
If `bins` is an integer, it defines the number of equal_width bins to be used in the histogram.
If `bins` is an array, it defines the bin edges to be used in the histogram, including the left edge of the
first bin and the right edge of the last bin.
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.
Values are ``"bar"``, ``"barstacked"``, ``"step"``, and ``"stepfilled"``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the histogram.
Range is ``0`` (transparent) to ``1`` (opaque).
Default depends on the ``figure_style`` configuration.
line_width : float
Width of the histogram edge.
Typical range is ``0.5`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
normalize : bool
Whether or not to normalize the histogram.
Default depends on the ``figure_style`` configuration.
orientation: str
Whether to plot the histogram on x-axis or on y-axis.
Values are ``"vertical"`` and ``"horizontal"``.
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.
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._bins = 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._orientation = orientation
self._show_params = show_params
self.data = np.asarray(data)
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
[docs]
@classmethod
def from_fit_residuals(
cls,
fit: Fit,
bins: int,
label: Optional[str] = None,
face_color: str | Inherit = INHERIT,
edge_color: str | Inherit = INHERIT,
hist_type: str | Inherit = INHERIT,
alpha: int | Inherit = INHERIT,
line_width: int | Inherit = INHERIT,
normalize: bool | Inherit = INHERIT,
orientation: str | Inherit = INHERIT,
show_params: bool | Inherit = INHERIT,
) -> 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.
bins : int | ArrayLike
If `bins` is an integer, it defines the number of equal_width bins to be used in the histogram.
If `bins` is an array, it defines the bin edges to be used in the histogram, including the left edge of the
first bin and the right edge of the last bin.
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.
Values are ``"bar"``, ``"barstacked"``, ``"step"``, and ``"stepfilled"``.
Default depends on the ``figure_style`` configuration.
alpha : float
Opacity of the histogram.
Range is ``0`` (transparent) to ``1`` (opaque).
Default depends on the ``figure_style`` configuration.
line_width : float
Width of the histogram edge.
Typical range is ``0.5`` to ``3`` points.
Default depends on the ``figure_style`` configuration.
normalize : bool
Whether or not to normalize the histogram.
Default depends on the ``figure_style`` configuration.
orientation: str
Whether to plot the histogram on x-axis or on y-axis.
Values are ``"vertical"`` and ``"horizontal"``.
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.
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)``).
Returns
-------
A :class:`~graphinglib.data_plotting_1d.Histogram` object created from the residuals of a fit.
"""
residuals = fit.get_residuals()
return cls(
residuals,
bins,
label,
face_color,
edge_color,
hist_type,
alpha,
line_width,
normalize,
orientation,
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._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 bins(self) -> int:
return self._bins
@bins.setter
def bins(self, bins: int) -> None:
self._bins = bins
_parameters = np.histogram(self._data, bins=self._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._get_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 orientation(self) -> str:
return self._orientation
@orientation.setter
def orientation(self, orientation: str) -> None:
self._orientation = orientation
@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 __eq__(self, other: Self) -> bool:
"""
Defines the equality between two histograms.
"""
return (
np.equal(self.bin_heights, other.bin_heights).all()
and np.equal(self.bin_centers, other.bin_centers).all()
)
def _get_label(self) -> None:
"""
Gives the label of the histogram (with or without parameters).
"""
lab = self._label
if lab and self._show_params:
lab += (
" :\n"
+ rf"$\mu$ = {0 if abs(self._mean) < 1e-3 else self._mean:.3f}, $\sigma$ = {self._standard_deviation:.3f}"
)
elif self._show_params:
lab = rf"$\mu$ = {0 if abs(self._mean) < 1e-3 else self._mean:.3f}, $\sigma$ = {self._standard_deviation:.3f}"
return 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 | Inherit = INHERIT,
show_std: bool | Inherit = INHERIT,
curve_color: str | Inherit = INHERIT,
mean_color: str | Inherit = INHERIT,
std_color: str | Inherit = INHERIT,
) -> 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.
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)``).
"""
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
[docs]
def to_desmos(self, decimal_precision: int = 2, to_clipboard: bool = False) -> str:
"""
Gives every bin's upper center in a Desmos-readable format. The outputted string can then be pasted into a
single Desmos cell and the object's data will be displayed.
Parameters
----------
decimal_precision : int, optional
Specifies the number of decimals of the formatted points.
Defaults to 2.
to_clipboard : bool, optional
Specifies whether the points should be directly copied to the user's clipboard in addition to being
returned as a string.
Defaults to False.
Returns
-------
formatted points : str
A list of tuples representing every data point.
"""
formatted_points = super().to_desmos(
self.bin_centers, self.bin_heights, decimal_precision
)
if to_clipboard:
copy_to_clipboard(formatted_points)
return formatted_points
def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs) -> None:
"""
Plots the element in the specified axes.
"""
params = {
"facecolor": (
to_rgba(self._face_color, self._alpha)
if self._face_color != INHERIT and self._alpha != INHERIT
else INHERIT
),
"edgecolor": (
to_rgba(self._edge_color, 1)
if self._edge_color != INHERIT
else self._edge_color
),
"linewidth": self._line_width,
}
params = {k: v for k, v in params.items() if v != INHERIT}
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 != INHERIT and self._alpha != INHERIT
else INHERIT
),
"edgecolor": (
to_rgba(self._edge_color, 1)
if self._edge_color != INHERIT
else self._edge_color
),
"histtype": self._hist_type,
"linewidth": self._line_width,
"density": self._normalize,
"orientation": self._orientation,
}
params = {k: v for k, v in params.items() if v != INHERIT}
axes.hist(
self._data,
bins=self._bins,
label=self.label, # uses the get_label() method
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 != INHERIT}
# Plots pdf on the y-axis if "orientation" is "horizontal".
if self._orientation != "vertical":
axes.plot(
y_data,
x_data,
zorder=z_order,
**params,
)
else:
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 != INHERIT:
params["colors"] = [self._pdf_std_color, self._pdf_std_color]
# Plots std on the y-axis if "orientation" is "horizontal".
if self._orientation != "vertical":
plt.hlines(
[
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,
)
else:
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 != INHERIT:
params["colors"] = [self._pdf_mean_color]
# Plots std on the y-axis if "orientation" is "horizontal".
if self._orientation != "vertical":
plt.hlines(
self._mean,
0,
curve_max_y,
linestyles=["dashed"],
zorder=z_order - 1,
**params,
)
else:
plt.vlines(
self._mean,
0,
curve_max_y,
linestyles=["dashed"],
zorder=z_order - 1,
**params,
)