Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

New Styling for Sliders #19256

Copy link
Copy link
Closed
Closed
Copy link
@ianhi

Description

@ianhi
Issue body actions

Problem

I've never loved the way Matplotlib Slider widgets look (and per jupyter-widgets/ipywidgets#3025 (comment) I am apparently not alone). However, beyond that I think that the current way the slider widgets are drawn introduces two issue that hamper usability.

  1. There is no clear handle for the user to grab
  2. No visual feedback that you've grabbed the slider

<speculation> More broadly I suspect that most people's expectation of what a slider UI element will look like is strongly shaped by how they look on the web. So if Matplotlib sliders look more similar to web sliders then they will be more natural for users. </speculation>

Proposed Solution

Something to the effect of this:

script with new slider definition + an example

import numpy as np
from matplotlib.widgets import SliderBase
from matplotlib import _api
import matplotlib.patches as mpatches
from matplotlib import transforms

class newSlider(SliderBase):
    cnt = _api.deprecated("3.4")(property(# Not real, but close enough.
        lambda self: len(self._observers.callbacks['changed'])))
    observers = _api.deprecated("3.4")(property( lambda self: self._observers.callbacks['changed']))

    def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None,
                 closedmin=True, closedmax=True, slidermin=None,
                 slidermax=None, dragging=True, valstep=None,
                 orientation='horizontal', *, initcolor='r',s=10, **kwargs):
        super().__init__(ax, orientation, closedmin, closedmax,
                                 valmin, valmax, valfmt, dragging, valstep)

        if slidermin is not None and not hasattr(slidermin, 'val'):
            raise ValueError(
                f"Argument slidermin ({type(slidermin)}) has no 'val'")
        if slidermax is not None and not hasattr(slidermax, 'val'):
            raise ValueError(
                f"Argument slidermax ({type(slidermax)}) has no 'val'")
        self.slidermin = slidermin
        self.slidermax = slidermax
        valinit = self._value_in_bounds(valinit)
        if valinit is None:
            valinit = valmin
        self.val = valinit
        self.valinit = valinit

        ax.axis('off')
        self.dot, = ax.plot([valinit],[.5], 'o', markersize=s, color='C0')
        trans = transforms.blended_transform_factory(
            ax.transData, ax.transAxes)
        self.rect = mpatches.Rectangle((valmin, .25), width=valinit-valmin, height=.5, transform=trans,
                              color='C0', alpha=0.75)
        self.above_rect = mpatches.Rectangle((valinit, .25), width=valmax-valinit, height=.5, transform=trans,
                              color='grey', alpha=0.5)
        ax.add_patch(self.rect)
        ax.add_patch(self.above_rect)

        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 _format(self, val):
        """Pretty-print *val*."""
        if self.valfmt is not None:
            return self.valfmt % val
        else:
            _, s, _ = self._fmt.format_ticks([self.valmin, val, self.valmax])
            # fmt.get_offset is actually the multiplicative factor, if any.
            return s + self._fmt.get_offset()
        
    def _value_in_bounds(self, val):
        """Makes sure *val* is with given bounds."""
        val = self._stepped_value(val)

        if val <= self.valmin:
            if not self.closedmin:
                return
            val = self.valmin
        elif val >= self.valmax:
            if not self.closedmax:
                return
            val = self.valmax

        if self.slidermin is not None and val <= self.slidermin.val:
            if not self.closedmin:
                return
            val = self.slidermin.val

        if self.slidermax is not None and val >= self.slidermax.val:
            if not self.closedmax:
                return
            val = self.slidermax.val
        return 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)
            self.dot.set_markeredgecolor('grey')
            self.dot.set_markerfacecolor('grey')
            self.ax.figure.canvas.draw_idle()
            return

        if event.name == 'button_press_event':
            self.dot.set_markeredgecolor('C0')
            self.dot.set_markerfacecolor('C0')
        if self.orientation == 'vertical':
            val = self._value_in_bounds(event.ydata)
        else:
            val = self._value_in_bounds(event.xdata)
        if val not in [None, self.val]:
            self.set_val(val)
    def set_val(self, val):
        """
        Set slider value to *val*.
        Parameters
        ----------
        val : float
        """
        self.dot.set_xdata([val])
        self.rect.set_width(val)
        self.above_rect.set_x(val)
        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):
        """
        Connect *func* as callback function to changes of the slider value.
        Parameters
        ----------
        func : callable
            Function to call when slider is changed.
            The function must accept a single float as its arguments.
        Returns
        -------
        int
            Connection id (which can be used to disconnect *func*).
        """
        return self._observers.connect('changed', lambda val: func(val))

def connnect_slider_to_ax(slider, ax):
    t = np.arange(0.0, 1.0, 0.001)
    f0 = 3
    delta_f = 5.0
    amp = 5
    s = amp * np.sin(2 * np.pi * f0 * t)
    l, = ax.plot(t, s, lw=2)
    def update(val):
        freq = slider.val
        l.set_ydata(amp*np.sin(2*np.pi*freq*t))
        # ax.figure.canvas.draw_idle()
    slider.on_changed(update)

if __name__ == '__main__':
    import matplotlib.pyplot as plt
    from matplotlib.widgets import Slider
    # new style
    fig, ax = plt.subplots()
    plt.subplots_adjust(bottom=0.25)
    new_slider_ax = plt.axes([0.25, 0.1, 0.65, 0.03])
    sNew = newSlider(new_slider_ax, 'Freq', 0.1, 30.0,valinit=5)
    connnect_slider_to_ax(sNew, ax)

    fig2, ax2 = plt.subplots()
    plt.subplots_adjust(bottom=0.25)
    old_slider_ax = plt.axes([0.25, 0.1, 0.65, 0.03])
    sOld = Slider(old_slider_ax, 'Freq', 0.1, 30.0, valinit=5)
    connnect_slider_to_ax(sOld, ax2)

    plt.show()

The above script will generate these two figures for comparison. On the left is my new proposed style, and on the right is the current style:
comparing-sliders

Additional context and prior art

https://www.smashingmagazine.com/2017/07/designing-perfect-slider/
https://material-ui.com/components/slider/
https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#IntSlider

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.