mpl-extensions/mpl-interactions

The rangeslider seems to be broken

kmdalton opened this issue · 2 comments

Bug report

I tried creating a rangeslider under ipyplot.imshow by passing a keyword argument ("r", 1., 99., 99). When I actuate the slider, I get a complicated traceback and nothing happens. Here's a simple example to reproduce. I was trying to create a percentile based contrast slider.

from matplotlib import pyplot as plt
from mpl_interactions import ipyplot as iplt
import numpy as np


img = plt.imread("https://matplotlib.org/3.3.1/_images/stinkbug.png")


fig,ax = plt.subplots()

def f(prange):
    pmin,pmax = prange
    vmin = np.percentile(img, pmin)
    vmax = np.percentile(img, pmax)
    truncated = img.copy()
    truncated = np.maximum(vmin, truncated)
    truncated = np.minimum(vmax, truncated)
    return truncated

controls = iplt.imshow(f, prange=("r", 1., 99., 99))
plt.show()

I get a traceback like this when I move the sliders:

Traceback (most recent call last):
  File "anaconda/envs/whatever/lib/python3.8/site-packages/matplotlib/cbook/__init__.py", line 224, in process
    func(*args, **kwargs)
  File "anaconda/envs/whatever/lib/python3.8/site-packages/mpl_interactions/helpers.py", line 394, in changeify
    update({"new": val})
  File "anaconda/envs/whatever/lib/python3.8/site-packages/mpl_interactions/controller.py", line 182, in slider_updated
    self._slider_updated(change, key, values)
  File "anaconda/envs/whatever/lib/python3.8/site-packages/mpl_interactions/controller.py", line 156, in _slider_updated
    self.params[key] = values[int(change["new"])]
TypeError: only size-1 arrays can be converted to Python scalars

Version Info
I saw this bug on 0.17.0 and 0.17.1

ianhi commented

I think this would have been naturally fixed by #189 but definitely deserves a fix sooner than that will be finished.


This seems to be an issue only with matplotlib sliders as they don't use tuples for their .val attribute and instead use numpy arrays - which is my fault because I wrote them :(

This was introduced by #155 which added the int casting for backwards compatibility with more numpy versions.

Fix incoming - thanks for the report!

ianhi commented

FWIW you can achieve the same percentile thresholding while avoiding any .copy operations like so:

fig, ax = plt.subplots()

def vmin(prange):
    return np.percentile(img, prange[0])

def vmax(prange):
    return np.percentile(img, prange[1])

controls = iplt.imshow(img, prange=("r", 1.0, 99.0, 99), vmin=vmin, vmax=vmax)
plt.show()

which I think should be more efficient as all you're doing is modifying the norm of the imshow instead of mucking around with the underlying data.


This also makes it clear how you might want to be able to pass vmin_vmax as a single callable:

def vmin_vmax(prange):
    return np.percentile(img, prange)

and then do iplt.imshow(img, prange(1,99,99), vmin_vmax=vmin_vmax)

I opened #196 about this