保存交互式Matplotlib图形

问题:保存交互式Matplotlib图形

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

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

Is there a way to save a Matplotlib figure such that it can be re-opened and have typical interaction restored? (Like the .fig format in MATLAB?)

I find myself running the same scripts many times to generate these interactive figures. Or I’m sending my colleagues multiple static PNG files to show different aspects of a plot. I’d rather send the figure object and have them interact with it themselves.


回答 0

这将是一个很棒的功能,但是AFAIK并没有在Matplotlib中实现,并且由于存储数据的方式可能很难实现自己。

我建议(a)将数据处理与生成图形分开(以唯一的名称保存数据),然后编写图形生成脚本(加载已保存数据的指定文件)并根据需要进行编辑或(b )另存为PDF / SVG / PostScript格式,并在某些精美的图形编辑器(如Adobe Illustrator(或Inkscape))中进行编辑。

编辑后,2012年秋季:正如其他人在下面指出的(尽管在此提及,因为这是公认的答案),自1.2版以来,Matplotlib允许您腌制人物。如发行说明所述,这是一项实验性功能,不支持在一个matplotlib版本中保存图形并在另一个matplotlib版本中打开图形。从不受信任的来源恢复泡菜通常也是不安全的。

对于共享/以后的编辑图(需要首先进行大量数据处理,并且可能需要在几个月后进行调整,例如在科学出版物的同行评审中进行调整),我仍然建议(1)的工作流程具有数据处理脚本,该脚本在生成图之前将处理后的数据(放入绘图中)保存到文件中,并且(2)具有单独的绘图生成脚本(您可以根据需要进行调整)以重新创建绘图。通过这种方式,您可以为每个绘图快速运行脚本并重新生成脚本(并使用新数据快速复制绘图设置)。话虽如此,腌制一个人物可能会方便短期/互动/探索性数据分析。

This would be a great feature, but AFAIK it isn’t implemented in Matplotlib and likely would be difficult to implement yourself due to the way figures are stored.

I’d suggest either (a) separate processing the data from generating the figure (which saves data with a unique name) and write a figure generating script (loading a specified file of the saved data) and editing as you see fit or (b) save as PDF/SVG/PostScript format and edit in some fancy figure editor like Adobe Illustrator (or Inkscape).

EDIT post Fall 2012: As others pointed out below (though mentioning here as this is the accepted answer), Matplotlib since version 1.2 allowed you to pickle figures. As the release notes state, it is an experimental feature and does not support saving a figure in one matplotlib version and opening in another. It’s also generally unsecure to restore a pickle from an untrusted source.

For sharing/later editing plots (that require significant data processing first and may need to be tweaked months later say during peer review for a scientific publication), I still recommend the workflow of (1) have a data processing script that before generating a plot saves the processed data (that goes into your plot) into a file, and (2) have a separate plot generation script (that you adjust as necessary) to recreate the plot. This way for each plot you can quickly run a script and re-generate it (and quickly copy over your plot settings with new data). That said, pickling a figure could be convenient for short term/interactive/exploratory data analysis.


回答 1

我刚刚发现了如何做到这一点。@pelson提到的“实验泡菜支持”效果很好。

试试这个:

# 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图形获得了出色的技巧。

I just found out how to do this. The “experimental pickle support” mentioned by @pelson works quite well.

Try this:

# Plot something
import matplotlib.pyplot as plt
fig,ax = plt.subplots()
ax.plot([1,2,3],[10,-10,30])

After your interactive tweaking, save the figure object as a binary file:

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

Later, open the figure and the tweaks should be saved and GUI interactivity should be present:

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

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

You can even extract the data from the plots:

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

(It works for lines, pcolor & imshow – pcolormesh works with some tricks to reconstruct the flattened data.)

I got the excellent tip from Saving Matplotlib Figures Using Pickle.


回答 2

从Matplotlib 1.2开始,我们现在具有实验性的pickle支持。试一试,看看它是否适合您的情况。如果您有任何问题,请通过Matplotlib邮件列表或通过在github.com/matplotlib/matplotlib上打开问题来告知我们

As of Matplotlib 1.2, we now have experimental pickle support. Give that a go and see if it works well for your case. If you have any issues, please let us know on the Matplotlib mailing list or by opening an issue on github.com/matplotlib/matplotlib.


回答 3

为什么不发送Python脚本呢?MATLAB的.fig文件要求收件人具有MATLAB才能显示它们,因此,这等效于发送需要Matplotlib显示的Python脚本。

或者(免责声明:我还没有尝试过),您可以尝试腌制该图:

import pickle
output = open('interactive figure.pickle', 'wb')
pickle.dump(gcf(), output)
output.close()

Why not just send the Python script? MATLAB’s .fig files require the recipient to have MATLAB to display them, so that’s about equivalent to sending a Python script that requires Matplotlib to display.

Alternatively (disclaimer: I haven’t tried this yet), you could try pickling the figure:

import pickle
output = open('interactive figure.pickle', 'wb')
pickle.dump(gcf(), output)
output.close()

回答 4

好问题。这是来自的文档文本pylab.save

pylab不再提供保存功能,尽管旧的pylab函数仍然可以作为matplotlib.mlab.save使用(您仍然可以在pylab中将其称为“ mlab.save”)。但是,对于纯文本文件,我们建议使用numpy.savetxt。为了保存numpy数组,我们建议使用numpy.save及其类似的numpy.load,它们可以在pylab中以np.save和np.load的形式提供。

Good question. Here is the doc text from pylab.save:

pylab no longer provides a save function, though the old pylab function is still available as matplotlib.mlab.save (you can still refer to it in pylab as “mlab.save”). However, for plain text files, we recommend numpy.savetxt. For saving numpy arrays, we recommend numpy.save, and its analog numpy.load, which are available in pylab as np.save and np.load.


回答 5

我想出了一种相对简单的方法(但还有些不常规)来保存我的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指令(使用module inspect),对其进行分析(使用module ast)以提取依赖于其的所有变量,函数和模块,从执行上下文中提取这些变量并对其进行序列化如python指令(变量的代码将类似于t=[0.0,2.0,0.01]…,模块的代码将类似于import matplotlib.pyplot as plt…)附加在该图指令之前。生成的python指令将另存为python脚本,其执行将重新构建原始的matplotlib图。

可以想象,这对于大多数(如果不是全部)matplotlib图形都适用。

I figured out a relatively simple way (yet slightly unconventional) to save my matplotlib figures. It works like this:

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

with function save_plot defined like this (simple version to understand the logic):

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'

or defining function save_plot like this (better version using zip compression to produce lighter figure files):

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'

This makes use a module libscript of my own, which mostly relies on modules inspect and ast. I can try to share it on Github if interest is expressed (it would first require some cleanup and me to get started with Github).

The idea behind this save_plot function and libscript module is to fetch the python instructions that create the figure (using module inspect), analyze them (using module ast) to extract all variables, functions and modules import it relies on, extract these from the execution context and serialize them as python instructions (code for variables will be like t=[0.0,2.0,0.01] … and code for modules will be like import matplotlib.pyplot as plt …) prepended to the figure instructions. The resulting python instructions are saved as a python script whose execution will re-build the original matplotlib figure.

As you can imagine, this works well for most (if not all) matplotlib figures.