Matplotlib交互的IPython Notebook小部件

10 投票
3 回答
8859 浏览
提问于 2025-04-18 14:47

我想用ipython notebook里的小工具来给内嵌的matplotlib图表增加一些互动性。

一般来说,图表可能会比较复杂,我只想更新图表中的某个特定部分。我知道小工具有一个内置的限制功能,可以防止过多请求涌入内核,但如果图表更新需要30秒,我可不想等那么久才更新一条线。

通过阅读示例笔记本,我创建了一个基本的例子,在mpl坐标轴上添加了一个十字光标(由两个滑块控制)。

问题是,图形显示了两次。以下是代码(单元格1):

fig, ax = plt.subplots() 
ax.plot([3,1,2,4,0,5,3,2,0,2,4])

... 图形显示了 ...,单元格2(编辑:感谢Thomas K的改进):

vline = ax.axvline(1)
hline = ax.axhline(0.5)

def set_cursor(x, y):
    vline.set_xdata((x, x))
    hline.set_ydata((y, y))
    display(fig)

最后(单元格3):

interact(set_cursor, x=(1, 9, 0.01), y=(0, 5, 0.01))

再次显示了带有小工具的图形。

所以问题是:

  1. 我怎么才能阻止第一次图形显示?
  2. 这样做对吗?还是有更好的方法?

编辑

我找到一个ipython配置选项,按照这个笔记本的说法,可以阻止图形显示。

%config InlineBackend.close_figures = False

虽然示例笔记本可以工作,但我不知道如何单独使用这个选项(不使用链接示例中的上下文管理类)来隐藏图形显示。

编辑2

我找到了一些关于InlineBackend.close_figures可配置项的文档

编辑3

受到@shadanan回答的启发,我想澄清一下我的目的,是在现有图形上添加一个光标,而不是在每次光标移动时都重新绘制图表。将三个单元合并成一个单元:

fig, ax = plt.subplots()
ax.plot([3,1,2,4,0,5,3,2,0,2,4])

vline = ax.axvline(1)
hline = ax.axhline(0.5)

def set_cursor(x, y):
    vline.set_xdata((x, x))
    hline.set_ydata((y, y))
    display(fig)

interact(set_cursor, x=(1, 9, 0.01), y=(0, 5, 0.01))

理论上“应该”可以工作,但实际上并不行。第一次执行单元时显示了两个图形。经过小工具交互后只显示一个图形。这就是需要像@shadanan回答中那样的解决方法的“奇怪行为”。ipython的开发者能对此做个评论吗?这是个bug吗?

3 个回答

2

你可以通过一种非常简单的方法来实现这个,使用新一点的 notebook 后端。

%matplotlib notebook
import matplotlib.pyplot as plt
from IPython.html.widgets import interactive

fig, ax = plt.subplots()
ax.plot(range(5))


vline = ax.axvline(1, color='k')
hline = ax.axhline(0.5, color='k')

def set_cursor(x, y):
    vline.set_xdata((x, x))
    hline.set_ydata((y, y))
    ax.figure.canvas.draw_idle()

然后在一个单独的单元格中:

interactive(set_cursor, x=ax.get_xlim(), y=ax.get_ylim())

每次你移动光标时,这个方法仍然会重新绘制整个图形,因为 notebook 目前不支持“blitting”技术(这个功能正在开发中,详细信息可以查看 https://github.com/matplotlib/matplotlib/pull/4290)。

2

我有一个临时的解决办法,只能显示一个图形。问题在于代码中有两个地方会生成图形,而我们其实只想要第二个,但不能简单地去掉第一个。这个解决办法是第一次执行时用第一个,之后的执行就用第二个。下面的代码通过一个初始化的标志来在两者之间切换:

%matplotlib inline
import matplotlib.pyplot as plt
from IPython.html.widgets import interact, interactive, fixed
from IPython.html import widgets
from IPython.display import clear_output, display, HTML

class InteractiveCursor(object):
    initialized = False
    fig = None
    ax = None
    vline = None
    hline = None

    def initialize(self):
        self.fig, self.ax = plt.subplots()
        self.ax.plot([3,1,2,4,0,5,3,2,0,2,4])
        self.vline = self.ax.axvline(1)
        self.hline = self.ax.axhline(0.5)

    def set_cursor(self, x, y):
        if not self.initialized:
            self.initialize()

        self.vline.set_xdata((x, x))
        self.hline.set_ydata((y, y))

        if self.initialized:
            display(self.fig)
        self.initialized = True

ic = InteractiveCursor()
def set_cursor(x, y):
    ic.set_cursor(x, y)

interact(set_cursor, x=(1, 9, 0.01), y=(0, 5, 0.01));

我认为这应该算是一个bug。我用面向对象的接口试过,结果也是同样的问题。

5

这个解决方案其实非常简单。为了避免显示第一个图形,我们只需要在调用 interact 之前加上一个 close() 的命令。

回想一下问题中的例子,像这样的单元格将正确显示一个交互式图形(而不是两个):

fig, ax = plt.subplots()
ax.plot([3,1,2,4,0,5,3,2,0,2,4])
plt.close(fig)

vline = ax.axvline(1)
hline = ax.axhline(0.5)

def set_cursor(x, y):
    vline.set_xdata((x, x))
    hline.set_ydata((y, y))
    display(fig)

interact(set_cursor, x=(1, 9, 0.01), y=(0, 5, 0.01))

一个更简洁的方法是定义一个叫 add_cursor 的函数(可以在单独的单元格或脚本中):

def add_cursor(fig, ax):
    plt.close(fig)

    vline = ax.axvline(1, color='k')
    hline = ax.axhline(0.5, color='k')

    def set_cursor(x, y):
        vline.set_xdata((x, x))
        hline.set_ydata((y, y))
        display(fig)

    interact(set_cursor, x=ax.get_xlim(), y=ax.get_ylim())

然后每当我们想添加一个交互式光标时,就调用它:

fig, ax = plt.subplots()
ax.plot([3,1,2,4,0,5,3,2,0,2,4])
add_cursor(fig, ax)

撰写回答