0

I am using 2 sliders to adjust the colorbar of a 2D heat map; one for the bottom (minimum) and one for the top (maximum). I want to make sure that the two cannot overlap i.e. if the full range is 0 till 5 and I set the maximum on 2 then the minimum cannot surpass 2. This should happen interactively. How can I achieve this? Additionally is there a maybe a way to integrate the two sliders into one? Thank you.

An example of my GUI. And the relevant part of the code:

def update(val, s=None):
    """Retreives the value from the sliders and updates the graph accordingly"""
    _cmin = s_cmin.val
    _cmax = s_cmax.val
    pcm.set_clim([_cmin, _cmax])
    plt.draw()

def reset(event):
    """Resets the sliders when the reset button is pressed"""
    s_cmin.reset()
    s_cmax.reset()

fig, ax = plt.subplots(figsize=(13,8))
plt.subplots_adjust(left=0.25,bottom=0.25)

# define axis minima and maxima:
x_min = Xi.min()
x_max = Xi.max()
y_min = Yi.min()
y_max = Yi.max()
c_min = Zi.min()
c_max = Zi.max()

pcm = ax.pcolormesh(Xi,Yi,Zi)
cb = plt.colorbar(pcm)
axcolor = 'lightgoldenrodyellow'
axx = plt.xlim([x_min, x_max])
ayy = plt.ylim([y_min, y_max])

# create a space in the figure to place the two sliders:
ax_cmin = plt.axes([0.15, 0.10, 0.65, 0.02], facecolor=axcolor)
ax_cmax = plt.axes([0.15, 0.15, 0.65, 0.02], facecolor=axcolor)
# the first argument is the rectangle, with values in percentage of the figure
# size: [left, bottom, width, height]

# create each slider on its corresponding place:
s_cmax = Slider(ax_cmax, 'max', c_min, c_max, valinit=c_max, valfmt='%1.4f')
s_cmin = Slider(ax_cmin, 'min', c_min, c_max, valinit=c_min, valfmt='%1.4f')

# set both sliders to call update when their value is changed:
s_cmin.on_changed(update)
s_cmax.on_changed(update)

# create a space in the figure to place the reset button
resetax = plt.axes([0.8, 0.025, 0.1, 0.04])
# create the reset button
button = Button(resetax, 'Reset', color=axcolor, hovercolor='0.975')
button.on_clicked(reset)

# create a space in the figure to place the textboxes:
axbox_xmin = plt.axes([0.07, 0.55, 0.04, 0.04])
axbox_xmax = plt.axes([0.12, 0.55, 0.04, 0.04])
axbox_ymin = plt.axes([0.07, 0.49, 0.04, 0.04])
axbox_ymax = plt.axes([0.12, 0.49, 0.04, 0.04])

# create the textboxes
tb_xmin = TextBox(axbox_xmin,'x', color=axcolor, hovercolor='0.975', label_pad=0.01)
tb_xmax = TextBox(axbox_xmax,'', color=axcolor, hovercolor='0.975')
tb_ymin = TextBox(axbox_ymin,'y', color=axcolor, hovercolor='0.975', label_pad=0.01)
tb_ymax = TextBox(axbox_ymax,'', color=axcolor, hovercolor='0.975')

# create the submit action
tb_xmin.on_submit(submit)
tb_xmax.on_submit(submit)
tb_ymin.on_submit(submit)
tb_ymax.on_submit(submit)

plt.show()
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
STJ
  • 113
  • 1
  • 2
  • 12

1 Answers1

0

In some cases it may indeed be desired to have one single Slider which can set some minimum and maximum simultaneously. So instead of having one single value, the slider could have two values and the rectangle inside the slider would be bounded by the two values, instead of starting at the minimum value of the slider.

The following would be solution to such a case. It uses a MinMaxSlider, i.e. a subclass of the Slider which is adapted to host two values.
Instead of a single input value, it would expect two values,

