0

I can't find the solution to update the graph when the slider is moved (in Jupyter notebook). The graph shows the sine and cosine functions, and their sum. The sine and cosine are weighed by the value of the slider (k) between 0 and 1:

  • sine = k * sin(x)
  • cosine = (1-k) * cos(x)

The arrays to plot are updated correctly, e.g. for k = 0.2:

np.max(graph.ysin), np.max(graph.ycos)
0.19997482553477502, 0.7995972339065481)

The plots don't update, they continue to show curves for k = .5 (initial value).

enter image description here

Note I don't want (as far as possible) to recreate the whole figure each time the slider changes, only to update the plots by passing the updated arrays with Line2D.set_data.

What I tried:

  • Adding plt.show () call (suggested here)
  • Adding fig.canvas.draw() (suggested here)
  • Adding %matplotlib notebook (suggested here)
  • Adding ax.relim() and ax.autoscale_view() (suggested here)

and some combinations of them...

class DynamicGraph ():
    def __init__ (self, ratio):
        # Define x range
        self.x = np.linspace (-np.pi, np.pi, 100)
        # Compute and display values
        self.init_figure (ratio)

    # Set figure, axe and plots
    def init_figure (self, ratio):
        self.fig, self.ax = plt.subplots (figsize=(8,3))

        self.ax.xaxis.set_major_locator(mpl.ticker.MultipleLocator(np.pi/2))
        self.ax.xaxis.set_major_formatter(mpl.ticker.FuncFormatter(lambda  x,pos:f'{x/np.pi} $\pi$'))
        self.ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(.25))

        self.ax.axhline(-.5, color='grey', lw=.5)
        self.ax.axhline(0, color='grey', lw=.5)
        self.ax.axhline(.5, color='grey', lw=.5)
        self.ax.axvline(0, color='grey', lw=.5)

        self.show_plots (ratio, init=True)
        plt.legend ()
        return

    # Create/update plots
    def show_plots (self, ratio, init=False):
        self._compute_values_ (ratio)        
        if init:
            self.sin_plot, = self.ax.plot(self.x, self.ysin, color='grey', ls=':', label='sin');
            self.cos_plot, = self.ax.plot(self.x, self.ycos, color='grey', ls='--', label='cos');
            self.sum_plot, = self.ax.plot(self.x, self.ysum, color='green', label='sin+cos');
        else:
            self.sin_plot.set_data (self.x, self.ysin)
            self.cos_plot.set_data (self.x, self.ycos)
            self.sum_plot.set_data (self.x, self.ysum)        
        return

    # Compute values which change with the slider value
    def _compute_values_ (self, ratio):
        self.ysin = ratio*np.sin(self.x)
        self.ycos = (1-ratio)*np.cos(self.x)
        self.ysum = self.ysin + self.ycos
        return


# Create slider, listen to changes
slider = widgets.FloatSlider (min=0, max=1, value=.5)
slider. observe(lambda change: graph.show_plots (slider.value), names='value')

# Show slider and graph
display(slider)
graph = DynamicGraph (slider.value)

I'd appreciate some explanation with the solution, e.g. if this has something to do with a particular context, or version, etc to understand why the above suggestions where not applicable.

mins
  • 6,478
  • 12
  • 56
  • 75

1 Answers1

0

After multiple attempts, I found this working combination of instructions:

  • Add %matplotlib notebook at the beginning of the code to allow interactive mode on Jupyter notebook (as opposed to Jupyterlab I assume).

  • Add fig.canvas.draw() after the data have been updated.

These instructions seems to be useless, but as I saw them use quite often, they may have some impact I ignore:

  • plt.ion() to start interactive mode
  • fig.canvas.flush_events() after fig.canvas.draw()

%matplotlib notebook    
#plt.ion()    

class DynamicGraph ():
    def __init__ (self, ratio):
        # Define x range
        self.x = np.linspace (-np.pi, np.pi, 100)
        # Compute and display values
        self.init_figure (ratio)

    # Set figure, axe and plots
    def init_figure (self, ratio):
        self.fig, self.ax = plt.subplots (figsize=(8,3))

        self.ax.xaxis.set_major_locator(mpl.ticker.MultipleLocator(np.pi/2))
        self.ax.xaxis.set_major_formatter(mpl.ticker.FuncFormatter(lambda  x,pos:f'{x/np.pi} $\pi$'))
        self.ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(.25))

        self.ax.axhline(-.5, color='grey', lw=.5)
        self.ax.axhline(0, color='grey', lw=.5)
        self.ax.axhline(.5, color='grey', lw=.5)
        self.ax.axvline(0, color='grey', lw=.5)

        self.show_plots (ratio, init=True)
        plt.legend ()
        return

    # Create/update plots
    def show_plots (self, ratio, init=False):
        self._compute_values_ (ratio)        
        if init:
            self.sin_plot, = self.ax.plot(self.x, self.ysin, color='grey', ls=':', label='sin');
            self.cos_plot, = self.ax.plot(self.x, self.ycos, color='grey', ls='--', label='cos');
            self.sum_plot, = self.ax.plot(self.x, self.ysum, color='green', label='sin+cos');
        else:
            self.sin_plot.set_data (self.x, self.ysin)
            self.cos_plot.set_data (self.x, self.ycos)
            self.sum_plot.set_data (self.x, self.ysum)        
            self.fig.canvas.draw()
            #self.fig.canvas.flush_events()   
        return

    # Compute values which change with the slider value
    def _compute_values_ (self, ratio):
        self.ysin = ratio*np.sin(self.x)
        self.ycos = (1-ratio)*np.cos(self.x)
        self.ysum = self.ysin + self.ycos
        return


# Create slider, listen to changes
slider = widgets.FloatSlider (min=0, max=1, value=.5)
slider. observe(lambda change: graph.show_plots (slider.value), names='value')

# Show slider and graph
display(slider)
graph = DynamicGraph (slider.value)
mins
  • 6,478
  • 12
  • 56
  • 75