保存交互式的Matplotlib图形。

156

有没有一种方法可以保存 Matplotlib 图形,以便重新打开并恢复典型的交互?(类似于 MATLAB 中的 .fig 格式?)

我发现自己多次运行相同的脚本来生成这些交互式图形。或者我向同事发送多个静态 PNG 文件以展示绘图的不同方面。我希望能够发送图形对象,并让他们自己与之交互。

7个回答

95
我刚刚发现了如何做到这一点。@pelson提到的“实验性pickle支持”效果很不错。
试试这个:
# Plot something
import matplotlib.pyplot as plt
fig,ax = plt.subplots()
ax.plot([1,2,3],[10,-10,30])

在您交互调整后,将图形对象保存为二进制文件:

import pickle
pickle.dump(fig, open('FigureObject.fig.pickle', 'wb')) # This is for Python 3 - py2 may need `file` instead of `open`

稍后打开该图,并应保存调整并保留GUI交互性:

import pickle
figx = pickle.load(open('FigureObject.fig.pickle', 'rb'))

figx.show() # Show the figure, edit it, etc.!

您甚至可以从图表中提取数据:

data = figx.axes[0].lines[0].get_data()

(它适用于线条、pcolor和imshow - pcolormesh使用一些技巧来重构扁平化的数据。)

我从使用Pickle保存Matplotlib图形获得了出色的提示。


我认为这种方法不够稳健,容易受版本变化等影响,并且不能在py2.x和py3.x之间进行交叉兼容。此外,我认为pickle文档中指出我们需要设置环境与对象被pickled(保存)时类似,但我发现在unpickling(加载)时不需要import matplotlib.pyplot as plt - 它会将导入语句保存在pickled文件中。 - Demis
6
你应该考虑关闭已打开的文件,例如:with open('FigureObject.fig.pickle', 'rb') as file: figx = pickle.load(file) - strpeter
1
我刚刚遇到了这个问题:'AttributeError: 'Figure' object has no attribute '_cachedRenderer'' - Timo Kvamme
2
如果您不希望脚本在 figx.show() 后继续运行并可能立即终止,您应该调用 plt.show(),它是阻塞的。 - maechler
很棒的答案!不幸的是,最后一个链接(使用Pickle保存Matplotlib图形)目前已经失效。 - Adriaan
链接对我来说似乎是有效的。很高兴它有帮助,我喜欢这个功能。 - Demis

42

2
有没有任何理由可以将这个有用的功能添加到图形的“另存为”本身中呢?是否可以加上“.pkl”作为保存格式? - dashesy
不错的想法 @dashesy。如果你想尝试实现它,我会支持的。 - pelson
1
这只在一部分后端上有效吗? 当我尝试在OSX上pickle一个简单的图像时,会出现“PicklingError:Can't pickle <type '_macosx.GraphicsContext'>: it's not found as _macosx.GraphicsContext”的错误。 - farenorth
只有在执行pickle之前调用plt.show()才会出现上述的PicklingError。因此,只需在pickle.dump()之后放置plt.show()即可。 - salomonvh
在我的MacOS 10.11上的py3.5中,fig.show()的顺序似乎并不重要 - 或许那个bug已经被修复了。我可以在show()之前/之后进行pickle而没有问题。 - Demis

36

这将是一个很棒的功能,但据我所知,Matplotlib尚未实现,由于图形存储的方式,自己实现可能会很困难。

我建议要么(a)分别处理数据并生成图表(将数据保存为唯一名称),编写一个图表生成脚本(加载指定的已保存数据文件),可根据需要进行编辑,或者(b)保存为PDF/SVG/PostScript格式,并在一些高级图形编辑器中进行编辑,如Adobe Illustrator(或Inkscape)。

2012年秋季更新: 如下面其他人所指出的(虽然在此作为被接受的答案提到),自Matplotlib 1.2版本以来,它允许您拾取图形。正如发布说明所述,这是一个实验性功能,不支持在一个Matplotlib版本中保存一个图形并在另一个版本中打开。从不受信任的来源恢复pickle也通常是不安全的。

