Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8b1d326

Browse files
authoredApr 4, 2025··
Merge pull request #45 from ksunden/caching_patches
Consolidate caching logic, implement patch subclasses
2 parents 7021e10 + 5581ada commit 8b1d326

File tree

4 files changed

+284
-53
lines changed

4 files changed

+284
-53
lines changed
 

‎data_prototype/artist.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from bisect import insort
2+
from collections import OrderedDict
23
from typing import Sequence
34
from contextlib import contextmanager
45

@@ -33,6 +34,8 @@ def __init__(
3334
**{"x": np.asarray([0, 1]), "y": np.asarray([0, 1])}
3435
)
3536

37+
self._caches = {}
38+
3639
def draw(self, renderer, graph: Graph) -> None:
3740
return
3841

@@ -119,6 +122,31 @@ def pick(self, mouseevent, graph: Graph | None = None):
119122
# which do not have an axes property but children might
120123
a.pick(mouseevent, graph)
121124

125+
def _get_dynamic_graph(self, query, description, graph, cacheset):
126+
return Graph([])
127+
128+
def _query_and_eval(self, container, requires, graph, cacheset=None):
129+
g = graph + self._graph
130+
query, q_cache_key = container.query(g)
131+
g = g + self._get_dynamic_graph(query, container.describe(), graph, cacheset)
132+
g_cache_key = g.cache_key()
133+
cache_key = (g_cache_key, q_cache_key)
134+
135+
cache = None
136+
if cacheset is not None:
137+
cache = self._caches.setdefault(cacheset, OrderedDict())
138+
if cache_key in cache:
139+
return cache[cache_key]
140+
141+
conv = g.evaluator(container.describe(), requires)
142+
ret = conv.evaluate(query)
143+
144+
# TODO: actually add to cache and prune
145+
# if cache is not None:
146+
# cache[cache_key] = ret
147+
148+
return ret
149+
122150

