diff --git a/data_prototype/patches.py b/data_prototype/patches.py index 94a50af..3229e55 100644 --- a/data_prototype/patches.py +++ b/data_prototype/patches.py @@ -3,7 +3,14 @@ from .wrappers import ProxyWrapper, _stale_wrapper from .containers import DataContainer -from matplotlib.patches import Patch as _Patch, Rectangle as _Rectangle +from matplotlib.patches import ( + Patch as _Patch, + Annulus as _Annulus, + Ellipse as _Ellipse, + Circle as _Circle, + Arc as _Arc, + Rectangle as _Rectangle, +) class PatchWrapper(ProxyWrapper): @@ -46,7 +53,14 @@ class PatchWrapper(ProxyWrapper): def __init__(self, data: DataContainer, nus=None, /, **kwargs): super().__init__(data, nus) - self._wrapped_instance = self._wrapped_class([0, 0], 0, 0, **kwargs) + # FIXME this is a bit of an ugly hack, basically we want a null instance + # because all attributes are determined at draw time, but each class has different signatures + # The one used here is surprisingly common, but perhaps instantiation should be pushed to the subclasses + # Additionally, hasattr breaks for recursion depth which should be looked at + try: + self._wrapped_instance = self._wrapped_class((0, 0), 0, **kwargs) + except TypeError: + self._wrapped_instance = self._wrapped_class((0, 0), 1, 0, **kwargs) @_stale_wrapper def draw(self, renderer): @@ -55,6 +69,11 @@ def draw(self, renderer): def _update_wrapped(self, data): for k, v in data.items(): + # linestyle and hatch do not work as arrays, + # but ArrayContainer requires arrays, so index into an array if needed + if k in ("linestyle", "hatch"): + if isinstance(v, np.ndarray): + v = v[0] getattr(self._wrapped_instance, f"set_{k}")(v) @@ -66,7 +85,8 @@ class RectangleWrapper(PatchWrapper): "get_width", "get_height", "get_angle", - "get_rotation_point" "set_x", + "get_rotation_point", + "set_x", "set_y", "set_width", "set_height", @@ -79,6 +99,7 @@ class RectangleWrapper(PatchWrapper): def _update_wrapped(self, data): for k, v in data.items(): + # rotation_point is a property without a set_rotation_point method if k == "rotation_point": self._wrapped_instance.rotation_point = v continue @@ -88,3 +109,94 @@ def _update_wrapped(self, data): if isinstance(v, np.ndarray): v = v[0] getattr(self._wrapped_instance, f"set_{k}")(v) + + +class AnnulusWrapper(PatchWrapper): + _wrapped_class = _Annulus + _privtized_methods = PatchWrapper._privtized_methods + ( + "get_angle", + "get_center", + "get_radii", + "get_width", + "set_angle", + "set_center", + "set_radii", + "set_width", + # set_semi[major|minor] overlap with set_radii + ) + # TODO: units, because "center" is one x and one y units + # Other things like semi-axis and width seem to _not_ be unit-ed, but maybe _could_ be + _xunits = () + _yunits = () + # Order is actually important... radii must come before width + required_keys = PatchWrapper.required_keys | {"center", "radii", "width", "angle"} + + +class EllipseWrapper(PatchWrapper): + _wrapped_class = _Ellipse + _privtized_methods = PatchWrapper._privtized_methods + ( + "get_angle", + "get_center", + "get_width", + "get_height", + "set_angle", + "set_center", + "set_width", + "set_height", + ) + # TODO: units, because "center" is one x and one y units + _xunits = ("width",) + _yunits = ("height",) + required_keys = PatchWrapper.required_keys | {"center", "width", "height", "angle"} + + +# While the actual patch inherits from ellipse, using it as a full ellipse doesn't make sense +# Therefore, the privitized methods, required keys, etc are not inheritied from EllipseWrapper +class CircleWrapper(PatchWrapper): + _wrapped_class = _Circle + _privtized_methods = PatchWrapper._privtized_methods + ( + "get_radius", + "get_center", + "set_radius", + "set_center", + ) + # TODO: units, because "center" is one x and one y units + # And "radius" is _both_ x and y units, so may not make sense unless units are the same + _xunits = () + _yunits = () + required_keys = PatchWrapper.required_keys | {"center", "radius"} + + +# Unlike Circle, Arc maintains width/height/etc from Ellipse +class ArcWrapper(EllipseWrapper): + _wrapped_class = _Arc + _privtized_methods = EllipseWrapper._privtized_methods + ( + "get_radius", + "get_center", + "get_theta1", + "get_theta2", + "set_radius", + "set_center", + "set_theta1", + "set_theta2", + ) + # TODO: units, because "center" is one x and one y units + # And theta1/2 could arguably pass through a units pipeline, but not the x/y one... + _xunits = () + _yunits = () + required_keys = EllipseWrapper.required_keys | {"theta1", "theta2"} + + def _update_wrapped(self, data): + for k, v in data.items(): + # theta[1,2] are properties without a set_rotation_point method + if k.startswith("theta"): + if isinstance(v, np.ndarray): + v = v[0] + setattr(self._wrapped_instance, k, v) + continue + # linestyle and hatch do not work as arrays, + # but ArrayContainer requires arrays, so index into an array if needed + elif k in ("linestyle", "hatch"): + if isinstance(v, np.ndarray): + v = v[0] + getattr(self._wrapped_instance, f"set_{k}")(v) diff --git a/data_prototype/tests/test_containers.py b/data_prototype/tests/test_containers.py index ddea3c6..4fadefb 100644 --- a/data_prototype/tests/test_containers.py +++ b/data_prototype/tests/test_containers.py @@ -14,7 +14,6 @@ def ac(): def _verify_describe(container): - data, cache_key = container.query(IdentityTransform(), [100, 100]) desc = container.describe() diff --git a/examples/simple_patch.py b/examples/simple_patch.py index 48f1c2a..22410a6 100644 --- a/examples/simple_patch.py +++ b/examples/simple_patch.py @@ -14,7 +14,7 @@ from data_prototype.containers import ArrayContainer -from data_prototype.patches import RectangleWrapper +from data_prototype.patches import RectangleWrapper, CircleWrapper, AnnulusWrapper, EllipseWrapper cont1 = ArrayContainer( x=np.array([-3]), @@ -35,15 +35,11 @@ ) 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"]), + center=np.array([0, 1]), + radius=np.array([0.8]), edgecolor=np.array([0, 0, 0]), facecolor=np.array([0.7, 0, 0]), - linewidth=np.array([6]), + linewidth=np.array([3]), linestyle=np.array(["-"]), antialiased=np.array([True]), hatch=np.array([""]), @@ -52,12 +48,56 @@ joinstyle=np.array(["round"]), ) +cont3 = ArrayContainer( + center=np.array([0, 4]), + width=np.array([2]), + height=np.array([1]), + angle=np.array([0.3]), + edgecolor=np.array([0, 0, 0.7]), + facecolor=np.array([0, 0.7, 0]), + linewidth=np.array([3]), + linestyle=np.array([":"]), + antialiased=np.array([True]), + hatch=np.array(["/"]), + fill=np.array([True]), + capstyle=np.array(["butt"]), + joinstyle=np.array(["round"]), +) + +cont4 = ArrayContainer( + center=np.array([1, 4]), + radii=np.array([3, 1]), + width=np.array([0.4]), + height=np.array([1]), + angle=np.array([0.3]), + theta1=np.array([0]), + theta2=np.array([2]), + edgecolor=np.array([0, 0.7, 0]), + facecolor=np.array([0.7, 0, 0.7]), + linewidth=np.array([3]), + linestyle=np.array(["-"]), + antialiased=np.array([True]), + hatch=np.array(["+"]), + fill=np.array([True]), + capstyle=np.array(["butt"]), + joinstyle=np.array(["round"]), +) + fig, ax = plt.subplots() ax.set_xlim(-5, 5) ax.set_ylim(0, 5) -rect1 = RectangleWrapper(cont1, {}) -rect2 = RectangleWrapper(cont2, {}) -ax.add_artist(rect1) -ax.add_artist(rect2) +rect = RectangleWrapper(cont1, {}) +circ = CircleWrapper(cont2, {}) +ellipse = EllipseWrapper(cont3, {}) + +# ArcWrapper is still broken due to no setters of theta1/2 +# arc = ArcWrapper(cont4, {}) + +annulus = AnnulusWrapper(cont4, {}) +ax.add_artist(rect) +ax.add_artist(circ) +ax.add_artist(ellipse) +# ax.add_artist(arc) +ax.add_artist(annulus) ax.set_aspect(1) plt.show()