对于共享/稍后编辑的绘图(需要进行重要数据处理并且可能需要在以后几个月内进行调整,例如在科学出版物的同行评审期间),我仍然建议使用以下工作流程:(1)编写一个数据处理脚本,在生成图表之前将处理好的数据(用于您的图表)保存到文件中;(2)拥有单独的图表生成脚本(根据需要进行调整)来重新创建图表。这样,对于每个图表,您可以快速运行脚本并重新生成它(并快速复制您的新数据的绘图设置)。尽管如此,pickling一个图表对于短期/交互式/探索性数据分析可能是方便的。


2
有点惊讶这个还没有被实现...但好吧,我会把处理后的数据保存在一个中间文件中,并将其和绘图脚本一起发送给同事。谢谢。 - Matt
2
我怀疑实现很困难,这就是为什么在MATLAB中它的表现如此糟糕。当我使用它时,图形会导致MATLAB崩溃,即使是稍微不同的版本也无法读取彼此的.fig文件。 - Adrian Ratnapala
6
pickle现在可以用于MPL图形,因此可以这样做,并且似乎工作得相当不错 - 就像Matlab的".fig"图形文件。 请参见下面的答案(目前)以了解如何执行此操作的示例。 - Demis
无论如何,您应该在整个数据分析过程中保持Python环境的稳定性,直到科学出版物发布之后,然后使用pickling即可。 - olq_plo
在没有保留确切的软件环境的情况下进行 Pickling 是没有意义的,我同意,但是为了长期重现结果,您仍然需要保留软件环境。然后你也可以使用 pickle。 - olq_plo
显示剩余4条评论

7
为什么不直接发送Python脚本呢?MATLAB的.fig文件需要接收者安装MATLAB才能显示,这与发送需要Matplotlib才能显示的Python脚本相当。或者(免责声明:我还没有尝试过),您可以尝试使用pickle保存图像。
import pickle
output = open('interactive figure.pickle', 'wb')
pickle.dump(gcf(), output)
output.close()

3
很遗憾,matplotlib图形不支持pickle处理,因此这种方法行不通。在幕后,有太多不支持pickling的C扩展程序。我完全同意只发送脚本+数据的做法...我猜我从来没有真正看到过matlab的保存为.fig文件的意义,所以我从未使用过它们。从我的经验来看,将独立的代码和数据发送给某人是最容易的方法。但是,如果matplotlib的图形对象可以被pickle处理,那就太好了。 - Joe Kington
1
即使我们的预处理数据有些庞大,而且绘图过程也很复杂。看起来这似乎是唯一的选择。谢谢。 - Matt
1
数字显然现在可以被pickle化 - 它运行得非常好!以下是示例。 - Demis
pickle的好处在于您不必以编程方式调整所有图形/子图间距/位置。 您可以使用MPL绘图的GUI使图形看起来漂亮等,然后保存“MyPlot.fig.pickle”文件-保留稍后根据需要调整绘图演示的能力。 这也是Matlab的“.fig”文件的优点。 当您需要更改fig的大小/纵横比(用于插入演示文稿/论文)时,特别有用。 - Demis

2

很好的问题。这里是来自pylab.save的文档:

尽管旧版的pylab函数仍然可用(您仍可以在pylab中引用它作为“mlab.save”),但现在的pylab不再提供保存函数。然而,对于纯文本文件,我们建议使用numpy.savetxt。对于保存numpy数组,我们建议使用numpy.save及其类似物numpy.load,它们在pylab中可用作np.save和np.load。


这将保存来自pylab对象的数据,但不允许您重新生成图形。 - dr jimbob
正确。我应该澄清一下,答案并不是建议使用 pylab.save。实际上,从文档中可以看出,不应该使用它。 - Steve Tjoa
有没有外部方法可以发送3D图形?甚至可能是一个简单的GUI到exe。 - CromeX

0

我找到了一个相对简单的方法(虽然有点不寻常)来保存我的matplotlib图形。它的工作原理如下:

import libscript

import matplotlib.pyplot as plt
import numpy as np

t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2*np.pi*t)

#<plot>
plt.plot(t, s)
plt.xlabel('time (s)')
plt.ylabel('voltage (mV)')
plt.title('About as simple as it gets, folks')
plt.grid(True)
plt.show()
#</plot>

save_plot(fileName='plot_01.py',obj=sys.argv[0],sel='plot',ctx=libscript.get_ctx(ctx_global=globals(),ctx_local=locals()))

具有以下定义的函数save_plot(简化版本以便理解逻辑):

