151

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Matt
  • 4,815
  • 5
  • 39
  • 40

7 Answers7

93

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.

Community
  • 1
  • 1
Demis
  • 5,278
  • 4
  • 23
  • 34
  • I believe this is not robust to version changes etc., and not cross-compatible between py2.x & py3.x. Also I thought the `pickle` documentation stated that we need to setup the environment similarly as when the object was pickled (saved), but I've found you don't need to `import matplotlib.pyplot as plt` when unpickling (loading) - it saves the import statements in the pickled file. – Demis Jan 30 '16 at 09:41
  • 6
    You should consider closing opened files: e.g. `with open('FigureObject.fig.pickle', 'rb') as file: figx = pickle.load(file)` – strpeter May 17 '18 at 12:11
  • 1
    I just get: 'AttributeError: 'Figure' object has no attribute '_cachedRenderer'' – Timo Kvamme Sep 17 '18 at 16:18
  • 2
    If you do not want the script to continue and probably terminate immediately after `figx.show()`, you should call `plt.show()` instead, which is blocking. – maechler Oct 22 '19 at 05:49
  • Great answer! Unfortunately, the last link (Saving Matplotlib Figures Using Pickle) is dead as of this moment. – Adriaan Oct 08 '21 at 07:47
  • Link appears to be live for me. Glad it helped, I love this feature. – Demis Oct 08 '21 at 19:21
42

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
pelson
  • 21,252
  • 4
  • 92
  • 99
  • 2
    Any reason this useful feature could be added to the figure's "Save as" itself. Adding .pkl perhaps? – dashesy Jan 29 '14 at 22:46
  • Good idea @dashesy. I'd support that if you wanted to give implementing it a go? – pelson May 30 '14 at 08:14
  • 1
    Does this only work on a subset of backends? When I try to pickle a simple figure in OSX, I get ``PicklingError: Can't pickle : it's not found as _macosx.GraphicsContext``. – farenorth Jun 09 '15 at 17:10
  • The above `PicklingError` only occurs if you call `plt.show()` before doing the pickle. So just place `plt.show()` after `pickle.dump()`. – salomonvh Oct 08 '15 at 10:18
  • On my py3.5 on MacOS 10.11, the order of `fig.show()` doesn't appear to matter - maybe that bug was fixed. I can pickle before/after `show()` without issue. – Demis Feb 01 '16 at 19:42
36

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.

jimh
  • 1,651
  • 2
  • 15
  • 28
dr jimbob
  • 17,259
  • 7
  • 59
  • 81
  • 2
    Somewhat surprised this isn't implemented.. But ok, I'll save the processed data in an intermediate file and send that and a script for plotting to colleagues. Thanks. – Matt Dec 06 '10 at 18:31
  • 2
    I suspect implementation is hard, which is why it works so poorly an MATLAB. Back when I used it, figures used to crash MATLAB, and even slightly different versions were not able to read each others .fig files. – Adrian Ratnapala Oct 11 '11 at 10:09
  • 6
    `pickle` now works on MPL figures, so this can be done and appears to work reasonable well - almost like a Matlab ".fig" figure file. See my answer below (for now?) for an example of how to do it. – Demis Jan 30 '16 at 09:43
  • @Demis: as ptomato pointed out in his answer below, it existed already at that time. – strpeter May 23 '18 at 09:52
  • @strpeter - Matlab figures weren't pickleable in 2010 as pointed out in [this comment](https://stackoverflow.com/questions/4348733/saving-interactive-matplotlib-figures/4348932?noredirect=1#comment4734371_4351517). The experimental feature was added with [matplotlib 1.2 released in Fall 2012](https://matplotlib.org/users/prev_whats_new/whats_new_1.2.html#figures-are-picklable). As noted there, you shouldn't expect it to work between matplotlib versions and you shouldn't open pickles coming from an untrusted source. – dr jimbob May 24 '18 at 17:46
  • If you split your processing and viewing as dr jimbob suggested, you can save your data with `.npy` files. But then, it's a bit of a hassle to run the script on this file, so what I did (I'm on ubuntu) is put `.desktop` entries in `/usr/share/applications` that launch the appropriate Python script, so that I just have to right click on the file I want to view, open with... and select the script I want, and it generates my figure on the fly ! You can look at my examples at https://github.com/matthieuheitz/npy_viewer. Have fun ! – matthieu Oct 26 '18 at 14:10
  • You should keep your Python environment stable for the whole data analysis until scientific publication anyway, and then pickling is fine. – olq_plo Mar 26 '21 at 09:31
  • @olq_plo - Agree it's a good idea to keep your environment stable (or at least be aware of changes and reprocess everything using the latest version). That said, it's good to be able to recreate your figures from your saved processed data in case anyone ever questions it. Having scripts saved that explain your choices makes tons of sense. Having a saved pickle to reopen a figure in matplotlib could be convenient, but it's not going to satisfy questions if someone (or yourself) can't reproduce some figure you made at a later date. – dr jimbob Mar 26 '21 at 14:43
  • Pickling without retaining the exact software environment makes no sense, I agree, but to reproduce the results long-term you need to preserve the software environment anyway. And theb you can also use pickle. – olq_plo Mar 26 '21 at 17:04
7

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()
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
ptomato
  • 56,175
  • 13
  • 112
  • 165
  • 3
    Unfortunately, matplotlib figures aren't pickleable, so that approach won't work. Behind the scenes, there are too many C extensions that don't support pickling. I completely agree on just sending the script + data, though... I guess I've never really seen the point of matlab's saved .fig's, so I never used them. Sending someone stand-alone code and data has been the easiest in the long run, in my experience, anyway. Still, it would be nice if matplotlib's figure objects where pickleable. – Joe Kington Dec 04 '10 at 04:04
  • 1
    Even our preprocessed data is somewhat large and the plotting procedure is complex. Looks like the only recourse though. thanks. – Matt Dec 06 '10 at 18:34
  • 1
    Figures are apparently now pickleable - it works quite well! Example below. – Demis Jan 30 '16 at 09:44
  • The benefit to pickle is that you don't have to programmatically adjust all the figure/subplot spacings/positions. You can use the MPL plot's GUI to make the figure look nice etc., then save the `MyPlot.fig.pickle` file - retaining the later ability to adjust the plot presentation as needed. This is also what's great about Matlab's `.fig` files. Especially useful when you need to change the size/aspect ratio of a fig (for inserting into presentations/papers). – Demis Feb 01 '16 at 19:45
2

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.

Steve Tjoa
  • 59,122
  • 18
  • 90
  • 101
  • This saves the data from the a pylab object, but doesn't allow you to regenerate the figure. – dr jimbob Dec 03 '10 at 19:06
  • Correct. I ought to clarify that the answer was not a recommendation to use `pylab.save`. In fact, from the doc text, it appears that one should *not* use it. – Steve Tjoa Dec 03 '10 at 20:55
  • Is there any external method to send a 3D Figure? Possible even a simple GUI to exe.. – CromeX Feb 20 '15 at 14:34
0

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.

Astrum42
  • 115
  • 1
  • 11
0

If you are looking to save python plots as an interactive figure to modify and share with others like MATLAB .fig file then you can try to use the following code. Here z_data.values is just a numpy ndarray and so you can use the same code to plot and save your own data. No need of using pandas then.

The file generated here can be opened and interactively modified by anyone with or without python just by clicking on it and opening in browsers like Chrome/Firefox/Edge etc.

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")
Sumit
  • 51
  • 2