Skip to content

Commit 0dec1bc

Browse files
committed
Add snap to SpanSelector
1 parent 574580c commit 0dec1bc

File tree

2 files changed

+51
-3
lines changed

2 files changed

+51
-3
lines changed

lib/matplotlib/tests/test_widgets.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@ def test_rectangle_selector():
6767
@pytest.mark.parametrize('spancoords', ['data', 'pixels'])
6868
@pytest.mark.parametrize('minspanx, x1', [[0, 10], [1, 10.5], [1, 11]])
6969
@pytest.mark.parametrize('minspany, y1', [[0, 10], [1, 10.5], [1, 11]])
70-
def test_rectangle_minspan(spancoords, minspanx, x1, minspany, y1):
71-
ax = get_ax()
70+
def test_rectangle_minspan(ax, spancoords, minspanx, x1, minspany, y1):
7271
# attribute to track number of onselect calls
7372
ax._n_onselect = 0
7473

@@ -924,6 +923,37 @@ def mean(vmin, vmax):
924923
assert ln2.stale is False
925924

926925

926+
def test_snapping_values_span_selector(ax):
927+
def onselect(*args):
928+
pass
929+
930+
tool = widgets.SpanSelector(ax, onselect, direction='horizontal',)
931+
snap_function = tool._snap
932+
933+
snap_values = np.linspace(0, 5, 11)
934+
values = np.array([-0.1, 0.1, 0.2, 0.5, 0.6, 0.7, 0.9, 4.76, 5.0, 5.5])
935+
expect = np.array([00.0, 0.0, 0.0, 0.5, 0.5, 0.5, 1.0, 5.00, 5.0, 5.0])
936+
values = snap_function(values, snap_values)
937+
assert_allclose(values, expect)
938+
939+
940+
def test_span_selector_snap(ax):
941+
def onselect(vmin, vmax):
942+
ax._got_onselect = True
943+
944+
snap_values = np.arange(50) * 4
945+
946+
tool = widgets.SpanSelector(ax, onselect, direction='horizontal',
947+
snap_values=snap_values)
948+
tool.extents = (17, 35)
949+
assert tool.extents == (16, 36)
950+
951+
tool.snap_values = None
952+
assert tool.snap_values is None
953+
tool.extents = (17, 35)
954+
assert tool.extents == (17, 35)
955+
956+
927957
def check_lasso_selector(**kwargs):
928958
ax = get_ax()
929959

lib/matplotlib/widgets.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2238,6 +2238,10 @@ def on_select(min: float, max: float) -> Any
22382238
If `True`, the event triggered outside the span selector will be
22392239
ignored.
22402240
2241+
snap_values : 1D array-like, default: None
2242+
Snap the extents of the selector to the values defined in
2243+
``snap_values``.
2244+
22412245
Examples
22422246
--------
22432247
>>> import matplotlib.pyplot as plt
@@ -2259,7 +2263,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False,
22592263
props=None, onmove_callback=None, interactive=False,
22602264
button=None, handle_props=None, grab_range=10,
22612265
state_modifier_keys=None, drag_from_anywhere=False,
2262-
ignore_event_outside=False):
2266+
ignore_event_outside=False, snap_values=None):
22632267

22642268
if state_modifier_keys is None:
22652269
state_modifier_keys = dict(clear='escape',
@@ -2278,6 +2282,7 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False,
22782282

22792283
self.visible = True
22802284
self._extents_on_press = None
2285+
self.snap_values = snap_values
22812286

22822287
# self._pressv is deprecated and we don't use it internally anymore
22832288
# but we maintain it until it is removed
@@ -2577,6 +2582,16 @@ def _contains(self, event):
25772582
"""Return True if event is within the patch."""
25782583
return self._selection_artist.contains(event, radius=0)[0]
25792584

2585+
@staticmethod
2586+
def _snap(values, snap_values):
2587+
"""Snap values to a given array values (snap_values)."""
2588+
indices = np.empty_like(values, dtype="uint")
2589+
# take into account machine precision
2590+
eps = np.min(np.abs(np.diff(snap_values))) * 1e-12
2591+
for i, v in enumerate(values):
2592+
indices[i] = np.abs(snap_values - v + np.sign(v) * eps).argmin()
2593+
return snap_values[indices]
2594+
25802595
@property
25812596
def extents(self):
25822597
"""Return extents of the span selector."""
@@ -2591,6 +2606,8 @@ def extents(self):
25912606
@extents.setter
25922607
def extents(self, extents):
25932608
# Update displayed shape
2609+
if self.snap_values is not None:
2610+
extents = tuple(self._snap(extents, self.snap_values))
25942611
self._draw_shape(*extents)
25952612
if self._interactive:
25962613
# Update displayed handles
@@ -2857,6 +2874,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent)
28572874
use_data_coordinates : bool, default: False
28582875
If `True`, the "square" shape of the selector is defined in
28592876
data coordinates instead of display coordinates.
2877+
28602878
"""
28612879

28622880

0 commit comments

Comments
 (0)