MinMaxSlider(... , valinit=0.5,valinit2=0.8)

such that the Sliderbar ranges from 0.5 to 0.8. Clicking on the slider would change the value which is closer to the click, making dragging rather easy.

In order to use this slider be aware that the function that the callback via on_changed now naturally has two arguments.

import six
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider

class MinMaxSlider(Slider):
    def __init__(self,ax, label, valmin, valmax, **kwargs):
        self.valinit2 = kwargs.pop("valinit2", valmax)
        self.val2 = self.valinit2
        Slider.__init__(self,ax, label, valmin, valmax, **kwargs)
        self.poly.xy = np.array([[self.valinit,0],[self.valinit,1],
                        [self.valinit2,1],[self.valinit2,0]])
        self.vline.set_visible(False)

    def set_val(self, val):
        if np.abs(val-self.val) < np.abs(val-self.val2):
            self.val = val
        else:
            self.val2 = val
        self.poly.xy = np.array([[self.val,0],[self.val,1],
                                 [self.val2,1],[self.val2,0]])
        self.valtext.set_text(self.valfmt % self.val +"\n"+self.valfmt % self.val2)
        if self.drawon:
            self.ax.figure.canvas.draw_idle()
        if not self.eventson:
            return
        for cid, func in six.iteritems(self.observers):
            func(self.val,self.val2)


import numpy as np

x = np.linspace(0,16,1001)
f = lambda x: np.sin(x)*np.sin(1.7*x+2)*np.sin(0.7*x+0.05)*x

fig,(ax, sliderax) = plt.subplots(nrows=2,gridspec_kw={"height_ratios":[1,0.05]})
fig.subplots_adjust(hspace=0.3)

ax.plot(x,f(x))

slider = MinMaxSlider(sliderax,"slider",x.min(),x.max(),
                      valinit=x.min(),valinit2=x.max())

def update(mini,maxi):
    ax.set_xlim(mini,maxi)

slider.on_changed(update)
update(x.min(),x.max())

plt.show()

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • What would be the proper way to define a reset method inside the class that can be called by i.e. 'button.on_clicked(MinMaxSlider.reset_val())'? – STJ Nov 20 '17 at 09:40
  • The method would need to reset val and val2 to their initial values valinit and valinit2. `def reset(self): self.val=self.valinit self.val2=self.valinit2 self.set_val(self.valinit)` – ImportanceOfBeingErnest Nov 20 '17 at 10:22
  • I have 'def reset(self): self.val=self.valinit self.val2=self.valinit2 self.set_val(self.valinit)' as my reset method and I use 'resetbutton.on_clicked(slider.reset())' as my call to reset. Without changing the rest of the code nothing happens. Can you point out what I missed? – STJ Nov 20 '17 at 14:06
  • I cannot say for sure, but assuming everything else is correctly implemented, you need `resetbutton.on_clicked(slider.reset)` (without the backets, because `slider.reset()` simply returns None. – ImportanceOfBeingErnest Nov 20 '17 at 14:58
  • If possible I have another follow up question. Lets say I change the range of my plot via another widget and as a result I want to change the min/max values of the colorbar and the slider. I can calculate the new min/max values easily but how do I make sure that both the slider and the colorbar are adjusted accordingly? – STJ Nov 28 '17 at 16:34
  • To change the slider range: https://stackoverflow.com/a/47537705/4124317 To change the colorbar: https://stackoverflow.com/questions/19322157/using-a-slider-to-change-variable-and-replot-matplotlib-figure – ImportanceOfBeingErnest Nov 28 '17 at 17:38
  • If possible I have another follow up question: how can I adjust the `valfmt` for this slider? Thank you! – STJ Dec 18 '17 at 11:07
  • The default is `valfmt='%1.2f'`, of course you can change it to anything you like. – ImportanceOfBeingErnest Dec 18 '17 at 11:13