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 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,
)