Skip to content

Commit

Permalink
Build out FlexibleRect implementation
Browse files Browse the repository at this point in the history
Move `plotobj` test fixture into `conftest` & change to a yielding fixture so the plots can be cleared during teardown. This prevents a bazillion plots being created (which matplotlib eventually warns about).
  • Loading branch information
sco1 committed Jun 27, 2024
1 parent 3efe219 commit 7e23b61
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 35 deletions.
55 changes: 24 additions & 31 deletions matplotlib_window/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ def validate_snap_to(self, snap_to: Line2D | None) -> Line2D | None:

return snap_to

def _disable_click(self) -> None:
"""Disconnect the button press event for the current instance."""
self.parent_canvas.mpl_disconnect(self.click_press)
self.click_press = -1


def limit_drag(plotted_data: npt.ArrayLike, query: float) -> float:
"""Clamp the query value within the bounds of the provided dataset."""
Expand Down Expand Up @@ -350,13 +355,13 @@ def __init__(
position: NUMERIC_T,
width: NUMERIC_T,
snap_to: Line2D | None = None,
edgecolor: str = "limegreen",
edgecolor: str | None = "limegreen",
facecolor: str = "limegreen",
alpha: NUMERIC_T = 0.4,
**kwargs: t.Any,
) -> None:
if width <= 0:
raise ValueError(f"Width value must be greater than 1. Received: {width}")
raise ValueError(f"Width value must be greater than 0. Received: {width}")

# Rectangle patches are located from their bottom left corner; because we want to span the
# full y range, we need to translate the y position to the bottom of the axes
Expand Down Expand Up @@ -477,7 +482,7 @@ def bounds(self) -> tuple[NUMERIC_T, NUMERIC_T]:
return l_pos, (l_pos + self.myobj.get_width())


class FlexibleRect(_DraggableObject):
class FlexibleRect:
"""
A flexible-width rectangle.
Expand All @@ -495,42 +500,30 @@ def __init__(
position: NUMERIC_T,
width: NUMERIC_T,
snap_to: Line2D | None = None,
allow_face_drag: bool = False,
edgecolor: str = "limegreen",
facecolor: str = "limegreen",
alpha: NUMERIC_T = 0.4,
) -> None:
if width <= 0:
raise ValueError(f"Width value must be greater than 1. Received: {width}")
raise ValueError(f"Width value must be greater than 0. Received: {width}")

raise NotImplementedError

def on_motion(self, event: Event) -> t.Any:
raise NotImplementedError

def limit_change(self, ax: Axes) -> None:
"""
Axes limit change callback.
Resize the rectangle to span the entirety of the y-axis if the axis limit is changed.
"""
raise NotImplementedError

def on_release(self, event: Event) -> t.Any:
raise NotImplementedError

def validate_snap_to(self, snap_to: Line2D | None) -> Line2D | None:
"""
Validate that the `snap_to` object, if provided, actually contains x data.
If `snap_to` is `None`, or is a plot object that contains x data, it is returned unchanged.
Otherwise an exception is raised.
# snap_to validation handled by DragRect & DragLine
# Create edges after face so they're topmost & take click priority
self.face = DragRect(
ax=ax, position=position, width=width, facecolor=facecolor, edgecolor=None, alpha=alpha
)
self.edges = [
DragLine(ax=ax, position=position, color=edgecolor, snap_to=snap_to),
DragLine(ax=ax, position=(position + width), color=edgecolor, snap_to=snap_to),
]

NOTE: This should be called after the draggable object is registered so the object is
instantiated & references are set.
"""
raise NotImplementedError
if not allow_face_drag:
self.face._disable_click()
else:
raise NotImplementedError

@property
def bounds(self) -> tuple[NUMERIC_T, NUMERIC_T]:
"""Return the x-axis locations of the left & right edges."""
raise NotImplementedError
return tuple(sorted(edge.location for edge in self.edges)) # type: ignore[return-value]
6 changes: 4 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@


@pytest.fixture
def plotobj() -> tuple[Figure, Axes]:
def plotobj() -> t.Generator[tuple[Figure, Axes], None, None]:
fig, ax = plt.subplots()
return fig, ax
yield fig, ax

plt.close()
18 changes: 18 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from matplotlib.backend_bases import FigureCanvasBase


def has_callback_to(
parent_canvas: FigureCanvasBase, query_obj: str, event: str = "button_press_event"
) -> bool:
"""
Check if any of the parent Canvas' callbacks for the provided `event` reference `query_obj`.
This is a bit of a hack since it checks the weakref's `repr` output, but seems to work ok.
"""
callbacks = parent_canvas.callbacks.callbacks.get(event, {})

for ref in callbacks.values():
if query_obj in repr(ref):
return True

return False
File renamed without changes.
37 changes: 35 additions & 2 deletions tests/test_base_objs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import pytest

from matplotlib_window.base import DragLine, DragRect, Orientation
from matplotlib_window.base import DragLine, DragRect, FlexibleRect, Orientation
from tests.conftest import PLOTOBJ_T
from tests.helpers import has_callback_to


def test_dragline_invalid_orientation_raises(plotobj: PLOTOBJ_T) -> None:
Expand All @@ -27,11 +28,43 @@ def test_dragline_get_location(

def test_dragrect_invalid_width_raises(plotobj: PLOTOBJ_T) -> None:
_, ax = plotobj
with pytest.raises(ValueError, match="greater than 1"):
with pytest.raises(ValueError, match="greater than"):
_ = DragRect(ax=ax, position=0, width=0)


def test_dragrect_get_bounds(plotobj: PLOTOBJ_T) -> None:
_, ax = plotobj
dr = DragRect(ax=ax, position=0, width=1)
assert dr.bounds == (0, 1)


def test_dragrect_click_disable(plotobj: PLOTOBJ_T) -> None:
_, ax = plotobj
dr = DragRect(ax=ax, position=0, width=1)

parent_canvas = dr.parent_canvas
assert has_callback_to(parent_canvas, "DragRect")

dr._disable_click()
assert dr.click_press == -1
assert not has_callback_to(parent_canvas, "DragRect")


def test_flexrect_invalid_width_raises(plotobj: PLOTOBJ_T) -> None:
_, ax = plotobj
with pytest.raises(ValueError, match="greater than"):
_ = FlexibleRect(ax=ax, position=0, width=0)


def test_flexrect_get_bounds(plotobj: PLOTOBJ_T) -> None:
_, ax = plotobj
dr = FlexibleRect(ax=ax, position=0, width=1)
assert dr.bounds == (0, 1)


def test_flexrect_disables_click(plotobj: PLOTOBJ_T) -> None:
_, ax = plotobj
dr = FlexibleRect(ax=ax, position=0, width=1, allow_face_drag=False)

parent_canvas = dr.face.parent_canvas
assert not has_callback_to(parent_canvas, "DragRect")

0 comments on commit 7e23b61

Please sign in to comment.