def save_plot(fileName='',obj=None,sel='',ctx={}):
    """
    Save of matplolib plot to a stand alone python script containing all the data and configuration instructions to regenerate the interactive matplotlib figure.

    Parameters
    ----------
    fileName : [string] Path of the python script file to be created.
    obj : [object] Function or python object containing the lines of code to create and configure the plot to be saved.
    sel : [string] Name of the tag enclosing the lines of code to create and configure the plot to be saved.
    ctx : [dict] Dictionary containing the execution context. Values for variables not defined in the lines of code for the plot will be fetched from the context.

    Returns
    -------
    Return ``'done'`` once the plot has been saved to a python script file. This file contains all the input data and configuration to re-create the original interactive matplotlib figure.
    """
    import os
    import libscript

    N_indent=4

    src=libscript.get_src(obj=obj,sel=sel)
    src=libscript.prepend_ctx(src=src,ctx=ctx,debug=False)
    src='\n'.join([' '*N_indent+line for line in src.split('\n')])

    if(os.path.isfile(fileName)): os.remove(fileName)
    with open(fileName,'w') as f:
        f.write('import sys\n')
        f.write('sys.dont_write_bytecode=True\n')
        f.write('def main():\n')
        f.write(src+'\n')

        f.write('if(__name__=="__main__"):\n')
        f.write(' '*N_indent+'main()\n')

return 'done'

或者像这样定义函数save_plot(使用zip压缩生成较轻的图形文件,更好的版本):

def save_plot(fileName='',obj=None,sel='',ctx={}):

    import os
    import json
    import zlib
    import base64
    import libscript

    N_indent=4
    level=9#0 to 9, default: 6
    src=libscript.get_src(obj=obj,sel=sel)
    obj=libscript.load_obj(src=src,ctx=ctx,debug=False)
    bin=base64.b64encode(zlib.compress(json.dumps(obj),level))

    if(os.path.isfile(fileName)): os.remove(fileName)
    with open(fileName,'w') as f:
        f.write('import sys\n')
        f.write('sys.dont_write_bytecode=True\n')
        f.write('def main():\n')
        f.write(' '*N_indent+'import base64\n')
        f.write(' '*N_indent+'import zlib\n')
        f.write(' '*N_indent+'import json\n')
        f.write(' '*N_indent+'import libscript\n')
        f.write(' '*N_indent+'bin="'+str(bin)+'"\n')
        f.write(' '*N_indent+'obj=json.loads(zlib.decompress(base64.b64decode(bin)))\n')
        f.write(' '*N_indent+'libscript.exec_obj(obj=obj,tempfile=False)\n')

        f.write('if(__name__=="__main__"):\n')
        f.write(' '*N_indent+'main()\n')

return 'done'

这里使用了我自己的一个模块libscript,它主要依赖于inspectast模块。如果有兴趣的话,我可以尝试在Github上分享它(首先需要进行一些清理工作并开始使用Github)。

save_plot函数和libscript模块背后的想法是获取创建图形的Python指令(使用inspect模块),分析它们(使用ast模块)以提取所有变量、函数和模块导入所依赖的内容,从执行上下文中提取这些内容,并将它们序列化为Python指令(变量的代码将类似于t=[0.0,2.0,0.01],模块的代码将类似于import matplotlib.pyplot as plt...),并将其作为前缀添加到图形指令中。生成的Python指令被保存为一个Python脚本,执行该脚本将重新构建原始的matplotlib图形。

正如您所想象的那样,这对于大多数(如果不是全部)matplotlib图形都有效。


0

如果您想将 Python 绘图保存为交互式图以便像 MATLAB .fig 文件那样进行修改并与他人共享,则可以尝试使用以下代码。在这里,z_data.values 仅是一个 numpy ndarray,因此您可以使用相同的代码来绘制和保存自己的数据,无需使用 pandas。

此处生成的文件可以通过单击它并在 Chrome/Firefox/Edge 等浏览器中打开,任何人都可以进行交互式修改,无论是否具有 python。

import plotly.graph_objects as go
import pandas as pd

z_data=pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/api_docs/mt_bruno_elevation.csv')

fig = go.Figure(data=[go.Surface(z=z_data.values)])

fig.update_layout(title='Mt Bruno Elevation', autosize=False,
                  width=500, height=500,
                  margin=dict(l=65, r=50, b=65, t=90))

fig.show()
fig.write_html("testfile.html")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接