Threading with Matplotlib and ipywidgets

I am having trouble finding a way to update a plot in real time with new data points while also having access to user input through widgets. Here is a quick example, the goal is to have the plot updating in real time and also be able to change the xlim of the plot using the slider widget. Any help would be much appreciated!

%matplotlib inline
import ipywidgets as widgets
from IPython.display import display
from IPython.display import clear_output
import matplotlib.pyplot as plt
import random
import time
import threading

output = widgets.Output(layout={'border': '3px solid black'})
slider = widgets.IntSlider(min = 10, max = 200, value = 50, step = 1)

display(output,slider)

def slider_handler(change):
    slider.value = change.new

slider.observe(slider_handler, names='value')
   
array = []
def work(slider):
    #display(layout)
    while True:
        fig, ax = plt.subplots()
        array.append(random.randint(0,10))
        ax.plot(array)
        ax.set_xlim(len(array) - slider.value, len(array))
        time.sleep(1)
        with output:
            output.clear_output(wait=True)
            plt.show()

thread = threading.Thread(target = work, args = (slider,))
thread.start()

Some consideration:

  • rather than threads, consider using the event loop
    • if you absolutely need threads, consider still using asyncio, and a deque
  • maybe have a look at using ipympl
    • doing full display updates are pretty expensive

Here’s a basic pattern: it delegates the asyncio stuff to an interval-based loop. Ideally, the do_work would also be async, but matplotlib isn’t.

import ipywidgets, asyncio, time
output = ipywidgets.Textarea()
running = ipywidgets.ToggleButton(description="start", icon="play")
interval = ipywidgets.FloatSlider(description="loop interval", value=1, min=0.001, max=10) # zero can hang if you don't await
ui = ipywidgets.VBox([ipywidgets.HBox([running, interval]), output])
tasks = dict()

def do_work(t):
    output.value = f"{t}: {time.time()}"

async def do_loop():
    t = 0
    while running.value:
        do_work(t)
        t += 1
        await asyncio.sleep(interval.value)

def on_running_changed(*change):
    task = tasks.pop("do_loop", None)
    output.value = f"maybe stopping {task}..."
    if task:
        output.value = f"stopping {task}..."
        task.cancel()
    if running.value:
        output.value = "starting..."
        tasks["do_loop"] = asyncio.get_event_loop().create_task(do_loop())
running.observe(on_running_changed, "value")

ui
2 Likes

Thanks! I had been trying to apply asyncio but somehow none of the code I was using was doing the trick. I built up from your template, modified a few things, and it fully works now! I think output widgets were screwing me over because they are definitely not async safe. I was able to change the main work function to an async while using matplotlib. Using the ipympl magic function was also a good suggestion.