Source code for graphinglib.shapes

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 Point

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


[docs] @dataclass class Arrow: """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. Default depends on the ``figure_style`` configuration. head_size : float, optional Scales the size of the arrow head. Default depends on the ``figure_style`` configuration. shrink : float Fraction of the total length of the arrow to shrink from both ends. A value of 0.5 means the arrow is no longer visible. Defaults to 0. two_sided : bool If ``True``, an arrow is shown at both head and tail. Defaults to ``False``. """
[docs] def __init__( self, pointA: tuple[float, float], pointB: tuple[float, float], color: str = "default", width: float | Literal["default"] = "default", head_size: float | Literal["default"] = "default", shrink: float = 0, 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 douple-sided, refers to the head. color : str Color of the arrow. Default depends on the ``figure_style`` configuration. width : float, optional Arrow line width. Default depends on the ``figure_style`` configuration. head_size : float, optional Scales the size of the arrow head. Default depends on the ``figure_style`` configuration. shrink : float Fraction of the total length of the arrow to shrink from both ends. A value of 0.5 means the arrow is no longer visible. Defaults to 0. two_sided : bool If ``True``, the arrow is double-sided. Defaults to ``False``. """ self._pointA = pointA self._pointB = pointB self._color = color self._width = width self._head_size = head_size self._shrink = shrink 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 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: self._style = "<|-|>" else: self._style = "-|>" head_length, head_width = self._head_size * 0.4, self._head_size * 0.2 props = { "arrowstyle": f"{self._style}, head_width={head_width}, head_length={head_length}", "color": self._color, "linewidth": self._width, } 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: """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. 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. Default depends on the ``figure_style`` configuration. """ _pointA: tuple[float, float] _pointB: tuple[float, float] _color: str = "default" _width: float | Literal["default"] = "default" _capped_line: bool = False _cap_width: float | Literal["default"] = "default"
[docs] def __init__( self, pointA: tuple[float, float], pointB: tuple[float, float], color: str = "default", width: float | Literal["default"] = "default", capped_line: bool = False, cap_width: float | Literal["default"] = "default", ): self._pointA = pointA self._pointB = pointB self._color = color self._width = width self._capped_line = capped_line self._cap_width = cap_width
@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
[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, } axes.annotate( "", self._pointA, self._pointB, zorder=z_order, arrowprops=props, )
[docs] class Polygon: """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. color : str, optional The color of the polygon (both the line and the fill). Default depends on the ``figure_style`` configuration. line_width : float, optional The width of the line. Default depends on the ``figure_style`` configuration. line_style : str, optional The style of the line. Default depends on the ``figure_style`` configuration. fill_alpha : float, optional The alpha value of the fill. Default depends on the ``figure_style`` configuration. """
[docs] def __init__( self, vertices: list[tuple[float, float]], fill: bool = "default", edge_color: str = "default", fill_color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", fill_alpha: float | Literal["default"] = "default", ): 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: kwargs = { "alpha": self._fill_alpha, "zorder": z_order - 1, } if self._fill_color is not None: kwargs["facecolor"] = self._fill_color polygon_fill = MPLPolygon(self.vertices, **kwargs) axes.add_patch(polygon_fill) # Create a polygon patch for the edge if self._edge_color is not None: kwargs = { "fill": None, "linewidth": self._line_width, "linestyle": self._line_style, "edgecolor": self._edge_color, "zorder": z_order, } polygon_edge = MPLPolygon(self.vertices, **kwargs) 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. Default depends on the ``figure_style`` configuration. line_style : str, optional The style of the line. Default depends on the ``figure_style`` configuration. fill_alpha : float, optional The alpha value of the fill. 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). """
[docs] def __init__( self, x_center: float, y_center: float, radius: float, fill: bool = "default", fill_color: str = "default", edge_color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", fill_alpha: float | Literal["default"] = "default", number_of_points: int = 100, ): if number_of_points < 4: raise ValueError("The number of points must be greater than or equal to 4") 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 if radius <= 0: raise ValueError("The radius must be positive") self._sh_polygon = sh.geometry.Point(x_center, y_center).buffer( radius, number_of_points // 4 )
@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._sh_polygon.exterior.coords ) @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._sh_polygon.exterior.coords ) @property def radius(self): return self._sh_polygon.exterior.length / (2 * np.pi) @radius.setter def radius(self, value): self._sh_polygon = sh.geometry.Point(self.x_center, self.y_center).buffer( value, self._sh_polygon.exterior.coords ) @property def diameter(self): return 2 * self.radius @diameter.setter def diameter(self, value): self.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. Default depends on the ``figure_style`` configuration. line_style : str, optional The style of the line. Default depends on the ``figure_style`` configuration. fill_alpha : float, optional The alpha value of the fill. Default depends on the ``figure_style`` configuration. """
[docs] def __init__( self, x_bottom_left: float, y_bottom_left: float, width: float, height: float, fill: bool = "default", fill_color: str = "default", edge_color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", fill_alpha: float | Literal["default"] = "default", ): if width <= 0: raise ValueError("The width must be positive") if height <= 0: raise ValueError("The height must be positive") 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 + width, y_bottom_left), (x_bottom_left + width, y_bottom_left + height), (x_bottom_left, y_bottom_left + 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): 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): 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 = "default", fill_color: str = "default", edge_color: str = "default", line_width: float | Literal["default"] = "default", line_style: str = "default", fill_alpha: float | Literal["default"] = "default", ) -> 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. Default depends on the ``figure_style`` configuration. line_style : str, optional The style of the line. Default depends on the ``figure_style`` configuration. fill_alpha : float, optional The alpha value of the fill. Default depends on the ``figure_style`` configuration. """ return cls( x - width / 2, y - height / 2, width, height, fill, fill_color, edge_color, line_width, line_style, fill_alpha, )