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