From 04651a701020a1c90b747f7d30c2ee914fcc53ee Mon Sep 17 00:00:00 2001
From: Pierre Raybaut
Date: Tue, 3 Sep 2024 17:33:49 +0200
Subject: [PATCH] Add `AnnotatedPolygon` annotation to items
---
CHANGELOG.md | 7 ++
doc/features/items/builder.rst | 2 +-
doc/features/items/overview.rst | 1 +
doc/features/items/reference.rst | 2 +
plotpy/builder/annotation.py | 82 ++++++++++++-----
plotpy/io.py | 1 +
plotpy/items/__init__.py | 1 +
plotpy/items/annotation.py | 92 ++++++++++++++++++-
plotpy/items/shape/polygon.py | 26 ++++++
plotpy/plot/base.py | 2 +
.../features/test_loadsaveitems_pickle.py | 26 +++---
plotpy/tests/unit/test_builder_annotation.py | 17 +++-
plotpy/tools/item.py | 2 +
13 files changed, 222 insertions(+), 39 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0bbfc28a..98c0795c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog #
+## Version 2.6.3 ##
+
+💥 New features / Enhancements:
+
+* Added `AnnotatedPolygon` annotation to items
+* Added `make.annotated_polygon` function to `plotpy.builder` module
+
## Version 2.6.2 ##
💥 New features / Enhancements:
diff --git a/doc/features/items/builder.rst b/doc/features/items/builder.rst
index 6b8981ec..e841c233 100644
--- a/doc/features/items/builder.rst
+++ b/doc/features/items/builder.rst
@@ -12,4 +12,4 @@ used to simplify the creation of plot items.
:members:
.. autoclass:: plotpy.builder.PlotBuilder
- :members: widget,dialog,window,gridparam,grid,mcurve,pcurve,curve,merror,perror,error,histogram,phistogram,range,vcursor,hcursor,xcursor,marker,image,maskedimage,maskedxyimage,rgbimage,quadgrid,pcolor,trimage,xyimage,imagefilter,contours,histogram2D,rectangle,ellipse,polygon,circle,segment,svg,annotated_point,annotated_rectangle,annotated_ellipse,annotated_circle,annotated_segment,label,legend,info_label,range_info_label,computation,computations,computation2d,computations2d
+ :members: widget,dialog,window,gridparam,grid,mcurve,pcurve,curve,merror,perror,error,histogram,phistogram,range,vcursor,hcursor,xcursor,marker,image,maskedimage,maskedxyimage,rgbimage,quadgrid,pcolor,trimage,xyimage,imagefilter,contours,histogram2D,rectangle,ellipse,polygon,circle,segment,svg,annotated_point,annotated_rectangle,annotated_ellipse,annotated_circle,annotated_segment,annotated_polygon,label,legend,info_label,range_info_label,computation,computations,computation2d,computations2d
diff --git a/doc/features/items/overview.rst b/doc/features/items/overview.rst
index fc0d2dc0..71c67b1a 100644
--- a/doc/features/items/overview.rst
+++ b/doc/features/items/overview.rst
@@ -75,6 +75,7 @@ The following annotation items are available:
* :py:class:`.AnnotatedObliqueRectangle`
* :py:class:`.AnnotatedEllipse`
* :py:class:`.AnnotatedCircle`
+* :py:class:`.AnnotatedPolygon`
Labels
^^^^^^
diff --git a/doc/features/items/reference.rst b/doc/features/items/reference.rst
index 25c83b38..3ac84ad3 100644
--- a/doc/features/items/reference.rst
+++ b/doc/features/items/reference.rst
@@ -124,6 +124,8 @@ Annotations
:members:
.. autoclass:: plotpy.items.AnnotatedCircle
:members:
+.. autoclass:: plotpy.items.AnnotatedPolygon
+ :members:
Labels
^^^^^^
diff --git a/plotpy/builder/annotation.py b/plotpy/builder/annotation.py
index 051fc6c9..9f742691 100644
--- a/plotpy/builder/annotation.py
+++ b/plotpy/builder/annotation.py
@@ -27,6 +27,7 @@
AnnotatedCircle,
AnnotatedEllipse,
AnnotatedPoint,
+ AnnotatedPolygon,
AnnotatedRectangle,
AnnotatedSegment,
)
@@ -126,10 +127,7 @@ def annotated_point(
def __annotated_shape(
self,
shapeclass,
- x0,
- y0,
- x1,
- y1,
+ points,
title,
subtitle,
show_label,
@@ -153,7 +151,10 @@ def __annotated_shape(
readonly=readonly,
private=private,
)
- shape = shapeclass(x0, y0, x1, y1, param)
+ if isinstance(points, np.ndarray):
+ shape = shapeclass(points, annotationparam=param)
+ else:
+ shape = shapeclass(*points, annotationparam=param)
shape.set_style("plot", "shape/drag")
return shape
@@ -195,12 +196,10 @@ def annotated_rectangle(
Returns:
:py:class:`.AnnotatedRectangle` object
"""
+ points = x0, y0, x1, y1
return self.__annotated_shape(
AnnotatedRectangle,
- x0,
- y0,
- x1,
- y1,
+ points,
title,
subtitle,
show_label,
@@ -259,12 +258,10 @@ def annotated_ellipse(
Returns:
:py:class:`.AnnotatedEllipse` object
"""
+ points = x0, y0, x1, y1
item = self.__annotated_shape(
AnnotatedEllipse,
- x0,
- y0,
- x1,
- y1,
+ points,
title,
subtitle,
show_label,
@@ -318,12 +315,10 @@ def annotated_circle(
Returns:
:py:class:`.AnnotatedCircle` object
"""
+ points = x0, y0, x1, y1
return self.__annotated_shape(
AnnotatedCircle,
- x0,
- y0,
- x1,
- y1,
+ points,
title,
subtitle,
show_label,
@@ -374,12 +369,57 @@ def annotated_segment(
Returns:
:py:class:`.AnnotatedSegment` object
"""
+ points = x0, y0, x1, y1
return self.__annotated_shape(
AnnotatedSegment,
- x0,
- y0,
- x1,
- y1,
+ points,
+ title,
+ subtitle,
+ show_label,
+ show_computations,
+ show_subtitle,
+ format,
+ uncertainty,
+ transform_matrix,
+ readonly,
+ private,
+ )
+
+ def annotated_polygon(
+ self,
+ points: np.ndarray,
+ title: str | None = None,
+ subtitle: str | None = None,
+ show_label: bool | None = None,
+ show_computations: bool | None = None,
+ show_subtitle: bool | None = None,
+ format: str | None = None,
+ uncertainty: float | None = None,
+ transform_matrix: np.ndarray | None = None,
+ readonly: bool | None = None,
+ private: bool | None = None,
+ ) -> AnnotatedPolygon:
+ """Make an annotated polygon `plot item`
+
+ Args:
+ points: polygon points
+ title: label name. Default is None
+ subtitle: label subtitle. Default is None
+ show_label: show label. Default is None
+ show_computations: show computations. Default is None
+ show_subtitle: show subtitle. Default is None
+ format: string formatting. Default is None
+ uncertainty: measurement relative uncertainty. Default is None
+ transform_matrix: transform matrix. Default is None
+ readonly: readonly. Default is None
+ private: private. Default is None
+
+ Returns:
+ :py:class:`.AnnotatedPolygon` object
+ """
+ return self.__annotated_shape(
+ AnnotatedPolygon,
+ points,
title,
subtitle,
show_label,
diff --git a/plotpy/io.py b/plotpy/io.py
index a2b05dd6..6f5b006a 100644
--- a/plotpy/io.py
+++ b/plotpy/io.py
@@ -620,6 +620,7 @@ def register_serializable_items(modname, classnames):
"AnnotatedObliqueRectangle",
"AnnotatedEllipse",
"AnnotatedCircle",
+ "AnnotatedPolygon",
"LabelItem",
"LegendBoxItem",
"SelectedLegendBoxItem",
diff --git a/plotpy/items/__init__.py b/plotpy/items/__init__.py
index 65fe898f..13fc1989 100644
--- a/plotpy/items/__init__.py
+++ b/plotpy/items/__init__.py
@@ -9,6 +9,7 @@
AnnotatedObliqueRectangle,
AnnotatedPoint,
AnnotatedRectangle,
+ AnnotatedPolygon,
AnnotatedSegment,
AnnotatedShape,
)
diff --git a/plotpy/items/annotation.py b/plotpy/items/annotation.py
index 50998f1c..2a04609e 100644
--- a/plotpy/items/annotation.py
+++ b/plotpy/items/annotation.py
@@ -28,6 +28,7 @@
from plotpy.items.shape.base import AbstractShape
from plotpy.items.shape.ellipse import EllipseShape
from plotpy.items.shape.point import PointShape
+from plotpy.items.shape.polygon import PolygonShape
from plotpy.items.shape.rectangle import ObliqueRectangleShape, RectangleShape
from plotpy.items.shape.segment import SegmentShape
from plotpy.mathutils.geometry import (
@@ -461,9 +462,8 @@ def boundingRect(self) -> QRectF:
class AnnotatedPoint(AnnotatedShape):
"""
- Construct an annotated point at coordinates (x, y)
- with properties set with *annotationparam*
- (see :py:class:`.styles.AnnotationParam`)
+ Construct an annotated point at coordinates (x, y) with properties set with
+ *annotationparam* (see :py:class:`.styles.AnnotationParam`)
"""
SHAPE_CLASS = PointShape
@@ -575,6 +575,92 @@ def get_infos(self) -> str:
)
+class AnnotatedPolygon(AnnotatedShape):
+ """
+ Construct an annotated polygon with properties set with *annotationparam*
+ (see :py:class:`.styles.AnnotationParam`)
+
+ Args:
+ points: List of points
+ closed: True if polygon is closed
+ annotationparam: Annotation parameters
+ """
+
+ SHAPE_CLASS = PolygonShape
+ LABEL_ANCHOR = "C"
+
+ def __init__(
+ self,
+ points: list[tuple[float, float]] | None = None,
+ closed: bool | None = None,
+ annotationparam: AnnotationParam | None = None,
+ ) -> None:
+ super().__init__(annotationparam)
+ self.shape: PolygonShape
+ if points is not None:
+ self.set_points(points)
+ if closed is not None:
+ self.set_closed(closed)
+ self.setIcon(get_icon("polygon.png"))
+
+ # ----Public API-------------------------------------------------------------
+ def set_points(self, points: list[tuple[float, float]] | None) -> None:
+ """Set the polygon points
+
+ Args:
+ points: List of point coordinates
+ """
+ self.shape.set_points(points)
+ self.set_label_position()
+
+ def get_points(self) -> list[tuple[float, float]]:
+ """Return the polygon points"""
+ return self.shape.get_points()
+
+ def set_closed(self, state: bool) -> None:
+ """Set the polygon closed state
+
+ Args:
+ state: True if polygon is closed
+ """
+ self.shape.set_closed(state)
+
+ def is_closed(self) -> bool:
+ """Return True if polygon is closed
+
+ Returns:
+ True if polygon is closed
+ """
+ return self.shape.is_closed()
+
+ # ----AnnotatedShape API-----------------------------------------------------
+ def create_shape(self):
+ """Return the shape object associated to this annotated shape object"""
+ shape = self.SHAPE_CLASS() # pylint: disable=not-callable
+ return shape
+
+ def set_label_position(self) -> None:
+ """Set label position, for instance based on shape position"""
+ x, y = self.shape.get_center()
+ self.label.set_pos(x, y)
+
+ def get_tr_center(self) -> tuple[float, float]:
+ """Return shape center coordinates after applying transform matrix"""
+ return self.shape.get_center()
+
+ def get_infos(self) -> str:
+ """Get informations on current shape
+
+ Returns:
+ str: Formatted string with informations on current shape
+ """
+ return "
".join(
+ [
+ _("Center:") + " " + self.get_tr_center_str(),
+ ]
+ )
+
+
class AnnotatedRectangle(AnnotatedShape):
"""
Construct an annotated rectangle between coordinates (x1, y1) and
diff --git a/plotpy/items/shape/polygon.py b/plotpy/items/shape/polygon.py
index 207c278a..adb1924c 100644
--- a/plotpy/items/shape/polygon.py
+++ b/plotpy/items/shape/polygon.py
@@ -154,6 +154,32 @@ def get_points(self) -> np.ndarray:
"""
return self.points
+ def set_closed(self, state: bool) -> None:
+ """Set closed state
+
+ Args:
+ state: True if the polygon is closed, False otherwise
+ """
+ self.closed = state
+
+ def is_closed(self) -> bool:
+ """Return True if the polygon is closed, False otherwise
+
+ Returns:
+ True if the polygon is closed, False otherwise
+ """
+ return self.closed
+
+ def get_center(self) -> tuple[float, float]:
+ """Return the center of the polygon
+
+ Returns:
+ Center of the polygon
+ """
+ if self.points is not None and self.points.size > 0:
+ return self.points.mean(axis=0)
+ return 0.0, 0.0
+
def boundingRect(self) -> QC.QRectF:
"""Return the bounding rectangle of the data
diff --git a/plotpy/plot/base.py b/plotpy/plot/base.py
index f346d2b8..0e750ec1 100644
--- a/plotpy/plot/base.py
+++ b/plotpy/plot/base.py
@@ -42,6 +42,7 @@
AnnotatedEllipse,
AnnotatedObliqueRectangle,
AnnotatedPoint,
+ AnnotatedPolygon,
AnnotatedRectangle,
AnnotatedSegment,
BaseImageItem,
@@ -2369,3 +2370,4 @@ def register_autoscale_type(cls, type_):
BasePlot.register_autoscale_type(AnnotatedObliqueRectangle)
BasePlot.register_autoscale_type(AnnotatedSegment)
BasePlot.register_autoscale_type(AnnotatedPoint)
+BasePlot.register_autoscale_type(AnnotatedPolygon)
diff --git a/plotpy/tests/features/test_loadsaveitems_pickle.py b/plotpy/tests/features/test_loadsaveitems_pickle.py
index 695c7b8a..4e443bd0 100644
--- a/plotpy/tests/features/test_loadsaveitems_pickle.py
+++ b/plotpy/tests/features/test_loadsaveitems_pickle.py
@@ -50,6 +50,18 @@ def build_items() -> list:
delta = delta + 0.01
y.append(y[-1] + delta)
+ polypoints1 = np.array(
+ [
+ [150.0, 330.0],
+ [270.0, 520.0],
+ [470.0, 480.0],
+ [520.0, 360.0],
+ [460.0, 200.0],
+ [250.0, 240.0],
+ ]
+ )
+ polypoints2 = polypoints1 * 0.5
+
items = [
make.curve(x, y, color="b"),
make.image(filename=filename),
@@ -71,19 +83,9 @@ def build_items() -> list:
make.ellipse(-10, 0.0, 0, 0, "el1"),
make.annotated_rectangle(0.5, 0.8, 3, 1.0, "rc1", "tutu"),
make.annotated_segment(-1, -1, 1, 1.0, "rc1", "tutu"),
+ make.annotated_polygon(polypoints2, "rc1", "tutu"),
Axes((0, 0), (1, 0), (0, 1)),
- PolygonShape(
- np.array(
- [
- [150.0, 330.0],
- [270.0, 520.0],
- [470.0, 480.0],
- [520.0, 360.0],
- [460.0, 200.0],
- [250.0, 240.0],
- ]
- )
- ),
+ PolygonShape(polypoints1),
]
return items
diff --git a/plotpy/tests/unit/test_builder_annotation.py b/plotpy/tests/unit/test_builder_annotation.py
index c278b453..9099f607 100644
--- a/plotpy/tests/unit/test_builder_annotation.py
+++ b/plotpy/tests/unit/test_builder_annotation.py
@@ -18,6 +18,16 @@
make.annotated_rectangle: [0.0, 0.0, 1.0, 1.0],
make.annotated_circle: [0.0, 0.0, 1.0, 1.0],
make.annotated_ellipse: [0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0],
+ make.annotated_polygon: np.array(
+ [
+ [150.0, 330.0],
+ [270.0, 520.0],
+ [470.0, 480.0],
+ [520.0, 360.0],
+ [460.0, 200.0],
+ [250.0, 240.0],
+ ]
+ ),
}
@@ -36,8 +46,7 @@ def _make_annotation(
):
"""Make annotation"""
args = DEFAULT_ARGS[method]
- return method(
- *args,
+ kwargs = dict(
title=title,
subtitle=subtitle,
show_label=show_label,
@@ -49,6 +58,9 @@ def _make_annotation(
readonly=readonly,
private=private,
)
+ if isinstance(args, np.ndarray):
+ return method(args, **kwargs)
+ return method(*args, **kwargs)
@pytest.mark.parametrize(
@@ -59,6 +71,7 @@ def _make_annotation(
make.annotated_rectangle,
make.annotated_circle,
make.annotated_ellipse,
+ make.annotated_polygon,
],
)
def test_builder_annotation_params(method):
diff --git a/plotpy/tools/item.py b/plotpy/tools/item.py
index 53402959..c911c6bb 100644
--- a/plotpy/tools/item.py
+++ b/plotpy/tools/item.py
@@ -9,6 +9,7 @@
AnnotatedCircle,
AnnotatedEllipse,
AnnotatedObliqueRectangle,
+ AnnotatedPolygon,
AnnotatedRectangle,
EllipseShape,
ObliqueRectangleShape,
@@ -123,6 +124,7 @@ def get_supported_items(self, plot):
AnnotatedEllipse,
AnnotatedObliqueRectangle,
AnnotatedCircle,
+ AnnotatedPolygon,
)
return [
item