Search code examples
pythonmatplotlibmatplotlib-widget

Use Matplotlib Plot and Widgets in function and return user input


My problem is the following:

I create a Matplotlib figure including some widget sliders and a button to close the figure. This works. What can I do to use this code inside a function which returns e.g. the values of the slides AFTER clicking the "close figure" button?

Here is the code (im3d is a 3d image numpy array):

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button

class IndexTracker(object):
    def __init__(self, ax, data3d, title):
        self.ax = ax
        ax.set_title(title)

        self.data3d = data3d
        rows, cols, self.slices = data3d.shape
        self.ind = self.slices//2

        self.im = ax.imshow(self.data3d[:, :, self.ind])
        self.update()

    def update(self):
        self.im.set_data(self.data3d[:, :, self.ind])
        ax.set_ylabel('slice %s' % self.ind)
        self.im.axes.figure.canvas.draw()
#
fig = plt.figure(figsize=(18, 8), dpi=80, facecolor='w', edgecolor='b')
ax  = fig.add_subplot(1,2,1)
ax2 = fig.add_subplot(1,2,2)

tracker1 = IndexTracker(ax, im3d, 'Select First Image')
tracker2 = IndexTracker(ax2, im3d, 'Select Last Image')

def slider_changed(value, tracker):
    numb = int(round(value))    
    tracker.ind = numb    
    tracker.update()

max0 = im3d.shape[2] -1

ax_start  = fig.add_axes([0.1, 0.85, 0.35, 0.03])
sl_start = Slider(ax_start, 'START', 0, max0, valinit=0, valfmt="%i")

ax_end  = fig.add_axes([0.6, 0.85, 0.35, 0.03])
sl_end = Slider(ax_end, 'END', 0, max0, valinit=0, valfmt="%i")

def sl_start_changed(val):
    slider_changed(sl_start.val,tracker1)

def sl_end_changed(val):
    slider_changed(sl_end.val,tracker2)

sl_start.on_changed(sl_start_changed)
sl_end.on_changed(sl_end_changed)

class Index(object):

    def close_figure(self, event):
        plt.close(fig)    

callback = Index()
ax_button = fig.add_axes([0.7, 0.06, 0.15, 0.075])
button = Button(ax_button, 'DONE')
button.on_clicked(callback.close_figure)
fig.canvas.manager.window.raise_()

plt.plot()

My first idea was to run a while loop after plt.plot(), something like this:

while not_done:
   time.sleep(0.5)

and change not_done to False inside the function close_figure. But in this case, the plot doesn't show.


Solution

  • The slider is still available after the figure is closed. Hence you can just access its val attribute after closing the figure

    fig, ax = plt.subplots()
    
    slider = Slider(...)
    
    # .. callbacks
    
    plt.show() # you may use plt.show(block=True) when this is run in interactive mode
    
    print(slider.val)
    

    Edit after clarifications:

    You are running spyder and use the IPython console within. This has several implications.

    • You need to turn interactive mode off, plt.ioff()
    • You need to deactivate "support" for matplotlib

    enter image description here

    Then the following code runs fine and only prints the values after the figure is closed.

    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.widgets import Slider, Button
    
    plt.ioff()
    
    im3d = np.random.rand(20,20,10)
    
    
    class IndexTracker(object):
        def __init__(self, ax, data3d, title):
            self.ax = ax
            ax.set_title(title)
    
            self.data3d = data3d
            rows, cols, self.slices = data3d.shape
            self.ind = self.slices//2
    
            self.im = ax.imshow(self.data3d[:, :, self.ind])
            self.update()
    
        def update(self):
            self.im.set_data(self.data3d[:, :, self.ind])
            ax.set_ylabel('slice %s' % self.ind)
            self.im.axes.figure.canvas.draw()
    #
    fig = plt.figure(figsize=(18, 8), dpi=80, facecolor='w', edgecolor='b')
    ax  = fig.add_subplot(1,2,1)
    ax2 = fig.add_subplot(1,2,2)
    
    tracker1 = IndexTracker(ax, im3d, 'Select First Image')
    tracker2 = IndexTracker(ax2, im3d, 'Select Last Image')
    
    def slider_changed(value, tracker):
        numb = int(round(value))    
        tracker.ind = numb    
        tracker.update()
    
    max0 = im3d.shape[2] -1
    
    ax_start  = fig.add_axes([0.1, 0.85, 0.35, 0.03])
    sl_start = Slider(ax_start, 'START', 0, max0, valinit=0, valfmt="%i")
    
    ax_end  = fig.add_axes([0.6, 0.85, 0.35, 0.03])
    sl_end = Slider(ax_end, 'END', 0, max0, valinit=0, valfmt="%i")
    
    def sl_start_changed(val):
        slider_changed(sl_start.val,tracker1)
    
    def sl_end_changed(val):
        slider_changed(sl_end.val,tracker2)
    
    sl_start.on_changed(sl_start_changed)
    sl_end.on_changed(sl_end_changed)
    
    class Index(object):
    
        def close_figure(self, event):
            plt.close(fig)    
    
    callback = Index()
    ax_button = fig.add_axes([0.7, 0.06, 0.15, 0.075])
    button = Button(ax_button, 'DONE')
    button.on_clicked(callback.close_figure)
    fig.canvas.manager.window.raise_()
    
    plt.show()
    
    print(sl_start.val, sl_end.val)
    

    Alternatively, you may just run the complete code in external command line, without the need for plt.ioff()

    enter image description here