Jupyter Notebook中matplotlib实时更新多图表避免闪烁问题
在Jupyter Notebook中实现无闪烁的双资源使用率实时图表
需求说明
希望在Jupyter Notebook中实时展示CPU和RAM的历史使用率,效果类似Windows系统的Process Explorer(进程资源管理器):
不需要交互功能,因此使用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
问题原因
- matplotlib inline模式使用全局figure状态管理,连续在两个Output上下文调用
plt.show()时,未清理的前一个figure会被带到下一个渲染流程,导致重复绘制 - 每次更新都重新反序列化图表模板、重新生成绘图对象,开销大且容易出现状态冲突
- 两个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
相关产品推荐
相关产品推荐

