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
Show file tree
Hide file tree
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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions data_prototype/conversion_edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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"),
Expand All @@ -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"]
Expand All @@ -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])
)
Expand Down Expand Up @@ -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 = (
Expand Down
Loading
Loading