You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jupyter Notebook中matplotlib实时更新多图表避免闪烁问题

在Jupyter Notebook中实现无闪烁的双资源使用率实时图表

需求说明

希望在Jupyter Notebook中实时展示CPU和RAM的历史使用率,效果类似Windows系统的Process Explorer(进程资源管理器):
process explorer plots

不需要交互功能,因此使用inline模式的matplotlib。运行独立后台线程更新两个不同图表时,仅更新单个图表运行正常,第二个图表会出现闪烁、重复绘制问题:
第二个图表闪烁异常效果

运行环境

已安装依赖版本如下:

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
    """
    fig, ax = plt.subplots(figsize=(15, 1.2))
    # Y axis min-max
    ax.set_ylim(0, 100)
    ax.grid(axis='y')
    # right tick labels
    ax.yaxis.tick_right()
    # hide ticks
    ax.yaxis.set_ticks_position('none')
    ax.set_frame_on(False)
    dat = pickle.dumps(fig)
    plt.close()
    return dat

def load_figure(dump):
    """
    Load Figure from dump
    
    Returns: (Figure, Axes)
    """
    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)
    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

问题原因

  1. matplotlib inline模式使用全局figure状态管理,连续在两个Output上下文调用plt.show()时,未清理的前一个figure会被带到下一个渲染流程,导致重复绘制
  2. 每次更新都重新反序列化图表模板、重新生成绘图对象,开销大且容易出现状态冲突
  3. 两个Output的渲染操作没有同步,抢占内核输出资源导致闪烁

修复方案

修改后的代码如下,可实现两个图表无闪烁实时更新:

import threading
import time

import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np
from IPython.display import display

# 提前为两个图表初始化独立的绘图对象,避免每次重建
def init_ax(color):
    fig, ax = plt.subplots(figsize=(15, 1.2))
    ax.set_ylim(0, 100)
    ax.grid(axis='y')
    ax.yaxis.tick_right()
    ax.yaxis.set_ticks_position('none')
    ax.set_frame_on(False)
    # 初始化空线条对象,后续直接更新数据即可
    line, = ax.plot([], color=color)
    plt.close(fig)
    return fig, ax, line

fig1, ax1, line1 = init_ax('green')
fig2, ax2, line2 = init_ax('red')

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])

# 加锁避免渲染操作抢占资源
render_lock = threading.Lock()

def worker():
    # 预先生成历史数据队列,模拟滚动更新
    history_len = 50
    data1 = [0]*history_len
    data2 = [0]*history_len
    while btn_start.value:
        # 更新数据
        data1.pop(0)
        data1.append(np.random.normal(scale=20) + 50)
        data2.pop(0)
        data2.append(np.random.normal(scale=20) + 50)
        
        line1.set_ydata(data1)
        line1.set_xdata(range(history_len))
        ax1.relim()
        ax1.autoscale_view(scalex=True, scaley=False)
        
        line2.set_ydata(data2)
        line2.set_xdata(range(history_len))
        ax2.relim()
        ax2.autoscale_view(scalex=True, scaley=False)
        
        # 一次性更新两个Output,加锁保证原子性
        with render_lock:
            with plt1_parent:
                plt1_parent.clear_output(wait=True)
                display(fig1)
            with plt2_parent:
                plt2_parent.clear_output(wait=True)
                display(fig2)
        
        time.sleep(1)

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

_interface

内容的提问来源于stack exchange,提问作者Winand

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 05:12:04