Skip to content

Commit

Permalink
FIX: no longer use custom RangeSlider
Browse files Browse the repository at this point in the history
  • Loading branch information
ianhi committed Sep 10, 2024
1 parent aecd190 commit f1d7bd6
Show file tree
Hide file tree
Showing 3 changed files with 10 additions and 301 deletions.
36 changes: 6 additions & 30 deletions docs/examples/range-sliders.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
Expand All @@ -31,8 +32,8 @@
"In order to create a RangeSlider rather than a Slider you prefix the tuples with either `\"r\"` or `\"range\"`, then the rest of the tuple is created according to the rules from converting tuples to sliders. So arrays will remain arrays, or the values will be passed through to np.linspace as appropriate.\n",
"```\n",
"# passed through to np.linspace\n",
"(\"range\", min, max, [step]) \n",
"(\"r\", min, max, [step])\n",
"(\"range\", min, max, [num]) \n",
"(\"r\", min, max, [num])\n",
"\n",
"# array used directly\n",
"(\"range\", np.array)\n",
Expand Down Expand Up @@ -93,38 +94,13 @@
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"## Using a Matplotlib RangeSlider\n",
"## DEPRECATED+REMOVED - mpl_interactions `RangeSliders`\n",
"\n",
"### But maptlotlib doesn't have range sliders???!?!?\n",
"\n",
"One of the implicit promises of this library is that it will work equally well both in and out of a jupyter notebook. So it leverages ipywidgets when available but otherwise will use matplotlib widgets. However, `ipywidgets` has [`RangeSlider`s](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#IntRangeSlider) while Matplotlib does not, so have we broken this contract? Happily the answer is no. RangeSliders are being added to matplolibt in [matplotlib/matplotlib#18829](https://github.com/matplotlib/matplotlib/pull/18829), and in the meantime they are available via {class}`~mpl_interactions.widgets.RangeSlider`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from mpl_interactions.widgets import RangeSlider\n",
"\n",
"fig, axs = plt.subplots(1, 2, figsize=(12, 5))\n",
"\n",
"# plot histogram of pixel intensities\n",
"axs[1].hist(im.flatten(), bins=\"auto\")\n",
"\n",
"# make thresholding slider\n",
"plt.subplots_adjust(bottom=0.25)\n",
"s_ax = plt.axes([0.575, 0.1, 0.25, 0.05])\n",
"slider = RangeSlider(s_ax, \"threshold\", im.min(), im.max(), valinit=(im.min(), im.max()))\n",
"\n",
"# create interactive controls\n",
"ctrls = iplt.imshow(im, vmin_vmax=slider, ax=axs[0])\n",
"iplt.axvline(ctrls[\"vmin\"], ax=axs[1], c=\"k\")\n",
"iplt.axvline(ctrls[\"vmax\"], ax=axs[1], c=\"k\")"
"Until version `3.4` `matplotlib` did not have a RangeSlider widget so this library provided one. Since `mpl-interactions` now depends on `matplotlib > 3.4` the `mpl-interactions` RangeSlider is no longer available.\n"
]
}
],
Expand Down
8 changes: 4 additions & 4 deletions mpl_interactions/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
from matplotlib.pyplot import figure, gca, gcf, ioff
from matplotlib.pyplot import sca as mpl_sca

from .widgets import RangeSlider

