Skip to content

Commit 9d2dfec

Browse files
committed
New SVG shapes (+ associated test)
1 parent ee2081f commit 9d2dfec

9 files changed

Lines changed: 318 additions & 0 deletions

File tree

doc/features/items/overview.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ The following shape items are available:
6060
* :py:class:`.Axes`
6161
* :py:class:`.XRangeSelection`
6262
* :py:class:`.Marker`
63+
* :py:class:`.RectangleSVGShape`
64+
* :py:class:`.SquareSVGShape`
65+
* :py:class:`.CircleSVGShape`
6366

6467
Annotations
6568
^^^^^^^^^^^

doc/features/items/reference.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ Shapes
9696
:members:
9797
.. autoclass:: plotpy.items.Marker
9898
:members:
99+
.. autoclass:: plotpy.items.RectangleSVGShape
100+
:members:
101+
.. autoclass:: plotpy.items.SquareSVGShape
102+
:members:
103+
.. autoclass:: plotpy.items.CircleSVGShape
104+
:members:
99105

100106
Annotations
101107
^^^^^^^^^^^

plotpy/builder.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
AnnotatedPoint,
3131
AnnotatedRectangle,
3232
AnnotatedSegment,
33+
CircleSVGShape,
3334
ContourItem,
3435
CurveItem,
3536
DataInfoLabel,
@@ -49,9 +50,11 @@
4950
RangeComputation2d,
5051
RangeInfo,
5152
RectangleShape,
53+
RectangleSVGShape,
5254
RGBImageItem,
5355
SegmentShape,
5456
SelectedLegendBoxItem,
57+
SquareSVGShape,
5558
TrImageItem,
5659
XRangeSelection,
5760
XYImageItem,
@@ -2312,6 +2315,45 @@ def segment(
23122315
"""
23132316
return self.__shape(SegmentShape, x0, y0, x1, y1, title)
23142317

2318+
def svg(
2319+
self,
2320+
shape: str,
2321+
fname_or_data: str | bytes,
2322+
x0: float,
2323+
y0: float,
2324+
x1: float,
2325+
y1: float,
2326+
title: str | None = None,
2327+
) -> (CircleSVGShape, RectangleSVGShape, SquareSVGShape):
2328+
"""Make a SVG shape `plot item`
2329+
2330+
Args:
2331+
shape: shape type ("circle", "rectangle", "square")
2332+
fname_or_data: filename or data
2333+
x0: shape x0 coordinate
2334+
y0: shape y0 coordinate
2335+
x1: shape x1 coordinate
2336+
y1: shape y1 coordinate
2337+
title: label name. Default is None
2338+
2339+
Returns:
2340+
SVG shape
2341+
"""
2342+
assert shape in ("circle", "rectangle", "square")
2343+
assert isinstance(fname_or_data, (str, bytes))
2344+
if isinstance(fname_or_data, str):
2345+
data = open(fname_or_data, "rb").read()
2346+
else:
2347+
data = fname_or_data
2348+
shapeklass = {
2349+
"circle": CircleSVGShape,
2350+
"rectangle": RectangleSVGShape,
2351+
"square": SquareSVGShape,
2352+
}[shape]
2353+
shape = self.__shape(shapeklass, x0, y0, x1, y1, title)
2354+
shape.set_data(data)
2355+
return shape
2356+
23152357
def __get_annotationparam(self, title: str, subtitle: str) -> AnnotationParam:
23162358
param = AnnotationParam(_("Annotation"), icon="annotation.png")
23172359
if title is not None:

plotpy/items/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,15 @@
5151
from .shapes import (
5252
AbstractShape,
5353
Axes,
54+
CircleSVGShape,
5455
EllipseShape,
5556
Marker,
5657
ObliqueRectangleShape,
5758
PointShape,
5859
PolygonShape,
5960
RectangleShape,
61+
RectangleSVGShape,
6062
SegmentShape,
63+
SquareSVGShape,
6164
XRangeSelection,
6265
)

plotpy/items/shapes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
from plotpy.items.shapes.range import XRangeSelection
1111
from plotpy.items.shapes.rectangle import ObliqueRectangleShape, RectangleShape
1212
from plotpy.items.shapes.segment import SegmentShape
13+
from plotpy.items.shapes.svg import CircleSVGShape, RectangleSVGShape, SquareSVGShape

plotpy/items/shapes/svg.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# For licensing and distribution details, please read carefully xgrid/__init__.py
4+
5+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
6+
7+
from __future__ import annotations
8+
9+
from qtpy import QtCore as QC
10+
from qtpy import QtGui as QG
11+
from qtpy import QtSvg as QS
12+
from qwt.scale_map import QwtScaleMap
13+
from qwt.symbol import QwtSymbol
14+
15+
from plotpy.items import shapes
16+
from plotpy.styles import ShapeParam
17+
18+
19+
class RectangleSVGShape(shapes.RectangleShape):
20+
"""Rectangle SVG shape
21+
22+
Args:
23+
x1: X coordinate of the top-left corner
24+
y1: Y coordinate of the top-left corner
25+
x2: X coordinate of the bottom-right corner
26+
y2: Y coordinate of the bottom-right corner
27+
svg_data: SVG bytes
28+
shapeparam: Shape parameters
29+
"""
30+
31+
def __init__(
32+
self,
33+
x1: float = 0.0,
34+
y1: float = 0.0,
35+
x2: float = 0.0,
36+
y2: float = 0.0,
37+
svg_data: bytes | None = None,
38+
shapeparam: ShapeParam = None,
39+
) -> None:
40+
self.svg_data = svg_data
41+
shapes.RectangleShape.__init__(self, x1, y1, x2, y2, shapeparam)
42+
43+
def set_data(self, svg_data: bytes) -> None:
44+
"""Set SVG data"""
45+
self.svg_data = svg_data
46+
47+
def draw(
48+
self,
49+
painter: QG.QPainter,
50+
xMap: QwtScaleMap,
51+
yMap: QwtScaleMap,
52+
canvasRect: QC.QRectF,
53+
) -> None:
54+
"""Draw shape (reimplement shapes.Shape.draw))"""
55+
points = self.transform_points(xMap, yMap)
56+
renderer = QS.QSvgRenderer(self.svg_data)
57+
renderer.render(painter, points.boundingRect())
58+
59+
60+
class SquareSVGShape(RectangleSVGShape):
61+
"""Square SVG shape
62+
63+
Args:
64+
svg_data: SVG bytes
65+
x1: X coordinate of the top-left corner
66+
y1: Y coordinate of the top-left corner
67+
x2: X coordinate of the bottom-right corner
68+
y2: Y coordinate of the bottom-right corner
69+
shapeparam: Shape parameters
70+
"""
71+
72+
def move_point_to(self, handle: int, pos: tuple[float], ctrl: bool = None):
73+
"""Reimplement RectangleShape.move_point_to"""
74+
nx, ny = pos
75+
x1, y1, x2, y2 = self.get_rect()
76+
if handle == 0:
77+
self.set_rect(nx, y2 - (x2 - nx), x2, y2)
78+
elif handle == 1:
79+
self.set_rect(x1, y2 - (nx - x1), nx, y2)
80+
elif handle == 2:
81+
self.set_rect(x1, y1, nx, y1 + (nx - x1))
82+
elif handle == 3:
83+
self.set_rect(nx, y1, x2, y1 + (x2 - nx))
84+
elif handle == -1:
85+
delta = (nx, ny) - self.points.mean(axis=0)
86+
self.points += delta
87+
88+
89+
class CircleSVGShape(shapes.EllipseShape):
90+
"""Circle SVG shape
91+
92+
Args:
93+
x1: X coordinate of the top-left corner
94+
y1: Y coordinate of the top-left corner
95+
x2: X coordinate of the bottom-right corner
96+
y2: Y coordinate of the bottom-right corner
97+
svg_data: SVG bytes
98+
shapeparam: Shape parameters
99+
"""
100+
101+
def __init__(
102+
self,
103+
x1: float = 0.0,
104+
y1: float = 0.0,
105+
x2: float = 0.0,
106+
y2: float = 0.0,
107+
svg_data: bytes | None = None,
108+
shapeparam: ShapeParam = None,
109+
) -> None:
110+
self.svg_data = svg_data
111+
shapes.EllipseShape.__init__(self, x1, y1, x2, y2, shapeparam)
112+
113+
def set_data(self, svg_data: bytes) -> None:
114+
"""Set SVG data"""
115+
self.svg_data = svg_data
116+
117+
def draw(
118+
self,
119+
painter: QG.QPainter,
120+
xMap: QwtScaleMap,
121+
yMap: QwtScaleMap,
122+
canvasRect: QC.QRect,
123+
) -> None:
124+
"""Draw shape (reimplement shapes.Shape.draw))"""
125+
points, line0, line1, rect = self.compute_elements(xMap, yMap)
126+
if canvasRect.intersects(rect.toRect()) and self.svg_data is not None:
127+
pen, brush, symbol = self.get_pen_brush(xMap, yMap)
128+
painter.setRenderHint(QG.QPainter.Antialiasing)
129+
painter.setPen(pen)
130+
painter.setBrush(brush)
131+
painter.drawLine(line0)
132+
painter.drawLine(line1)
133+
painter.save()
134+
painter.translate(rect.center())
135+
painter.rotate(-line0.angle())
136+
painter.translate(-rect.center())
137+
renderer = QS.QSvgRenderer(self.svg_data)
138+
renderer.render(painter, rect)
139+
painter.restore()
140+
if symbol != QwtSymbol.NoSymbol:
141+
for i in range(points.size()):
142+
symbol.drawSymbol(painter, points[i].toPoint())

plotpy/tests/gui/svg_sample.svg

Lines changed: 44 additions & 0 deletions
Loading

plotpy/tests/gui/svg_target.svg

Lines changed: 42 additions & 0 deletions
Loading

plotpy/tests/gui/test_svgshapes.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Licensed under the terms of the BSD 3-Clause
4+
# (see plotpy/LICENSE for details)
5+
6+
"""Test showing SVG shapes"""
7+
8+
# guitest: show
9+
10+
import os.path as osp
11+
12+
from guidata.qthelpers import qt_app_context
13+
14+
from plotpy.builder import make
15+
from plotpy.tests import vistools as ptv
16+
17+
18+
def get_path(fname):
19+
"""Return absolute path of data file"""
20+
return osp.join(osp.dirname(__file__), fname)
21+
22+
23+
def test_image():
24+
"""Testing ImageItem object"""
25+
with qt_app_context(exec_loop=True):
26+
path1, path2 = get_path("svg_sample.svg"), get_path("svg_target.svg")
27+
csvg = make.svg("circle", path2, 0, 0, 100, 100, "Circle")
28+
rsvg = make.svg("rectangle", path1, 150, 0, 200, 100, "Rect")
29+
ssvg = make.svg("square", path1, 0, 150, 100, 250, "Square")
30+
items = [csvg, rsvg, ssvg]
31+
_win = ptv.show_items(items, wintitle="SVG shapes", lock_aspect_ratio=True)
32+
33+
34+
if __name__ == "__main__":
35+
test_image()

0 commit comments

Comments
 (0)