123151
class CompatibilityArtist:
124152
"""A compatibility shim to ducktype as a classic Matplotlib Artist.

‎data_prototype/conversion_edge.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,3 +418,12 @@ def __add__(self, other: Graph) -> Graph:
418418
aother = {k: v for k, v in other._aliases}
419419
aliases = tuple((aself | aother).items())
420420
return Graph(self._edges + other._edges, aliases)
421+
422+
def cache_key(self):
423+
"""A cache key representing the graph.
424+
425+
Current implementation is a new UUID, that is to say uncachable.
426+
"""
427+
import uuid
428+
429+
return str(uuid.uuid4())

‎data_prototype/patches.py

Lines changed: 207 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,28 @@
77

88
from .wrappers import ProxyWrapper, _stale_wrapper
99

10-
from .containers import DataContainer
11-
1210
from .artist import Artist, _renderer_group
13-
from .description import Desc
14-
from .conversion_edge import Graph, CoordinateEdge, DefaultEdge
11+
from .description import Desc, desc_like
12+
from .containers import DataContainer
13+
from .conversion_edge import Graph, CoordinateEdge, DefaultEdge, TransformEdge
1514

1615

1716
class Patch(Artist):
1817
def __init__(self, container, edges=None, **kwargs):
1918
super().__init__(container, edges, **kwargs)
2019

2120
scalar = Desc((), "display") # ... this needs thinking...
22-
edges = [
21+
def_edges = [
2322
CoordinateEdge.from_coords("xycoords", {"x": "auto", "y": "auto"}, "data"),
2423
CoordinateEdge.from_coords("codes", {"codes": "auto"}, "display"),
25-
CoordinateEdge.from_coords("facecolor", {"color": Desc(())}, "display"),
26-
CoordinateEdge.from_coords("edgecolor", {"color": Desc(())}, "display"),
24+
CoordinateEdge.from_coords("facecolor", {"facecolor": Desc(())}, "display"),
25+
CoordinateEdge.from_coords("edgecolor", {"edgecolor": Desc(())}, "display"),
26+
CoordinateEdge.from_coords(
27+
"facecolor_rgba", {"facecolor": Desc(("M",))}, "display"
28+
),
29+
CoordinateEdge.from_coords(
30+
"edgecolor_rgba", {"edgecolor": Desc(("M",))}, "display"
31+
),
2732
CoordinateEdge.from_coords("linewidth", {"linewidth": Desc(())}, "display"),
2833
CoordinateEdge.from_coords("hatch", {"hatch": Desc(())}, "display"),
2934
CoordinateEdge.from_coords("alpha", {"alpha": Desc(())}, "display"),
@@ -34,39 +39,34 @@ def __init__(self, container, edges=None, **kwargs):
3439
DefaultEdge.from_default_value("alpha_def", "alpha", scalar, 1),
3540
DefaultEdge.from_default_value("hatch_def", "hatch", scalar, None),
3641
]
37-
self._graph = self._graph + Graph(edges)
42+
self._graph = self._graph + Graph(def_edges)
3843

3944
def draw(self, renderer, graph: Graph) -> None:
4045
if not self.get_visible():
4146
return
42-
g = graph + self._graph
4347
desc = Desc(("N",), "display")
4448
scalar = Desc((), "display") # ... this needs thinking...
4549

4650
require = {
4751
"x": desc,
4852
"y": desc,
4953
"codes": desc,
50-
"facecolor": scalar,
51-
"edgecolor": scalar,
54+
"facecolor": Desc((), "display"),
55+
"edgecolor": Desc(("M",), "display"),
5256
"linewidth": scalar,
5357
"linestyle": scalar,
5458
"hatch": scalar,
5559
"alpha": scalar,
5660
}
5761

58-
# copy from line
59-
conv = g.evaluator(self._container.describe(), require)
60-
query, _ = self._container.query(g)
61-
evald = conv.evaluate(query)
62-
63-
clip_conv = g.evaluator(
64-
self._clip_box.describe(),
65-
{"x": Desc(("N",), "display"), "y": Desc(("N",), "display")},
62+
evald = self._query_and_eval(
63+
self._container, require, graph, cacheset="default"
6664
)
67-
clip_query, _ = self._clip_box.query(g)
68-
clipx, clipy = clip_conv.evaluate(clip_query).values()
69-
# copy from line
65+
66+
clip_req = {"x": Desc(("N",), "display"), "y": Desc(("N",), "display")}
67+
clipx, clipy = self._query_and_eval(
68+
self._clip_box, clip_req, graph, cacheset="clip"
69+
).values()
7070

7171
path = mpath.Path._fast_from_codes_and_verts(
7272
verts=np.vstack([evald["x"], evald["y"]]).T, codes=evald["codes"]
@@ -75,7 +75,7 @@ def draw(self, renderer, graph: Graph) -> None:
7575
with _renderer_group(renderer, "patch", None):
7676
gc = renderer.new_gc()
7777

78-
gc.set_foreground(evald["facecolor"], isRGBA=False)
78+
gc.set_foreground(evald["edgecolor"], isRGBA=False)
7979
gc.set_clip_rectangle(
8080
mtransforms.Bbox.from_extents(clipx[0], clipy[0], clipx[1], clipy[1])
8181
)
@@ -111,6 +111,190 @@ def draw(self, renderer, graph: Graph) -> None:
111111
gc.restore()
112112

113113

114+
class RectangleContainer(DataContainer): ...
115+
116+
117+
class Rectangle(Patch):
118+
def __init__(self, container, edges=None, **kwargs):
119+
super().__init__(container, edges, **kwargs)
120+
121+
rect = mpath.Path.unit_rectangle()
122+
123+
desc = Desc((4,), "abstract_path")
124+
scalar = Desc((), "data")
125+
scalar_auto = Desc(())
126+
def_edges = [
127+
CoordinateEdge.from_coords(
128+
"llxycoords",
129+
{"lower_left_x": scalar_auto, "lower_left_y": scalar_auto},
130+
"data",
131+
),
132+
CoordinateEdge.from_coords(
133+
"urxycoords",
134+
{"upper_right_x": scalar_auto, "upper_right_y": scalar_auto},
135+
"data",
136+
),
137+
CoordinateEdge.from_coords(
138+
"rpxycoords",
139+
{"rotation_point_x": scalar_auto, "rotation_point_y": scalar_auto},
140+
"data",
141+
),
142+
CoordinateEdge.from_coords("anglecoords", {"angle": scalar_auto}, "data"),
143+
DefaultEdge.from_default_value(
144+
"x_def", "x", desc, rect.vertices.T[0], weight=0.1
145+
),
146+
DefaultEdge.from_default_value(
147+
"y_def", "y", desc, rect.vertices.T[1], weight=0.1
148+
),
149+
DefaultEdge.from_default_value(
150+
"codes_def",
151+
"codes",
152+
desc_like(desc, coordinates="display"),
153+
rect.codes,
154+
weight=0.1,
155+
),
156+
DefaultEdge.from_default_value("angle_def", "angle", scalar, 0),
157+
DefaultEdge.from_default_value(
158+
"rotation_point_x_def", "rotation_point_x", scalar, 0
159+
),
160+
DefaultEdge.from_default_value(
161+
"rotation_point_y_def", "rotation_point_y", scalar, 0
162+
),
163+
]
164+
165+
self._graph = self._graph + Graph(def_edges)
166+
167+
def _get_dynamic_graph(self, query, description, graph, cacheset):
168+
if cacheset == "clip":
169+
return Graph([])
170+
171+
desc = Desc((), "data")
172+
173+
requires = {
174+
"upper_right_x": desc,
175+
"upper_right_y": desc,
176+
"lower_left_x": desc,
177+
"lower_left_y": desc,
178+
"angle": desc,
179+
"rotation_point_x": desc,
180+
"rotation_point_y": desc,
181+
}
182+
183+
g = graph + self._graph
184+
185+
conv = g.evaluator(description, requires)
186+
evald = conv.evaluate(query)
187+
188+
bbox = mtransforms.Bbox.from_extents(
189+
evald["lower_left_x"],
190+
evald["lower_left_y"],
191+
evald["upper_right_x"],
192+
evald["upper_right_y"],
193+
)
194+
rotation_point = (evald["rotation_point_x"], evald["rotation_point_y"])
195+
196+
scale = mtransforms.BboxTransformTo(bbox)
197+
rotate = (
198+
mtransforms.Affine2D()
199+
.translate(-rotation_point[0], -rotation_point[1])
200+
.rotate_deg(evald["angle"])
201+
.translate(*rotation_point)
202+
)
203+
204+
descn: Desc = Desc(("N",), coordinates="data")
205+
xy: dict[str, Desc] = {"x": descn, "y": descn}
206+
edges = [
207+
TransformEdge(
208+
"scale_and_rotate",
209+
desc_like(xy, coordinates="abstract_path"),
210+
xy,
211+
transform=scale + rotate,
212+
)
213+
]
214+
215+
return Graph(edges)
216+
217+
218+
class RegularPolygon(Patch):
219+
def __init__(self, container, edges=None, **kwargs):
220+
super().__init__(container, edges, **kwargs)
221+
222+
scalar = Desc((), "data")
223+
scalar_auto = Desc(())
224+
def_edges = [
225+
CoordinateEdge.from_coords(
226+
"centercoords",
227+
{"center_x": scalar_auto, "center_y": scalar_auto},
228+
"data",
229+
),
230+
CoordinateEdge.from_coords(
231+
"orientationcoords", {"orientation": scalar_auto}, "data"
232+
),
233+
CoordinateEdge.from_coords("radiuscoords", {"radius": scalar_auto}, "data"),
234+
CoordinateEdge.from_coords(
235+
"num_vertices_coords", {"num_vertices": scalar_auto}, "data"
236+
),
237+
DefaultEdge.from_default_value("orientation_def", "orientation", scalar, 0),
238+
DefaultEdge.from_default_value("radius_def", "radius", scalar, 5),
239+
]
240+
241+
self._graph = self._graph + Graph(def_edges)
242+
243+
def _get_dynamic_graph(self, query, description, graph, cacheset):
244+
if cacheset == "clip":
245+
return Graph([])
246+
247+
desc = Desc((), "data")
248+
desc_abs = Desc(("N",), "abstract_path")
249+
250+
requires = {
251+
"center_x": desc,
252+
"center_y": desc,
253+
"radius": desc,
254+
"orientation": desc,
255+
"num_vertices": desc,
256+
}
257+
258+
g = graph + self._graph
259+
260+
conv = g.evaluator(description, requires)
261+
evald = conv.evaluate(query)
262+
263+
circ = mpath.Path.unit_regular_polygon(evald["num_vertices"])
264+
265+
scale = mtransforms.Affine2D().scale(evald["radius"])
266+
rotate = mtransforms.Affine2D().rotate(evald["orientation"])
267+
translate = mtransforms.Affine2D().translate(
268+
evald["center_x"], evald["center_y"]
269+
)
270+
271+
descn: Desc = Desc(("N",), coordinates="data")
272+
xy: dict[str, Desc] = {"x": descn, "y": descn}
273+
edges = [
274+
TransformEdge(
275+
"scale_and_rotate",
276+
desc_like(xy, coordinates="abstract_path"),
277+
xy,
278+
transform=scale + rotate + translate,
279+
),
280+
DefaultEdge.from_default_value(
281+
"x_def", "x", desc_abs, circ.vertices.T[0], weight=0.1
282+
),
283+
DefaultEdge.from_default_value(
284+
"y_def", "y", desc_abs, circ.vertices.T[1], weight=0.1
285+
),
286+
DefaultEdge.from_default_value(
287+
"codes_def",
288+
"codes",
289+
desc_like(desc_abs, coordinates="display"),
290+
circ.codes,
291+
weight=0.1,
292+
),
293+
]
294+
295+
return Graph(edges)
296+
297+
114298
class PatchWrapper(ProxyWrapper):
115299
_wrapped_class = _Patch
116300
_privtized_methods = (

‎examples/simple_patch.py

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,53 +12,63 @@
1212
import numpy as np
1313

1414
import matplotlib.pyplot as plt
15+
import matplotlib.patches as mpatches
1516

1617
from data_prototype.containers import ArrayContainer
18+
from data_prototype.artist import CompatibilityAxes
1719

18-
from data_prototype.patches import RectangleWrapper
20+
from data_prototype.patches import Rectangle
1921

2022
cont1 = ArrayContainer(
21-
x=np.array([-3]),
22-
y=np.array([0]),
23-
width=np.array([2]),
24-
height=np.array([3]),
25-
angle=np.array([0]),
26-
rotation_point=np.array(["center"]),
23+
lower_left_x=np.array(-3),
24+
lower_left_y=np.array(0),
25+
upper_right_x=np.array(-1),
26+
upper_right_y=np.array(3),
2727
edgecolor=np.array([0, 0, 0]),
28-
facecolor=np.array([0.0, 0.7, 0, 0.5]),
29-
linewidth=np.array([3]),
30-
linestyle=np.array(["-"]),
28+
hatch_color=np.array([0, 0, 0]),
29+
facecolor="green",
30+
linewidth=3,
31+
linestyle="-",
3132
antialiased=np.array([True]),
32-
hatch=np.array(["*"]),
33+
hatch="*",
3334
fill=np.array([True]),
3435
capstyle=np.array(["round"]),
3536
joinstyle=np.array(["miter"]),
37+
alpha=np.array(0.5),
3638
)
3739

3840
cont2 = ArrayContainer(
39-
x=np.array([0]),
40-
y=np.array([1]),
41-
width=np.array([2]),
42-
height=np.array([3]),
43-
angle=np.array([30]),
44-
rotation_point=np.array(["center"]),
45-
edgecolor=np.array([0, 0, 0]),
46-
facecolor=np.array([0.7, 0, 0]),
47-
linewidth=np.array([6]),
48-
linestyle=np.array(["-"]),
41+
lower_left_x=0,
42+
lower_left_y=np.array(1),
43+
upper_right_x=np.array(2),
44+
upper_right_y=np.array(5),
45+
angle=30,
46+
rotation_point_x=np.array(1),
47+
rotation_point_y=np.array(3.5),
48+
edgecolor=np.array([0.5, 0.2, 0]),
49+
hatch_color=np.array([0, 0, 0]),
50+
facecolor="red",
51+
linewidth=6,
52+
linestyle="-",
4953
antialiased=np.array([True]),
50-
hatch=np.array([""]),
54+
hatch="",
5155
fill=np.array([True]),
52-
capstyle=np.array(["butt"]),
53-
joinstyle=np.array(["round"]),
56+
capstyle=np.array(["round"]),
57+
joinstyle=np.array(["miter"]),
5458
)
5559

56-
fig, ax = plt.subplots()
57-
ax.set_xlim(-5, 5)
58-
ax.set_ylim(0, 5)
59-
rect1 = RectangleWrapper(cont1, {})
60-
rect2 = RectangleWrapper(cont2, {})
60+
fig, nax = plt.subplots()
61+
ax = CompatibilityAxes(nax)
62+
nax.add_artist(ax)
63+
nax.set_xlim(-5, 5)
64+
nax.set_ylim(0, 5)
65+
66+
rect = mpatches.Rectangle((4, 1), 2, 3, linewidth=6, edgecolor="black", angle=30)
67+
nax.add_artist(rect)
68+
69+
rect1 = Rectangle(cont1, {})
70+
rect2 = Rectangle(cont2, {})
6171
ax.add_artist(rect1)
6272
ax.add_artist(rect2)
63-
ax.set_aspect(1)
73+
nax.set_aspect(1)
6474
plt.show()

0 commit comments

Comments
 (0)
Please sign in to comment.