Search code examples
pythonmatplotlibjupyter-notebookoutputipywidgets

How to embed LiveGraph in ipywidgets Output?


By using this answer to produce a LiveGraph and this answer to update variables to a thread, I was able to generate a graph that updates itself each second and whose amplitude is determined by a slider (code below). Both answers were incredibly helpful!

%matplotlib notebook
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation
from threading import Thread, Lock
import time
import ipywidgets as widgets
from IPython.display import display
import numpy as np
'''#################### Live Graph ####################''' 
# Create a new class which is a live graph
class LiveGraph(object):
    def __init__(self, baseline):
        self.x_data, self.y_data = [], []
        self.figure = plt.figure()
        self.line, = plt.plot(self.x_data, self.y_data)
        self.animation = FuncAnimation(self.figure, self.update, interval=1200)
        # define variable to be updated as a list
        self.baseline = [baseline]
        self.lock = Lock()
        self.th = Thread(target=self.thread_f, args = (self.baseline,), daemon=True)
        # start thread
        self.th.start()
    
    def update_baseline(self,baseline):
        # updating a list updates the thread argument
        with self.lock:
            self.baseline[0] = baseline
    
    # Updates animation
    def update(self, frame):
        self.line.set_data(self.x_data, self.y_data)
        self.figure.gca().relim()
        self.figure.gca().autoscale_view()
        return self.line,
    
    def show(self):
        plt.show()
    
    # Function called by thread that updates variables
    def thread_f(self, base):
        x = 0
        while True:
            self.x_data.append(x)
            x += 1
            self.y_data.append(base[0])    
            time.sleep(1)  
            
'''#################### Slider ####################'''            
# Function that updates baseline to slider value
def update_baseline(v):
    global g
    new_value = v['new']
    g.update_baseline(new_value)
    
slider = widgets.IntSlider(
    value=10,
    min=0,
    max=200,
    step=1,
    description='value:',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='d'
)
slider.observe(update_baseline, names = 'value')

'''#################### Display ####################''' 

display(slider)
g = LiveGraph(slider.value)

Still, I would like to put the graph inside a bigger interface which has other widgets. It seems that I should put the LiveGraph inside the Output widget, but when I replace the 'Display section' of my code by the code shown below, no figure is displayed.

out = widgets.Output(layout={'border': '1px solid black'})
with out:
    g = LiveGraph(slider.value)

vbox = widgets.VBox([slider,out], align_self='stretch',justify_content='center')       
vbox

Is there a way to embed this LiveGraph in the output widget or in a box widget?


Solution

  • I found a solution by avoiding using FuncAnimation and the Output widget altogether while keeping my backend as inline. Also changing from matplotlib to bqplot was essential!

    The code is shown below (be careful because, as it is, it keeps increasing a list).

    Details of things I tried:

    I had no success updating the graph by a thread when using the Output Widget (tried clearing axes with ax.clear, redrawing the whole plot - since it is a static backend - and also using clear_output() command). Also, ipywidgets does not allow placing a matplotlib figure straight inside a container, but it does if it is a bqplot figure!

    I hope this answer helps anyone trying to integrate ipywidgets with a plot that constantly updates itself within an interface full of other widgets.

    %matplotlib inline
    import bqplot.pyplot as plt
    from threading import Thread, Lock
    import time
    import ipywidgets as widgets
    from IPython.display import display
    import numpy as np
    
    fig = plt.figure()
    t, value = [], []
    lines = plt.plot(x=t, y=value)
    
    # Function that updates baseline to slider value
    def update_baseline(v):
        global base, lock
        with lock:
            new_value = v['new']
            base = new_value
        
    slider = widgets.IntSlider(
        value=10,
        min=0,
        max=200,
        step=1,
        description='value:',
        disabled=False,
        continuous_update=False,
        orientation='horizontal',
        readout=True,
        readout_format='d'
    )
    base = slider.value
    slider.observe(update_baseline, names = 'value')
    
    def thread_f():
        global t, value, base, lines
        x = 0
        while True:
            t.append(x)
            x += 1
            value.append(base)
            with lines.hold_sync():
                lines.x = t
                lines.y = value
            
            time.sleep(0.1)
            
    lock = Lock()
    th = Thread(target=thread_f, daemon=True)
    # start thread
    th.start()
    
    vbox = widgets.VBox([slider,fig], align_self='stretch',justify_content='center')       
    vbox
    

    P.S. I'm new to using threads, so be careful as the thread may not be properly stopped with this code.

    bqplot==0.12.29
    ipywidgets==7.6.3
    numpy==1.20.2