Skip to content

Consolidate caching logic, implement patch subclasses #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 4, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions data_prototype/artist.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from bisect import insort
from collections import OrderedDict
from typing import Sequence
from contextlib import contextmanager

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

self._caches = {}

def draw(self, renderer, graph: Graph) -> None:
return

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

def _get_dynamic_graph(self, query, description, graph, cacheset):
return Graph([])

def _query_and_eval(self, container, requires, graph, cacheset=None):
g = graph + self._graph
query, q_cache_key = container.query(g)
g = g + self._get_dynamic_graph(query, container.describe(), graph, cacheset)
g_cache_key = g.cache_key()
cache_key = (g_cache_key, q_cache_key)

cache = None
if cacheset is not None:
cache = self._caches.setdefault(cacheset, OrderedDict())
if cache_key in cache:
return cache[cache_key]

conv = g.evaluator(container.describe(), requires)
ret = conv.evaluate(query)

# TODO: actually add to cache and prune
# if cache is not None:
# cache[cache_key] = ret

return ret


class CompatibilityArtist:
"""A compatibility shim to ducktype as a classic Matplotlib Artist.
9 changes: 9 additions & 0 deletions data_prototype/conversion_edge.py
Original file line number Diff line number Diff line change
@@ -418,3 +418,12 @@ def __add__(self, other: Graph) -> Graph:
aother = {k: v for k, v in other._aliases}
aliases = tuple((aself | aother).items())
return Graph(self._edges + other._edges, aliases)

def cache_key(self):
"""A cache key representing the graph.