__all__ = [
"sca",
"decompose_bbox",
Expand Down Expand Up @@ -416,7 +414,9 @@ def update_text(val):

def create_mpl_range_selection_slider(ax, label, values, slider_format_string):
"""Create a slider that behaves similarly to the ipywidgets selection slider."""
slider = RangeSlider(ax, label, 0, len(values) - 1, valinit=(0, len(values) - 1), valstep=1)
slider = mwidgets.RangeSlider(
ax, label, 0, len(values) - 1, valinit=(0, len(values) - 1), valstep=1
)

def update_text(val):
slider.valtext.set_text(
Expand All @@ -440,7 +440,7 @@ def process_mpl_widget(val, update):
if isinstance(val, mwidgets.RadioButtons):
cb = val.on_clicked(partial(changeify, update=partial(update, values=None)))
return val.value_selected, val, cb, hash(repr(val.labels))
elif isinstance(val, (mwidgets.Slider, mwidgets.RangeSlider, RangeSlider)):
elif isinstance(val, (mwidgets.Slider, mwidgets.RangeSlider)):
# TODO: proper inherit matplotlib rand
# potential future improvement:
# check if valstep has been set and then try to infer the values
Expand Down
267 changes: 0 additions & 267 deletions mpl_interactions/widgets.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Custom matplotlib widgets made for use in this library."""

import numpy as np
from matplotlib import cbook, ticker
from matplotlib.cbook import CallbackRegistry
from matplotlib.widgets import AxesWidget
Expand Down Expand Up @@ -200,269 +199,3 @@ def reset(self):
"""Reset the slider to the initial value."""
if self.val != self.valinit:
self.set_val(self.valinit)


class RangeSlider(SliderBase):
"""
A slider representing a floating point range.
Create a slider from *valmin* to *valmax* in axes *ax*. For the slider to
remain responsive you must maintain a reference to it. Call
:meth:`on_changed` to connect to the slider event.
Attributes
----------
val : tuple[float]
Slider value.
"""

def __init__(
self,
ax,
label,
valmin,
valmax,
valinit=None,
valfmt=None,
closedmin=True,
closedmax=True,
dragging=True,
valstep=None,
orientation="horizontal",
**kwargs,
):
"""
Create a RangeSlider.
Parameters
----------
ax : Axes
The Axes to put the slider in.
label : str
Slider label.
valmin : float
The minimum value of the slider.
valmax : float
The maximum value of the slider.
valinit : tuple[float] or None, default: None
The initial positions of the slider. If None the initial positions
will be at the 25th and 75th percentiles of the range.
valfmt : str, default: None
%-format string used to format the slider values. If None, a
`.ScalarFormatter` is used instead.
closedmin : bool, default: True
Whether the slider interval is closed on the bottom.
closedmax : bool, default: True
Whether the slider interval is closed on the top.
dragging : bool, default: True
If True the slider can be dragged by the mouse.
valstep : float, default: None
If given, the slider will snap to multiples of *valstep*.
orientation : {'horizontal', 'vertical'}, default: 'horizontal'
The orientation of the slider.
**kwargs:
Passed to axhspan
Notes
-----
Additional kwargs are passed on to ``self.poly`` which is the
`~matplotlib.patches.Rectangle` that draws the slider knob. See the
`.Rectangle` documentation for valid property names (``facecolor``,
``edgecolor``, ``alpha``, etc.).
"""
super().__init__(
ax, orientation, closedmin, closedmax, valmin, valmax, valfmt, dragging, valstep
)

self.val = valinit
if valinit is None:
valinit = np.array([valmin + 0.25 * valmax, valmin + 0.75 * valmax])
else:
valinit = self._value_in_bounds(valinit)
self.val = valinit
self.valinit = valinit
if orientation == "vertical":
self.poly = ax.axhspan(valinit[0], valinit[1], 0, 1, **kwargs)
else:
self.poly = ax.axvspan(valinit[0], valinit[1], 0, 1, **kwargs)

if orientation == "vertical":
self.label = ax.text(
0.5,
1.02,
label,
transform=ax.transAxes,
verticalalignment="bottom",
horizontalalignment="center",
)

self.valtext = ax.text(
0.5,
-0.02,
self._format(valinit),
transform=ax.transAxes,
verticalalignment="top",
horizontalalignment="center",
)
else:
self.label = ax.text(
-0.02,
0.5,
label,
transform=ax.transAxes,
verticalalignment="center",
horizontalalignment="right",
)

self.valtext = ax.text(
1.02,
0.5,
self._format(valinit),
transform=ax.transAxes,
verticalalignment="center",
horizontalalignment="left",
)

self.set_val(valinit)

def _min_in_bounds(self, min):
"""Ensure the new min value is between valmin and self.val[1]."""
if min <= self.valmin:
if not self.closedmin:
return self.val[0]
min = self.valmin # noqa: A001

if min > self.val[1]:
min = self.val[1] # noqa: A001
return self._stepped_value(min)

def _max_in_bounds(self, max):
"""Ensure the new max value is between valmax and self.val[0]."""
if max >= self.valmax:
if not self.closedmax:
return self.val[1]
max = self.valmax # noqa: A001

if max <= self.val[0]:
max = self.val[0] # noqa: A001
return self._stepped_value(max)

def _value_in_bounds(self, val):
return (self._min_in_bounds(val[0]), self._max_in_bounds(val[1]))

def _update_val_from_pos(self, pos):
"""Given a position update the *val*."""
idx = np.argmin(np.abs(self.val - pos))
if idx == 0:
val = self._min_in_bounds(pos)
self.set_min(val)
else:
val = self._max_in_bounds(pos)
self.set_max(val)

def _update(self, event):
"""Update the slider position."""
if self.ignore(event) or event.button != 1:
return

if event.name == "button_press_event" and event.inaxes == self.ax:
self.drag_active = True
event.canvas.grab_mouse(self.ax)

if not self.drag_active:
return

elif (event.name == "button_release_event") or (
event.name == "button_press_event" and event.inaxes != self.ax
):
self.drag_active = False
event.canvas.release_mouse(self.ax)
return
if self.orientation == "vertical":
self._update_val_from_pos(event.ydata)
else:
self._update_val_from_pos(event.xdata)

def _format(self, val):
"""Pretty-print *val*."""
if self.valfmt is not None:
return (self.valfmt % val[0], self.valfmt % val[1])
else:
# fmt.get_offset is actually the multiplicative factor, if any.
_, s1, s2, _ = self._fmt.format_ticks([self.valmin, *val, self.valmax])
# fmt.get_offset is actually the multiplicative factor, if any.
s1 += self._fmt.get_offset()
s2 += self._fmt.get_offset()
# use raw string to avoid issues with backslashes from
return rf"({s1}, {s2})"

def set_min(self, val):
"""Set the lower value of the slider to *val*.
Parameters
----------
val : float
The value to set the min to.
"""
self.set_val((val, self.val[1]))

def set_max(self, val):
"""Set the higher value of the slider to *val*.
Parameters
----------
val : float
The value to set the max to.
"""
self.set_val((self.val[0], val))

def set_val(self, val):
"""Set slider value to *val*.
Parameters
----------
val : tuple or arraylike of float
The position to move the slider to.
"""
val = np.sort(np.asanyarray(val))
if val.shape != (2,):
raise ValueError(f"val must have shape (2,) but has shape {val.shape}")
val[0] = self._min_in_bounds(val[0])
val[1] = self._max_in_bounds(val[1])
xy = self.poly.xy
if self.orientation == "vertical":
xy[0] = 0, val[0]
xy[1] = 0, val[1]
xy[2] = 1, val[1]
xy[3] = 1, val[0]
xy[4] = 0, val[0]
else:
xy[0] = val[0], 0
xy[1] = val[0], 1
xy[2] = val[1], 1
xy[3] = val[1], 0
xy[4] = val[0], 0
self.poly.xy = xy
self.valtext.set_text(self._format(val))
if self.drawon:
self.ax.figure.canvas.draw_idle()
self.val = val
if self.eventson:
self._observers.process("changed", val)

def on_changed(self, func):
"""Attach a callback to when the slider is changed.
Parameters
----------
func : callable
Function to call when slider is changed.
The function must accept a numpy array with shape (2,) float
as its argument.
Returns
-------
int
Connection id (which can be used to disconnect *func*)
"""
return self._observers.connect("changed", func)

0 comments on commit f1d7bd6

Please sign in to comment.