3

I want to display history of CPU and RAM usage in Jupyter Notebook in real time. Something like this (Process Explorer in Windows): process explorer plots

I don't interactivity so I use matplotlib in inline mode. I run a separate background thread and try to update two different plots from there. It works well with one plot but the second one blinks and has duplicates.

enter image description here

Here's a minimal example (also I pickle/unpickle plot so I can initialize it only once and reuse later).

Installed packages:

ipykernel                         5.1.3
ipywidgets                        7.5.1
jupyter                           1.0.0
jupyter-core                      4.6.1
matplotlib                        3.1.1 
notebook                          6.0.0
import pickle
import threading
import time

import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np


def init_history_plot():
    """
    Create plot template (dump)
    
    Returns: pickled str
    """
#     plt.figure(figsize=(15, 1.2))
#     ax = plt.axes()
    fig, ax = plt.subplots(figsize=(15, 1.2))
    # Y axis min-max
    ax.set_ylim(0, 100)
#     ax.get_xaxis().set_visible(False)
    ax.grid(axis='y')
    # right tick labels https://stackoverflow.com/a/13369977
    ax.yaxis.tick_right()
    # hide ticks https://stackoverflow.com/a/33707647
    ax.yaxis.set_ticks_position('none')
    # borders https://stackoverflow.com/a/27361819
    # for i in ax.spines.values():  # 'left', 'right', 'top', 'bottom'
    #     ax.spines[i].set_visible(False)
    # https://stackoverflow.com/questions/18603959/borderless-matplotlib-plots
    ax.set_frame_on(False)
    dat = pickle.dumps(fig)
    plt.close()
    return dat

def load_figure(dump):
    """
    Load Figure from dump
    
    Returns: (Figure, Axes)
    """
    # https://github.com/ipython/ipykernel/issues/231
    import ipykernel.pylab.backend_inline as back_inline
    import matplotlib.backends.backend_agg as back_agg
    back_inline.new_figure_manager_given_figure = back_agg.new_figure_manager_given_figure
    figure = pickle.loads(dump)
    # https://github.com/matplotlib/matplotlib/issues/17627/
    figure._cachedRenderer = None
    return figure, figure.axes[0]


template_fig = init_history_plot()

btn_start = widgets.ToggleButton(description="Start thread")
plt1_parent = widgets.Output()
plt2_parent = widgets.Output()
_interface = widgets.VBox(children=[btn_start, plt1_parent, plt2_parent])

def worker():
    while btn_start.value:
        with plt1_parent:
            plt1_parent.clear_output(wait=True)
            fig, ax = load_figure(template_fig)
            dat = np.random.normal(scale=20, size=50) + 50
            ax.plot(dat, color='green')
            plt.show()
        # THE FOLLOWING BLOCK BLINKS
        with plt2_parent:
            plt2_parent.clear_output(wait=True)
            fig, ax = load_figure(template_fig)
            dat = np.random.normal(scale=20, size=50) + 50
            ax.plot(dat, color='red')
            plt.show()
        ############################
        time.sleep(1)

def start_thread(_):
    if btn_start.value:
        thread = threading.Thread(target=worker)
        thread.start()
btn_start.observe(start_thread, 'value')

_interface
Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Winand
  • 2,093
  • 3
  • 28
  • 48

1 Answers1

0

I think your code is really cool, and so I greedily stole it because I also want to have plots that live-update in a Jupyter notebook without blocking the kernel.

Anyway, the weird flickering was bothering me as well. I tried switching from threading to asyncio, simply because I am more familiar with asyncio than threading, and that actually seems to have solved the problem! Though I have no idea why.

Try:

import pickle
import asyncio
import time

import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np


def init_history_plot():
    """
    Create plot template (dump)
    
    Returns: pickled str
    """
#     plt.figure(figsize=(15, 1.2))
#     ax = plt.axes()
    fig, ax = plt.subplots(figsize=(15, 1.2))
    # Y axis min-max
    ax.set_ylim(0, 100)
#     ax.get_xaxis().set_visible(False)
    ax.grid(axis='y')
    # right tick labels https://stackoverflow.com/a/13369977
    ax.yaxis.tick_right()
    # hide ticks https://stackoverflow.com/a/33707647
    ax.yaxis.set_ticks_position('none')
    # borders https://stackoverflow.com/a/27361819
    # for i in ax.spines.values():  # 'left', 'right', 'top', 'bottom'
    #     ax.spines[i].set_visible(False)
    # https://stackoverflow.com/questions/18603959/borderless-matplotlib-plots
    ax.set_frame_on(False)
    dat = pickle.dumps(fig)
    plt.close()
    return dat

def load_figure(dump):
    """
    Load Figure from dump
    
    Returns: (Figure, Axes)
    """
    # https://github.com/ipython/ipykernel/issues/231
    import ipykernel.pylab.backend_inline as back_inline
    import matplotlib.backends.backend_agg as back_agg
    back_inline.new_figure_manager_given_figure = back_agg.new_figure_manager_given_figure
    figure = pickle.loads(dump)
    # https://github.com/matplotlib/matplotlib/issues/17627/
    figure._cachedRenderer = None
    return figure, figure.axes[0]


template_fig = init_history_plot()

btn_start = widgets.ToggleButton(description="Start thread")
plt1_parent = widgets.Output()
plt2_parent = widgets.Output()
_interface = widgets.VBox(children=[btn_start, plt1_parent, plt2_parent])

async def worker():
    while btn_start.value:
        with plt1_parent:
            plt1_parent.clear_output(wait=True)
            fig, ax = load_figure(template_fig)
            dat = np.random.normal(scale=20, size=50) + 50
            ax.plot(dat, color='green')
            plt.show()
        with plt2_parent:
            plt2_parent.clear_output(wait=True)
            fig, ax = load_figure(template_fig)
            dat = np.random.normal(scale=20, size=50) + 50
            ax.plot(dat, color='red')
            plt.show()
        await asyncio.sleep(1)

def start_thread(_):
    if btn_start.value:
        task = asyncio.create_task(worker())
btn_start.observe(start_thread, 'value')

_interface

jkflowers
  • 81
  • 6