Current implementation is a new UUID, that is to say uncachable.
"""
import uuid

return str(uuid.uuid4())
230 changes: 207 additions & 23 deletions data_prototype/patches.py
Original file line number Diff line number Diff line change
@@ -7,23 +7,28 @@

from .wrappers import ProxyWrapper, _stale_wrapper

from .containers import DataContainer

from .artist import Artist, _renderer_group
from .description import Desc
from .conversion_edge import Graph, CoordinateEdge, DefaultEdge
from .description import Desc, desc_like
from .containers import DataContainer
from .conversion_edge import Graph, CoordinateEdge, DefaultEdge, TransformEdge


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

scalar = Desc((), "display") # ... this needs thinking...
edges = [
def_edges = [
CoordinateEdge.from_coords("xycoords", {"x": "auto", "y": "auto"}, "data"),
CoordinateEdge.from_coords("codes", {"codes": "auto"}, "display"),
CoordinateEdge.from_coords("facecolor", {"color": Desc(())}, "display"),
CoordinateEdge.from_coords("edgecolor", {"color": Desc(())}, "display"),
CoordinateEdge.from_coords("facecolor", {"facecolor": Desc(())}, "display"),
CoordinateEdge.from_coords("edgecolor", {"edgecolor": Desc(())}, "display"),
CoordinateEdge.from_coords(
"facecolor_rgba", {"facecolor": Desc(("M",))}, "display"
),
CoordinateEdge.from_coords(
"edgecolor_rgba", {"edgecolor": Desc(("M",))}, "display"
),
CoordinateEdge.from_coords("linewidth", {"linewidth": Desc(())}, "display"),
CoordinateEdge.from_coords("hatch", {"hatch": Desc(())}, "display"),
CoordinateEdge.from_coords("alpha", {"alpha": Desc(())}, "display"),
@@ -34,39 +39,34 @@ def __init__(self, container, edges=None, **kwargs):
DefaultEdge.from_default_value("alpha_def", "alpha", scalar, 1),
DefaultEdge.from_default_value("hatch_def", "hatch", scalar, None),
]
self._graph = self._graph + Graph(edges)
self._graph = self._graph + Graph(def_edges)

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

require = {
"x": desc,
"y": desc,
"codes": desc,
"facecolor": scalar,
"edgecolor": scalar,
"facecolor": Desc((), "display"),
"edgecolor": Desc(("M",), "display"),
"linewidth": scalar,
"linestyle": scalar,
"hatch": scalar,
"alpha": scalar,
}

# copy from line
conv = g.evaluator(self._container.describe(), require)
query, _ = self._container.query(g)
evald = conv.evaluate(query)

clip_conv = g.evaluator(
self._clip_box.describe(),
{"x": Desc(("N",), "display"), "y": Desc(("N",), "display")},
evald = self._query_and_eval(
self._container, require, graph, cacheset="default"
)
clip_query, _ = self._clip_box.query(g)
clipx, clipy = clip_conv.evaluate(clip_query).values()
# copy from line

clip_req = {"x": Desc(("N",), "display"), "y": Desc(("N",), "display")}
clipx, clipy = self._query_and_eval(
self._clip_box, clip_req, graph, cacheset="clip"
).values()

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

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


class RectangleContainer(DataContainer): ...


class Rectangle(Patch):
def __init__(self, container, edges=None, **kwargs):
super().__init__(container, edges, **kwargs)

rect = mpath.Path.unit_rectangle()

desc = Desc((4,), "abstract_path")
scalar = Desc((), "data")
scalar_auto = Desc(())
def_edges = [
CoordinateEdge.from_coords(
"llxycoords",
{"lower_left_x": scalar_auto, "lower_left_y": scalar_auto},
"data",
),
CoordinateEdge.from_coords(
"urxycoords",
{"upper_right_x": scalar_auto, "upper_right_y": scalar_auto},
"data",
),
CoordinateEdge.from_coords(
"rpxycoords",
{"rotation_point_x": scalar_auto, "rotation_point_y": scalar_auto},
"data",
),
CoordinateEdge.from_coords("anglecoords", {"angle": scalar_auto}, "data"),
DefaultEdge.from_default_value(
"x_def", "x", desc, rect.vertices.T[0], weight=0.1
),
DefaultEdge.from_default_value(
"y_def", "y", desc, rect.vertices.T[1], weight=0.1
),
DefaultEdge.from_default_value(
"codes_def",
"codes",
desc_like(desc, coordinates="display"),
rect.codes,
weight=0.1,
),
DefaultEdge.from_default_value("angle_def", "angle", scalar, 0),
DefaultEdge.from_default_value(
"rotation_point_x_def", "rotation_point_x", scalar, 0
),
DefaultEdge.from_default_value(
"rotation_point_y_def", "rotation_point_y", scalar, 0
),
]

self._graph = self._graph + Graph(def_edges)

def _get_dynamic_graph(self, query, description, graph, cacheset):
if cacheset == "clip":
return Graph([])

desc = Desc((), "data")

requires = {
"upper_right_x": desc,
"upper_right_y": desc,
"lower_left_x": desc,
"lower_left_y": desc,
"angle": desc,
"rotation_point_x": desc,
"rotation_point_y": desc,
}

g = graph + self._graph

conv = g.evaluator(description, requires)
evald = conv.evaluate(query)

bbox = mtransforms.Bbox.from_extents(
evald["lower_left_x"],
evald["lower_left_y"],
evald["upper_right_x"],
evald["upper_right_y"],
)
rotation_point = (evald["rotation_point_x"], evald["rotation_point_y"])

scale = mtransforms.BboxTransformTo(bbox)
rotate = (
mtransforms.Affine2D()
.translate(-rotation_point[0], -rotation_point[1])
.rotate_deg(evald["angle"])
.translate(*rotation_point)
)

descn: Desc = Desc(("N",), coordinates="data")
xy: dict[str, Desc] = {"x": descn, "y": descn}
edges = [
TransformEdge(
"scale_and_rotate",
desc_like(xy, coordinates="abstract_path"),
xy,
transform=scale + rotate,
)
]

return Graph(edges)


class RegularPolygon(Patch):
def __init__(self, container, edges=None, **kwargs):
super().__init__(container, edges, **kwargs)

scalar = Desc((), "data")
scalar_auto = Desc(())
def_edges = [
CoordinateEdge.from_coords(
"centercoords",
{"center_x": scalar_auto, "center_y": scalar_auto},
"data",
),
CoordinateEdge.from_coords(
"orientationcoords", {"orientation": scalar_auto}, "data"
),
CoordinateEdge.from_coords("radiuscoords", {"radius": scalar_auto}, "data"),
CoordinateEdge.from_coords(
"num_vertices_coords", {"num_vertices": scalar_auto}, "data"
),
DefaultEdge.from_default_value("orientation_def", "orientation", scalar, 0),
DefaultEdge.from_default_value("radius_def", "radius", scalar, 5),
]

self._graph = self._graph + Graph(def_edges)

def _get_dynamic_graph(self, query, description, graph, cacheset):
if cacheset == "clip":
return Graph([])

desc = Desc((), "data")
desc_abs = Desc(("N",), "abstract_path")

requires = {
"center_x": desc,
"center_y": desc,
"radius": desc,
"orientation": desc,
"num_vertices": desc,
}

g = graph + self._graph

conv = g.evaluator(description, requires)
evald = conv.evaluate(query)

circ = mpath.Path.unit_regular_polygon(evald["num_vertices"])

scale = mtransforms.Affine2D().scale(evald["radius"])
rotate = mtransforms.Affine2D().rotate(evald["orientation"])
translate = mtransforms.Affine2D().translate(
evald["center_x"], evald["center_y"]
)

descn: Desc = Desc(("N",), coordinates="data")
xy: dict[str, Desc] = {"x": descn, "y": descn}
edges = [
TransformEdge(
"scale_and_rotate",
desc_like(xy, coordinates="abstract_path"),
xy,
transform=scale + rotate + translate,
),
DefaultEdge.from_default_value(
"x_def", "x", desc_abs, circ.vertices.T[0], weight=0.1
),
DefaultEdge.from_default_value(
"y_def", "y", desc_abs, circ.vertices.T[1], weight=0.1
),
DefaultEdge.from_default_value(
"codes_def",
"codes",
desc_like(desc_abs, coordinates="display"),
circ.codes,
weight=0.1,
),
]

return Graph(edges)


class PatchWrapper(ProxyWrapper):
_wrapped_class = _Patch
_privtized_methods = (
70 changes: 40 additions & 30 deletions examples/simple_patch.py
Original file line number Diff line number Diff line change
@@ -12,53 +12,63 @@
import numpy as np

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

from data_prototype.containers import ArrayContainer
from data_prototype.artist import CompatibilityAxes

from data_prototype.patches import RectangleWrapper
from data_prototype.patches import Rectangle

cont1 = ArrayContainer(
x=np.array([-3]),
y=np.array([0]),
width=np.array([2]),
height=np.array([3]),
angle=np.array([0]),
rotation_point=np.array(["center"]),
lower_left_x=np.array(-3),
lower_left_y=np.array(0),
upper_right_x=np.array(-1),
upper_right_y=np.array(3),
edgecolor=np.array([0, 0, 0]),
facecolor=np.array([0.0, 0.7, 0, 0.5]),
linewidth=np.array([3]),
linestyle=np.array(["-"]),
hatch_color=np.array([0, 0, 0]),
facecolor="green",
linewidth=3,
linestyle="-",
antialiased=np.array([True]),
hatch=np.array(["*"]),
hatch="*",
fill=np.array([True]),
capstyle=np.array(["round"]),
joinstyle=np.array(["miter"]),
alpha=np.array(0.5),
)

cont2 = ArrayContainer(
x=np.array([0]),
y=np.array([1]),
width=np.array([2]),
height=np.array([3]),
angle=np.array([30]),
rotation_point=np.array(["center"]),
edgecolor=np.array([0, 0, 0]),
facecolor=np.array([0.7, 0, 0]),
linewidth=np.array([6]),
linestyle=np.array(["-"]),
lower_left_x=0,
lower_left_y=np.array(1),
upper_right_x=np.array(2),
upper_right_y=np.array(5),
angle=30,
rotation_point_x=np.array(1),
rotation_point_y=np.array(3.5),
edgecolor=np.array([0.5, 0.2, 0]),
hatch_color=np.array([0, 0, 0]),
facecolor="red",
linewidth=6,
linestyle="-",
antialiased=np.array([True]),
hatch=np.array([""]),
hatch="",
fill=np.array([True]),
capstyle=np.array(["butt"]),
joinstyle=np.array(["round"]),
capstyle=np.array(["round"]),
joinstyle=np.array(["miter"]),
)

fig, ax = plt.subplots()
ax.set_xlim(-5, 5)
ax.set_ylim(0, 5)
rect1 = RectangleWrapper(cont1, {})
rect2 = RectangleWrapper(cont2, {})
fig, nax = plt.subplots()
ax = CompatibilityAxes(nax)
nax.add_artist(ax)
nax.set_xlim(-5, 5)
nax.set_ylim(0, 5)

rect = mpatches.Rectangle((4, 1), 2, 3, linewidth=6, edgecolor="black", angle=30)
nax.add_artist(rect)

rect1 = Rectangle(cont1, {})
rect2 = Rectangle(cont2, {})
ax.add_artist(rect1)
ax.add_artist(rect2)
ax.set_aspect(1)
nax.set_aspect(1)
plt.show()