Skip to content

Commit 55974ef

Browse files
committed
fix(plot2d): a capped rectangle is born at the cap, not half the image
add_rectangle_widget's default size is half the image, so a widget created with max_extent appeared huge and then snapped down to the cap the first time it was used — which reads as a bug ("the ROI is always huge and then it clamps down"). The default now honours max_extent per axis. An explicit w/h still wins, and an uncapped widget is unchanged.
1 parent ed70965 commit 55974ef

2 files changed

Lines changed: 55 additions & 2 deletions

File tree

anyplotlib/plot2d/_plot2d.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,11 +1620,25 @@ def add_rectangle_widget(self, x: float | None = None, y: float | None = None,
16201620
(the dragged corner pins, the opposite corner stays put).
16211621
"""
16221622
iw, ih = self._state["image_width"], self._state["image_height"]
1623+
# The DEFAULT size honours max_extent. Without this a capped rectangle is
1624+
# born at half the image and then snaps down to the cap on first use —
1625+
# the ROI visibly appears huge and collapses, which looks like a bug.
1626+
def _cap(axis_default, cap):
1627+
return axis_default if cap is None else min(axis_default, float(cap))
1628+
1629+
if max_extent is None:
1630+
cap_w = cap_h = None
1631+
elif isinstance(max_extent, (tuple, list)):
1632+
cap_w, cap_h = max_extent[0], max_extent[1]
1633+
else:
1634+
cap_w = cap_h = max_extent
1635+
def_w = _cap(iw * 0.5, cap_w)
1636+
def_h = _cap(ih * 0.5, cap_h)
16231637
widget = RectangleWidget(lambda: None,
16241638
x=float(x) if x is not None else iw * 0.25,
16251639
y=float(y) if y is not None else ih * 0.25,
1626-
w=float(w) if w is not None else iw * 0.5,
1627-
h=float(h) if h is not None else ih * 0.5,
1640+
w=float(w) if w is not None else def_w,
1641+
h=float(h) if h is not None else def_h,
16281642
color=color, linewidth=linewidth,
16291643
show_handles=show_handles,
16301644
max_extent=max_extent)

anyplotlib/tests/test_interactive/test_widget_max_extent.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,45 @@ def test_add_rectangle_widget_passes_max_extent(self):
9191
assert w.max_w == 8.0 and w.max_h == 12.0
9292

9393

94+
class TestDefaultSizeHonoursTheCap:
95+
"""A capped rectangle must be BORN at the cap.
96+
97+
The default size is half the image, so a capped ROI used to appear huge and
98+
then snap down to the cap on first use — which reads as a bug ("the ROI is
99+
always huge and then it clamps down")."""
100+
101+
def test_default_size_is_capped(self):
102+
fig, ax = apl.subplots(1, 1)
103+
v = ax.imshow(np.zeros((256, 256), dtype=np.float32))
104+
w = v.add_rectangle_widget(max_extent=16.0)
105+
assert w.w == 16.0 and w.h == 16.0
106+
107+
def test_default_size_respects_a_per_axis_cap(self):
108+
fig, ax = apl.subplots(1, 1)
109+
v = ax.imshow(np.zeros((256, 256), dtype=np.float32))
110+
w = v.add_rectangle_widget(max_extent=(8, 32))
111+
assert w.w == 8.0 and w.h == 32.0
112+
113+
def test_uncapped_default_is_unchanged(self):
114+
fig, ax = apl.subplots(1, 1)
115+
v = ax.imshow(np.zeros((256, 256), dtype=np.float32))
116+
w = v.add_rectangle_widget()
117+
assert w.w == 128.0 and w.h == 128.0 # half the image, as before
118+
119+
def test_explicit_size_wins_over_the_cap_default(self):
120+
"""An explicit w/h is the caller's business; the cap only bounds drags."""
121+
fig, ax = apl.subplots(1, 1)
122+
v = ax.imshow(np.zeros((256, 256), dtype=np.float32))
123+
w = v.add_rectangle_widget(w=4, h=4, max_extent=16.0)
124+
assert w.w == 4.0 and w.h == 4.0
125+
126+
def test_cap_larger_than_the_image_does_not_inflate_the_default(self):
127+
fig, ax = apl.subplots(1, 1)
128+
v = ax.imshow(np.zeros((32, 32), dtype=np.float32))
129+
w = v.add_rectangle_widget(max_extent=1000.0)
130+
assert w.w == 16.0 and w.h == 16.0 # still half the image
131+
132+
94133
# ═══════════════════════════════════════════════════════════════════════════
95134
# 2. Drag behaviour in the browser — the logic lives in figure_esm.js, so a
96135
# Python-only test would prove nothing about what the user experiences.

0 commit comments

Comments
 (0)