Source code for graphinglib.shapes

from .inherit import INHERIT, Inherit

from copy import deepcopy
from dataclasses import dataclass
from typing import Literal, Optional

import matplotlib.pyplot as plt
import numpy as np
import shapely as sh
import shapely.ops as ops
from matplotlib.patches import Polygon as MPLPolygon
from shapely import LineString
from shapely import Polygon as ShPolygon

from .data_plotting_1d import Curve
from .graph_elements import Plottable, Point

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


[docs] @dataclass class Arrow(Plottable): """This class implements an arrow object. Parameters ---------- pointA : tuple[float, float] Point A of the arrow. If the arrow is single-sided, refers to the tail. pointB : tuple[float, float] Point B of the arrow. If the arrow is single-sided, refers to the head. color : str Color of the arrow. Default depends on the ``figure_style`` configuration. width : float, optional Arrow line width. Typical range is ``0.5`` to ``4``. Default depends on the ``figure_style`` configuration. head_size : float, optional Scales the size of the arrow head. Typical range is ``0.5`` to ``3``. Default depends on the ``figure_style`` configuration. style : ``Literal["->", "-|>", "-[", "]->", "simple", "fancy", "wedge"] | Inherit``, optional The style of the arrow. For a visual explanation of all available styles, see the gallery `Arrow Styles <https://graphinglib.org/latest/examples/arrow_styles.html>`_ example. Values are ``"->"``, ``"-|>"``, ``"-["``, ``"]->"``, ``"simple"``, ``"fancy"``, and ``"wedge"``. Default depends on the ``figure_style`` configuration. .. warning:: The ``style`` parameter differs slightly from matplotlib's ``arrowstyle`` since GraphingLib also uses a ``two_sided`` parameter to explicitly define whether the arrow is single or double-sided. shrink : float Fraction of the total length of the arrow to shrink from both ends. Range is ``0`` to ``0.5``. A value of ``0.5`` means the arrow is no longer visible. Defaults to 0. alpha : float Opacity of the arrow. Range is ``0`` (transparent) to ``1`` (opaque). Default depends on the ``figure_style`` configuration. two_sided : bool If ``True``, an arrow is shown at both head and tail. Defaults to ``False``. 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, pointA: tuple[float, float], pointB: tuple[float, float], color: str | Inherit = INHERIT, width: float | Inherit = INHERIT, head_size: float | Inherit = INHERIT, shrink: float = 0, style: Literal["->", "-|>", "-[", "]->", "simple", "fancy", "wedge"] | Inherit = INHERIT, alpha: float | Inherit = INHERIT, two_sided: bool = False, ): """This class implements an arrow object. Parameters ---------- pointA : tuple[float, float] Point A of the arrow. If the arrow is single-sided, refers to the tail. pointB : tuple[float, float] Point B of the arrow. If the arrow is double-sided, refers to the head. color : str Color of the arrow. Default depends on the ``figure_style`` configuration. width : float, optional Arrow line width. Typical range is ``0.5`` to ``4``. Default depends on the ``figure_style`` configuration. head_size : float, optional Scales the size of the arrow head. Typical range is ``0.5`` to ``3``. Default depends on the ``figure_style`` configuration. style : ``Literal["->", "-|>", "-[", "]->", "simple", "fancy", "wedge"] | Inherit``, optional The style of the arrow. For a visual explanation of all available styles, see the gallery `Arrow Styles <https://graphinglib.org/latest/examples/arrow_styles.html>`_ example. Values are ``"->"``, ``"-|>"``, ``"-["``, ``"]->"``, ``"simple"``, ``"fancy"``, and ``"wedge"``. Default depends on the ``figure_style`` configuration. .. warning:: The ``style`` parameter differs slightly from matplotlib's ``arrowstyle`` since GraphingLib also uses a ``two_sided`` parameter to explicitly define whether the arrow is single or double-sided. shrink : float Fraction of the total length of the arrow to shrink from both ends. Range is ``0`` to ``0.5``. A value of ``0.5`` means the arrow is no longer visible. Defaults to 0. alpha : float Opacity of the arrow. Range is ``0`` (transparent) to ``1`` (opaque). Default depends on the ``figure_style`` configuration. two_sided : bool If ``True``, the arrow is double-sided. Defaults to ``False``. 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._pointA = pointA self._pointB = pointB self._color = color self._width = width self._head_size = head_size self._shrink = shrink self.style = style self._alpha = alpha self._two_sided = two_sided
@property def pointA(self): return self._pointA @pointA.setter def pointA(self, value): self._pointA = value @property def pointB(self): return self._pointB @pointB.setter def pointB(self, value): self._pointB = value @property def color(self): return self._color @color.setter def color(self, value): self._color = value @property def width(self): return self._width @width.setter def width(self, value): self._width = value @property def head_size(self): return self._head_size @head_size.setter def head_size(self, value): self._head_size = value @property def shrink(self): return self._shrink @shrink.setter def shrink(self, value): self._shrink = value @property def style(self): return self._style @style.setter def style(self, value): if value not in [ "->", "-|>", "-[", "]->", "simple", "fancy", "wedge", INHERIT, ]: raise ValueError( "Invalid head style. Valid options are: '->', '-|>', '-[', ']->', 'simple', 'fancy', " "'wedge', or INHERIT." ) self._style = value @property def alpha(self): return self._alpha @alpha.setter def alpha(self, value): self._alpha = value @property def two_sided(self): return self._two_sided @two_sided.setter def two_sided(self, value): self._two_sided = value def _shrink_points(self): x_length, y_length = ( self._pointA[0] - self._pointB[0], self._pointA[1] - self._pointB[1], ) newA = ( self._pointB[0] + (1 - self._shrink) * x_length, self._pointB[1] + (1 - self._shrink) * y_length, ) newB = ( self._pointA[0] - (1 - self._shrink) * x_length, self._pointA[1] - (1 - self._shrink) * y_length, ) return newA, newB
[docs] def copy(self) -> Self: """ Returns a deep copy of the :class:`~graphinglib.shapes.Arrow` object. """ return deepcopy(self)
def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs): if self._two_sided: match self._style: case "->": style = "<->" case "-|>": style = "<|-|>" case "-[": style = "]-[" case _: raise ValueError( "The head style must be '->', '-|>' or '-[' for two-sided arrows." ) else: style = self._style if self._head_size != INHERIT: head_length, head_width = self._head_size * 0.4, self._head_size * 0.2 # Set specific arrow properties match self._style: case "->" | "-|>" | "simple" | "fancy": prop_style_values = ( f"head_width={head_width}, head_length={head_length}" ) case "-[": prop_style_values = f"widthB={head_width}" + ( f", widthA={head_width}" if self._two_sided else "" ) case "]->": prop_style_values = f"widthA={head_width}" case "wedge": prop_style_values = f"tail_width={head_width}" arrow_style = f"{style}, {prop_style_values}" else: arrow_style = style props = { "arrowstyle": arrow_style, "color": self._color, "linewidth": self._width, "alpha": self._alpha, } props = {k: v for k, v in props.items() if v != INHERIT} if self._shrink != 0: shrinkPointA, shrinkPointB = self._shrink_points() axes.annotate( "", shrinkPointB, shrinkPointA, zorder=z_order, arrowprops=props, ) else: axes.annotate( "", self._pointB, self._pointA, zorder=z_order, arrowprops=props, )
[docs] @dataclass class Line(Plottable): """This class implements a line object. Parameters ---------- pointA : tuple[float, float] Point A of the line. pointB : tuple[float, float] Point B of the line. color : str Color of the line. Default depends on the ``figure_style`` configuration. width : float, optional Line width. Typical range is ``0.5`` to ``4``. Default depends on the ``figure_style`` configuration. capped_line : bool If ``True``, the line is capped on both ends. Defaults to ``False``. cap_width : float Width of the caps. Typical range is ``0.5`` to ``4``. Default depends on the ``figure_style`` configuration. alpha : float Opacity of the line. 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)``). """ _pointA: tuple[float, float] _pointB: tuple[float, float] _color: str | Inherit = INHERIT _width: float | Inherit = INHERIT _capped_line: bool = False _cap_width: float | Inherit = INHERIT _alpha: float | Inherit = INHERIT
[docs] def __init__( self, pointA: tuple[float, float], pointB: tuple[float, float], color: str | Inherit = INHERIT, width: float | Inherit = INHERIT, capped_line: bool = False, cap_width: float | Inherit = INHERIT, alpha: float | Inherit = INHERIT, ): self._pointA = pointA self._pointB = pointB self._color = color self._width = width self._capped_line = capped_line self._cap_width = cap_width self._alpha = alpha
@property def pointA(self) -> tuple[float, float]: return self._pointA @pointA.setter def pointA(self, value: tuple[float, float]): self._pointA = value @property def pointB(self) -> tuple[float, float]: return self._pointB @pointB.setter def pointB(self, value: tuple[float, float]): self._pointB = value @property def color(self) -> str: return self._color @color.setter def color(self, value: str): self._color = value @property def width(self) -> float: return self._width @width.setter def width(self, value: float): self._width = value @property def capped_line(self) -> bool: return self._capped_line @capped_line.setter def capped_line(self, value: bool): self._capped_line = value @property def cap_width(self) -> float: return self._cap_width @cap_width.setter def cap_width(self, value: float): self._cap_width = value @property def alpha(self) -> float: return self._alpha @alpha.setter def alpha(self, value: float): self._alpha = value
[docs] def copy(self) -> Self: """ Returns a deep copy of the :class:`~graphinglib.shapes.Line` object. """ return deepcopy(self)
def _plot_element(self, axes: plt.axes, z_order: int, **kwargs): if self._capped_line: style = f"|-|, widthA={self._cap_width / 2}, widthB={self._cap_width / 2}" else: style = "-" props = { "arrowstyle": style, "color": self._color, "linewidth": self._width, "alpha": self._alpha, } props = {k: v for k, v in props.items() if v != INHERIT} axes.annotate( "", self._pointA, self._pointB, zorder=z_order, arrowprops=props, )
[docs] class Polygon(Plottable): """This class implements a Polygon object. Parameters ---------- vertices : list[tuple[float, float]] List of coordinates that define the polygon. fill : bool, optional Whether the polygon should be filled or not. Default depends on the ``figure_style`` configuration. edge_color : str, optional The color of the polygon's edge. Default depends on the ``figure_style`` configuration. fill_color : str, optional The color of the polygon's fill. Default depends on the ``figure_style`` configuration. line_width : float, optional The width of the line. Typical range is ``0.5`` to ``4``. Default depends on the ``figure_style`` configuration. line_style : str, optional The style of the line. Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and ``"dotted"``. Default depends on the ``figure_style`` configuration. fill_alpha : float, optional The alpha value of the fill. 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, vertices: list[tuple[float, float]], fill: bool | Inherit = INHERIT, edge_color: str | Inherit = INHERIT, fill_color: str | Inherit = INHERIT, line_width: float | Inherit = INHERIT, line_style: str | Inherit = INHERIT, fill_alpha: float | Inherit = INHERIT, ): self._fill = fill self._edge_color = edge_color self._fill_color = fill_color self._line_width = line_width self._line_style = line_style self._fill_alpha = fill_alpha self._sh_polygon = ShPolygon(vertices)
@property def fill(self): return self._fill @fill.setter def fill(self, value): self._fill = value @property def fill_color(self): return self._fill_color @fill_color.setter def fill_color(self, value): self._fill_color = value @property def edge_color(self): return self._edge_color @edge_color.setter def edge_color(self, value): self._edge_color = value @property def line_width(self): return self._line_width @line_width.setter def line_width(self, value): self._line_width = value @property def line_style(self): return self._line_style @line_style.setter def line_style(self, value): self._line_style = value @property def fill_alpha(self): return self._fill_alpha @fill_alpha.setter def fill_alpha(self, value): self._fill_alpha = value @property def vertices(self): return np.array(self._sh_polygon.exterior.coords) @vertices.setter def vertices(self, value): self._sh_polygon = ShPolygon(value) @property def area(self): return self._sh_polygon.area @property def perimeter(self): return self._sh_polygon.length def __contains__(self, point: Point) -> bool: return self._sh_polygon.contains(sh.geometry.Point(point._x, point._y))
[docs] def copy(self) -> Self: """ Returns a deep copy of the :class:`~graphinglib.shapes.Polygon` object. """ return deepcopy(self)
[docs] def get_centroid_coordinates(self) -> tuple[float, float]: """Returns the center coordinates of the polygon. Returns ------- tuple[float, float] The center coordinates of the polygon. """ return self._sh_polygon.centroid.coords[0]
[docs] def create_centroid_point(self) -> Point: """Returns the center point of the polygon. Returns ------- :class:`~graphinglib.graph_elements.Point` The center point of the polygon. """ return Point(*self.get_centroid_coordinates())
[docs] def create_intersection(self, other: Self, copy_style: bool = False) -> Self: """ Returns the intersection of the polygon with another polygon. Parameters ---------- other : :class:`~graphinglib.shapes.Polygon` The other polygon. copy_style : bool, optional If ``True``, the current polygon's parameters are copied to the new polygon. If ``False``, the new polygon will have default parameters. Default is ``False``. Returns ------- :class:`~graphinglib.shapes.Polygon` The intersection of the two polygons. """ if copy_style: new_poly = self.copy() new_poly._sh_polygon = self._sh_polygon.intersection(other._sh_polygon) return new_poly else: return Polygon( list(self._sh_polygon.intersection(other._sh_polygon).exterior.coords) )
[docs] def create_union(self, other: Self, copy_style: bool = False) -> Self: """ Returns the union of the polygon with another polygon. Parameters ---------- other : :class:`~graphinglib.shapes.Polygon` The other polygon. copy_style : bool, optional If ``True``, the current polygon's parameters are copied to the new polygon. If ``False``, the new polygon will have default parameters. Default is ``False``. Returns ------- :class:`~graphinglib.shapes.Polygon` The union of the two polygons. """ if copy_style: new_poly = self.copy() new_poly._sh_polygon = self._sh_polygon.union(other._sh_polygon) return new_poly else: return Polygon( list(self._sh_polygon.union(other._sh_polygon).exterior.coords) )
[docs] def create_difference(self, other: Self, copy_style: bool = False) -> Self: """ Returns the difference of the polygon with another polygon. Parameters ---------- other : :class:`~graphinglib.shapes.Polygon` The other polygon to subtract from the current polygon. copy_style : bool, optional If ``True``, the current polygon's parameters are copied to the new polygon. If ``False``, the new polygon will have default parameters. Default is ``False``. Returns ------- :class:`~graphinglib.shapes.Polygon` The difference of the two polygons. """ if copy_style: new_poly = self.copy() new_poly._sh_polygon = self._sh_polygon.difference(other._sh_polygon) return new_poly else: return Polygon( list(self._sh_polygon.difference(other._sh_polygon).exterior.coords) )
[docs] def create_symmetric_difference( self, other: Self, copy_style: bool = False ) -> list[Self]: """ Returns the symmetric difference of the polygon with another polygon. In general, this can create more than one polygon, so the result is returned as a list of polygons even if there is only one. Parameters ---------- other : :class:`~graphinglib.shapes.Polygon` The other polygon to find the symmetric difference with. copy_style : bool, optional If ``True``, the current polygon's parameters are copied to the new polygon. If ``False``, the new polygon will have default parameters. Default is ``False``. Returns ------- list[:class:`~graphinglib.shapes.Polygon`] A list of polygons resulting from the symmetric difference. """ if copy_style: new_poly = self.copy() new_poly._sh_polygon = self._sh_polygon.symmetric_difference( other._sh_polygon ) return new_poly else: multi_poly = self._sh_polygon.symmetric_difference(other._sh_polygon) if multi_poly.geom_type == "MultiPolygon": return [ Polygon(list(p.exterior.coords)) for p in list(multi_poly.geoms) ] else: return [Polygon(list(multi_poly.exterior.coords))]
[docs] def translate(self, dx: float, dy: float) -> Self | None: """ Translates the polygon by the specified amount. Parameters ---------- dx : float The amount to move the polygon in the x direction. dy : float The amount to move the polygon in the y direction. """ self._sh_polygon = sh.affinity.translate(self._sh_polygon, xoff=dx, yoff=dy)
[docs] def rotate( self, angle: float, center: Optional[tuple[float, float]] = None, use_rad: bool = False, ) -> Self: """ Rotates the polygon by the specified angle. Parameters ---------- angle : float The angle by which to rotate the polygon (in degrees by default). center : tuple[float, float], optional The center of rotation. If not specified, the centroid of the polygon is used. use_rad : bool, optional Set to ``True`` if the angle is in radians instead of degrees. Default is ``False``. """ if center is None: center = self.get_centroid_coordinates() # Use shapely.affinity.rotate to rotate the polygon self._sh_polygon = sh.affinity.rotate( self._sh_polygon, angle, origin=center, use_radians=use_rad )
[docs] def scale( self, x_scale: float, y_scale: float, center: Optional[tuple[float, float]] = None, ) -> Self: """ Scales the polygon by the specified factors. Parameters ---------- x_scale : float The factor by which to scale the polygon in the x direction. y_scale : float The factor by which to scale the polygon in the y direction. center : tuple[float, float], optional The center of the scaling. If not specified, the centroid of the polygon is used. """ if center is None: center = self.get_centroid_coordinates() # Use shapely.affinity.scale to scale the polygon self._sh_polygon = sh.affinity.scale( self._sh_polygon, xfact=x_scale, yfact=y_scale, origin=center )
[docs] def skew( self, x_skew: float, y_skew: float, center: Optional[tuple[float, float]] = None, use_rad: bool = False, ) -> Self: """ Skews the polygon by the specified factors. Parameters ---------- x_skew : float The factor by which to skew the polygon in the x direction. y_skew : float The factor by which to skew the polygon in the y direction. center : tuple[float, float], optional The center of the skewing. If not specified, the centroid of the polygon is used. use_rad : bool, optional Set to ``True`` if the skewing factors are in radians instead of degrees. Default is ``False``. """ if center is None: center = self.get_centroid_coordinates() # Use shapely.affinity.skew to skew the polygon self._sh_polygon = sh.affinity.skew( self._sh_polygon, xs=x_skew, ys=y_skew, origin=center, use_radians=use_rad )
[docs] def split(self, curve: Curve, copy_style: bool = False) -> list[Self]: """ Splits the polygon by a curve. Parameters ---------- curve : :class:`~graphinglib.data_plotting_1d.Curve` The curve to split the polygon by. copy_style : bool, optional If ``True``, the current polygon's parameters are copied to the new polygons. If ``False``, the new polygons will have default parameters. Default is ``False``. Returns ------- list[:class:`~shapely.geometry.polygon.Polygon`] The list of polygons resulting from the split. """ if not isinstance(curve, Curve): raise TypeError("The curve must be a Curve object") sh_curve = LineString([(x, y) for x, y in zip(curve._x_data, curve._y_data)]) split_sh_polygons = ops.split(self._sh_polygon, sh_curve) split_sh_polygons = [ p.simplify(0.001 * p.length) for p in list(split_sh_polygons.geoms) ] polygons = [Polygon(list(p.exterior.coords)) for p in split_sh_polygons] if copy_style: for polygon in polygons: polygon._fill = self._fill polygon._fill_color = self._fill_color polygon._edge_color = self._edge_color polygon._line_width = self._line_width polygon._line_style = self._line_style polygon._fill_alpha = self._fill_alpha return polygons
[docs] def linear_transformation(self, matrix: np.ndarray) -> Self: """ Applies a transformation matrix to the polygon. Parameters ---------- transform : numpy.ndarray The transformation matrix to apply. The matrix should be a 2x2 matrix for 2D transformations. """ new_points = np.dot(self.vertices, matrix) self._sh_polygon = ShPolygon(new_points)
[docs] def get_intersection_coordinates(self, other: Self) -> list[tuple[float, float]]: """ Returns the coordinates of the intersection points of the borders of the two polygons. Parameters ---------- other : :class:`~graphinglib.shapes.Polygon` The other polygon. Returns ------- list[tuple[float, float]] The coordinates of the intersection of the two polygons. """ intersection = self._sh_polygon.boundary.intersection( other._sh_polygon.boundary ) return [(p.x, p.y) for p in intersection.geoms]
[docs] def create_intersection_points(self, other: Self | Curve) -> list[Point]: """ Returns the intersection points of the borders of the two polygons. Parameters ---------- other : :class:`~graphinglib.shapes.Polygon` or :class:`~graphinglib.data_plotting_1d.Curve` The other polygon. Returns ------- list[:class:`~graphinglib.graph_elements.Point`] The intersection points of the two polygons. """ if isinstance(other, Curve): # create curve points from the x_data and y_data of the curve other_points = [(x, y) for x, y in zip(other._x_data, other._y_data)] other_boundary = LineString(other_points) intersection = self._sh_polygon.boundary.intersection(other_boundary) return [Point(p.x, p.y) for p in intersection.geoms] elif isinstance(other, Polygon): intersection = self._sh_polygon.boundary.intersection( other._sh_polygon.boundary ) return [Point(p.x, p.y) for p in intersection.geoms] else: raise TypeError("The other object must be a Polygon or a Curve")
def _plot_element(self, axes: plt.Axes, z_order: int, **kwargs): # Create a polygon patch for the fill if self._fill: params = { "alpha": self._fill_alpha, "zorder": z_order, } if self._fill_color is not None: params["facecolor"] = self._fill_color params = {k: v for k, v in params.items() if v != INHERIT} polygon_fill = MPLPolygon(self.vertices, **params) axes.add_patch(polygon_fill) # Create a polygon patch for the edge if self._edge_color is not None: params = { "fill": None, "linewidth": self._line_width, "linestyle": self._line_style, "edgecolor": self._edge_color, "zorder": z_order, } params = {k: v for k, v in params.items() if v != INHERIT} polygon_edge = MPLPolygon(self.vertices, **params) axes.add_patch(polygon_edge)
[docs] @dataclass class Circle(Polygon): """This class implements a Circle object with a given center and radius. Parameters ---------- x_center : float The x coordinate of the :class:`~graphinglib.shapes.Circle`. y_center : float The y coordinate of the :class:`~graphinglib.shapes.Circle`. radius : float The radius of the :class:`~graphinglib.shapes.Circle`. fill : bool, optional Whether the circle should be filled or not. Default depends on the ``figure_style`` configuration. fill_color : str, optional The color of the circle's fill. Default depends on the ``figure_style`` configuration. edge_color : str, optional The color of the circle's edge. Default depends on the ``figure_style`` configuration. line_width : float, optional The width of the line. Typical range is ``0.5`` to ``4``. Default depends on the ``figure_style`` configuration. line_style : str, optional The style of the line. Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and ``"dotted"``. Default depends on the ``figure_style`` configuration. fill_alpha : float, optional The alpha value of the fill. Range is ``0`` (transparent) to ``1`` (opaque). Default depends on the ``figure_style`` configuration. number_of_points : int, optional The number of points to use to approximate the circle. Default is 100 (covers approximately 99.9% of perfect circle area). 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_center: float, y_center: float, radius: float, fill: bool | Inherit = INHERIT, fill_color: str | Inherit = INHERIT, edge_color: str | Inherit = INHERIT, line_width: float | Inherit = INHERIT, line_style: str | Inherit = INHERIT, fill_alpha: float | Inherit = INHERIT, number_of_points: int = 100, ): self.number_of_points = number_of_points self._fill = fill self._fill_color = fill_color self._edge_color = edge_color self._line_width = line_width self._line_style = line_style self._fill_alpha = fill_alpha self._sh_polygon = sh.geometry.Point(x_center, y_center).buffer( 1, self._num_points // 4 ) self.radius = radius
@property def x_center(self): return self.get_centroid_coordinates()[0] @x_center.setter def x_center(self, value): self._sh_polygon = sh.geometry.Point(value, self.y_center).buffer( self.radius, self._num_points // 4 ) @property def y_center(self): return self.get_centroid_coordinates()[1] @y_center.setter def y_center(self, value): self._sh_polygon = sh.geometry.Point(self.x_center, value).buffer( self.radius, self._num_points // 4 ) @property def radius(self): return self._sh_polygon.exterior.length / (2 * np.pi) @radius.setter def radius(self, value): if value <= 0: raise ValueError("The radius must be positive") self._sh_polygon = sh.geometry.Point(self.x_center, self.y_center).buffer( value, self._num_points // 4 ) @property def diameter(self): return 2 * self.radius @diameter.setter def diameter(self, value): self.radius = value / 2 @property def number_of_points(self): return self._num_points @number_of_points.setter def number_of_points(self, value): if value < 4: raise ValueError("The number of points must be greater than or equal to 4") self._num_points = value @property def circumference(self): return self._sh_polygon.exterior.length
[docs] @dataclass class Ellipse(Polygon): """This class implements an Ellipse object with a given center, two radii and an optional rotation angle. Parameters ---------- x_center : float The x coordinate of the center of the ellipse. y_center : float The y coordinate of the center of the ellipse. x_radius : float The radius of the ellipse along the x axis. y_radius : float The radius of the ellipse along the y axis. angle : float, optional The rotation angle of the ellipse in degrees. Defaults to 0. fill : bool, optional Whether the ellipse should be filled or not. Default depends on the ``figure_style`` configuration. fill_color : str, optional The color of the ellipse's fill. Default depends on the ``figure_style`` configuration. edge_color : str, optional The color of the ellipse's edge. Default depends on the ``figure_style`` configuration. line_width : float, optional The width of the line. Typical range is ``0.5`` to ``4``. Default depends on the ``figure_style`` configuration. line_style : str, optional The style of the line. Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and ``"dotted"``. Default depends on the ``figure_style`` configuration. fill_alpha : float, optional The alpha value of the fill. Range is ``0`` (transparent) to ``1`` (opaque). Default depends on the ``figure_style`` configuration. number_of_points : int, optional The number of points to use to approximate the ellipse. Default is 100 (covers approximately 99.9% of perfect ellipse area). 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_center: float, y_center: float, x_radius: float, y_radius: float, angle: float = 0, fill: bool | Inherit = INHERIT, fill_color: str | Inherit = INHERIT, edge_color: str | Inherit = INHERIT, line_width: float | Inherit = INHERIT, line_style: str | Inherit = INHERIT, fill_alpha: float | Inherit = INHERIT, number_of_points: int = 100, ): self.number_of_points = number_of_points self._fill = fill self._fill_color = fill_color self._edge_color = edge_color self._line_width = line_width self._line_style = line_style self._fill_alpha = fill_alpha self._x_radius = 1 self._y_radius = 1 self._angle = 0 self._sh_polygon = sh.geometry.Point(x_center, y_center).buffer( 1, self._num_points // 4 ) self.x_radius = x_radius self.y_radius = y_radius self.angle = angle
def _rebuild( self, x_center: float, y_center: float, x_radius: float, y_radius: float, angle: float, ): self._x_radius = x_radius self._y_radius = y_radius self._angle = angle self._sh_polygon = sh.geometry.Point(x_center, y_center).buffer( 1, self._num_points // 4 ) self._sh_polygon = sh.affinity.scale( self._sh_polygon, xfact=x_radius, yfact=y_radius, origin=(x_center, y_center), ) if angle != 0: self._sh_polygon = sh.affinity.rotate( self._sh_polygon, angle, origin=(x_center, y_center) ) @property def x_center(self): return self.get_centroid_coordinates()[0] @x_center.setter def x_center(self, value): self._rebuild(value, self.y_center, self._x_radius, self._y_radius, self._angle) @property def y_center(self): return self.get_centroid_coordinates()[1] @y_center.setter def y_center(self, value): self._rebuild(self.x_center, value, self._x_radius, self._y_radius, self._angle) @property def x_radius(self): return self._x_radius @x_radius.setter def x_radius(self, value): if value <= 0: raise ValueError("The x radius must be positive") self._rebuild(self.x_center, self.y_center, value, self._y_radius, self._angle) @property def y_radius(self): return self._y_radius @y_radius.setter def y_radius(self, value): if value <= 0: raise ValueError("The y radius must be positive") self._rebuild(self.x_center, self.y_center, self._x_radius, value, self._angle) @property def angle(self): return self._angle @angle.setter def angle(self, value): self._rebuild( self.x_center, self.y_center, self._x_radius, self._y_radius, value ) @property def number_of_points(self): return self._num_points @number_of_points.setter def number_of_points(self, value): if value < 4: raise ValueError("The number of points must be greater than or equal to 4") self._num_points = value @property def width(self): """Returns the width of the ellipse along the x axis.""" return 2 * self._x_radius @width.setter def width(self, value): """Controls the width of the ellipse along the x axis.""" if value <= 0: raise ValueError("The width must be positive") self.x_radius = value / 2 @property def height(self): """Returns the height of the ellipse along the y axis.""" return 2 * self._y_radius @height.setter def height(self, value): """Controls the height of the ellipse along the y axis.""" if value <= 0: raise ValueError("The height must be positive") self.y_radius = value / 2 @property def circumference(self): return self._sh_polygon.exterior.length
[docs] @dataclass class Rectangle(Polygon): """This class implements a Rectangle object with a given bottom left corner, width and height. Parameters ---------- x_bottom_left : float The x coordinate of the bottom left corner of the :class:`~graphinglib.shapes.Rectangle`. y_bottom_left : float The y coordinate of the bottom left corner of the :class:`~graphinglib.shapes.Rectangle`. width : float The width of the :class:`~graphinglib.shapes.Rectangle`. height : float The height of the :class:`~graphinglib.shapes.Rectangle`. fill : bool, optional Whether the rectangle should be filled or not. Default depends on the ``figure_style`` configuration. fill_color : str, optional The color of the rectangle's fill. Default depends on the ``figure_style`` configuration. edge_color : str, optional The color of the rectangle's edge. Default depends on the ``figure_style`` configuration. line_width : float, optional The width of the line. Typical range is ``0.5`` to ``4``. Default depends on the ``figure_style`` configuration. line_style : str, optional The style of the line. Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and ``"dotted"``. Default depends on the ``figure_style`` configuration. fill_alpha : float, optional The alpha value of the fill. 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_bottom_left: float, y_bottom_left: float, width: float, height: float, fill: bool | Inherit = INHERIT, fill_color: str | Inherit = INHERIT, edge_color: str | Inherit = INHERIT, line_width: float | Inherit = INHERIT, line_style: str | Inherit = INHERIT, fill_alpha: float | Inherit = INHERIT, ): self._fill = fill self._fill_color = fill_color self._edge_color = edge_color self._line_width = line_width self._line_style = line_style self._fill_alpha = fill_alpha self._sh_polygon = ShPolygon( [ (x_bottom_left, y_bottom_left), (x_bottom_left + 1, y_bottom_left), (x_bottom_left + 1, y_bottom_left + 1), (x_bottom_left, y_bottom_left + 1), ] ) self.width = width self.height = height
@property def x_bottom_left(self): return self.vertices[0][0] @x_bottom_left.setter def x_bottom_left(self, value): self._sh_polygon = ShPolygon( [ (value, self.y_bottom_left), (value + self.width, self.y_bottom_left), (value + self.width, self.y_bottom_left + self.height), (value, self.y_bottom_left + self.height), ] ) @property def y_bottom_left(self): return self.vertices[0][1] @y_bottom_left.setter def y_bottom_left(self, value): self._sh_polygon = ShPolygon( [ (self.x_bottom_left, value), (self.x_bottom_left + self.width, value), (self.x_bottom_left + self.width, value + self.height), (self.x_bottom_left, value + self.height), ] ) @property def width(self): return self.vertices[1][0] - self.vertices[0][0] @width.setter def width(self, value): if value <= 0: raise ValueError("The width must be positive") self._sh_polygon = ShPolygon( [ (self.x_bottom_left, self.y_bottom_left), (self.x_bottom_left + value, self.y_bottom_left), (self.x_bottom_left + value, self.y_bottom_left + self.height), (self.x_bottom_left, self.y_bottom_left + self.height), ] ) @property def height(self): return self.vertices[2][1] - self.vertices[1][1] @height.setter def height(self, value): if value <= 0: raise ValueError("The height must be positive") self._sh_polygon = ShPolygon( [ (self.x_bottom_left, self.y_bottom_left), (self.x_bottom_left + self.width, self.y_bottom_left), (self.x_bottom_left + self.width, self.y_bottom_left + value), (self.x_bottom_left, self.y_bottom_left + value), ] ) @property def center(self): return self.get_centroid_coordinates() @center.setter def center(self, value): x, y = value self._sh_polygon = ShPolygon( [ (x - self.width / 2, y - self.height / 2), (x + self.width / 2, y - self.height / 2), (x + self.width / 2, y + self.height / 2), (x - self.width / 2, y + self.height / 2), ] )
[docs] @classmethod def from_center( cls, x: float, y: float, width: float, height: float, fill: bool | Inherit = INHERIT, fill_color: str | Inherit = INHERIT, edge_color: str | Inherit = INHERIT, line_width: float | Inherit = INHERIT, line_style: str | Inherit = INHERIT, fill_alpha: float | Inherit = INHERIT, ) -> Self: """Creates a :class:`~graphinglib.shapes.Rectangle` from its center point, width and height. Parameters ---------- x : float The x coordinate of the center point. y : float The y coordinate of the center point. width : float The width of the :class:`~graphinglib.shapes.Rectangle`. height : float The height of the :class:`~graphinglib.shapes.Rectangle`. fill : bool, optional Whether the rectangle should be filled or not. Default depends on the ``figure_style`` configuration. fill_color : str, optional The color of the rectangle's fill. Default depends on the ``figure_style`` configuration. edge_color : str, optional The color of the rectangle's edge. Default depends on the ``figure_style`` configuration. line_width : float, optional The width of the line. Typical range is ``0.5`` to ``4``. Default depends on the ``figure_style`` configuration. line_style : str, optional The style of the line. Values include ``"-"``, ``"--"``, ``"-."``, ``":"``, ``"solid"``, ``"dashed"``, ``"dashdot"``, and ``"dotted"``. Default depends on the ``figure_style`` configuration. fill_alpha : float, optional The alpha value of the fill. 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)``). """ return cls( x - width / 2, y - height / 2, width, height, fill, fill_color, edge_color, line_width, line_style, fill_alpha, )