Source code for data_morph.shapes.circles

"""Shapes that are circular in nature."""

from __future__ import annotations

from numbers import Number

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Axes

from ..data.dataset import Dataset
from ..plotting.style import plot_with_custom_style
from .bases.shape import Shape


[docs] class Circle(Shape): """ Class representing a hollow circle. .. plot:: :scale: 75 :caption: This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader from data_morph.shapes.circles import Circle _ = Circle(DataLoader.load_dataset('panda')).plot() Parameters ---------- dataset : Dataset The starting dataset to morph into other shapes. radius : numbers.Number, optional The radius of the circle. """ def __init__(self, dataset: Dataset, radius: Number | None = None) -> None: self.center: np.ndarray = dataset.df[['x', 'y']].mean().to_numpy() """numpy.ndarray: The (x, y) coordinates of the circle's center.""" self.radius: Number = radius or dataset.df[['x', 'y']].std().mean() * 1.5 """numbers.Number: The radius of the circle.""" def __repr__(self) -> str: x, y = self.center return f'<{self.__class__.__name__} center={(float(x), float(y))} radius={self.radius}>'
[docs] def distance(self, x: Number, y: Number) -> float: """ Calculate the absolute distance between this circle's edge and a point (x, y). Parameters ---------- x, y : numbers.Number Coordinates of a point in 2D space. Returns ------- float The absolute distance between this circle's edge and the point (x, y). """ return abs( self._euclidean_distance(self.center, np.array([x, y])) - self.radius )
[docs] @plot_with_custom_style def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. Parameters ---------- ax : matplotlib.axes.Axes, optional An optional :class:`~matplotlib.axes.Axes` object to plot on. Returns ------- matplotlib.axes.Axes The :class:`~matplotlib.axes.Axes` object containing the plot. """ if not ax: fig, ax = plt.subplots(layout='constrained') fig.get_layout_engine().set(w_pad=0.2, h_pad=0.2) _ = ax.axis('equal') _ = ax.add_patch(plt.Circle(self.center, self.radius, ec='k', fill=False)) _ = ax.autoscale() return ax
[docs] class Rings(Shape): """ Class representing rings comprising multiple concentric circles. .. plot:: :scale: 75 :caption: This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader from data_morph.shapes.circles import Rings _ = Rings(DataLoader.load_dataset('panda')).plot() Parameters ---------- dataset : Dataset The starting dataset to morph into other shapes. num_rings : int, default 4 The number of rings to include. Must be greater than 1. See Also -------- Circle : The individual rings are represented as circles. """ def __init__(self, dataset: Dataset, num_rings: int = 4) -> None: if not isinstance(num_rings, int): raise TypeError('num_rings must be an integer') if num_rings <= 1: raise ValueError('num_rings must be greater than 1') stdev = dataset.df.std().mean() self.circles: list[Circle] = [ Circle(dataset, r) for r in np.linspace(stdev / num_rings * 2, stdev * 2, num_rings) ] """list[Circle]: The individual rings represented by :class:`Circle` objects.""" self._centers = np.array([circle.center for circle in self.circles]) self._radii = np.array([circle.radius for circle in self.circles]) def __repr__(self) -> str: return self._recursive_repr('circles')
[docs] def distance(self, x: Number, y: Number) -> float: """ Calculate the minimum absolute distance between any of this shape's circles' edges and a point (x, y). Parameters ---------- x, y : numbers.Number Coordinates of a point in 2D space. Returns ------- float The minimum absolute distance between any of this shape's circles' edges and the point (x, y). See Also -------- Circle.distance : Rings consists of multiple circles, so we use the minimum distance to one of the circles. """ point = np.array([x, y]) return np.min( np.abs(np.linalg.norm(self._centers - point, axis=1) - self._radii) )
[docs] @plot_with_custom_style def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. Parameters ---------- ax : matplotlib.axes.Axes, optional An optional :class:`~matplotlib.axes.Axes` object to plot on. Returns ------- matplotlib.axes.Axes The :class:`~matplotlib.axes.Axes` object containing the plot. """ for circle in self.circles: ax = circle.plot(ax) return ax
[docs] class Bullseye(Rings): """ Class representing a bullseye shape comprising two concentric circles. .. plot:: :scale: 75 :caption: This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader from data_morph.shapes.circles import Bullseye _ = Bullseye(DataLoader.load_dataset('panda')).plot() Parameters ---------- dataset : Dataset The starting dataset to morph into other shapes. See Also -------- Rings : The Bullseye is a special case where we only have 2 rings. """ def __init__(self, dataset: Dataset) -> None: super().__init__(dataset=dataset, num_rings=2)