Skip to content

Add PatchWrappers for Annulus, Ellipse, Circle, Arc #25

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
118 changes: 115 additions & 3 deletions data_prototype/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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)


Expand All @@ -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",
Expand All @@ -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
Expand All @@ -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)
1 change: 0 additions & 1 deletion data_prototype/tests/test_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ def ac():


def _verify_describe(container):

data, cache_key = container.query(IdentityTransform(), [100, 100])
desc = container.describe()

Expand Down
64 changes: 52 additions & 12 deletions examples/simple_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand All @@ -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([""]),
Expand All @@ -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()