rgerum / pylustrator

Visualisations of data are at the core of every publication of scientific research results. They have to be as clear as possible to facilitate the communication of research. As data can have different formats and shapes, the visualisations often have to be adapted to reflect the data as well as possible. We developed Pylustrator, an interface to directly edit python generated matplotlib graphs to finalize them for publication. Therefore, subplots can be resized and dragged around by the mouse, text and annotations can be added. The changes can be saved to the initial plot file as python code.
GNU General Public License v3.0
706 stars 38 forks source link

Convert an existing matplotlib figure to pylustrator #25

Closed remiberthoz closed 4 years ago

remiberthoz commented 4 years ago

Hello! Pylustrator is an awesome tool, thanks!

I would like to ask whether you would agree to add a function that converts an existing matplotlib Figure to a figure editable with the Pylustrator GUI.

My use case is the following:

I want to separate data analysis and figure beautification as much as possible, yet keep the figure creation near the analysis for debugging.

src/
    analysis/             #     interactively generate simple
        project1.ipynb    # <-- matplotlib axes for debugging          
paper/
    figure1.py            # <-- beautify the axes and assemble
    document.tex          #     these into an illustration

Data would be analysed in project1.ipynb, and very basic debugging plots would be created there with matplotlib. This analysis notebook should be independent of the way my illustration will look in the paper, and does not need Pylustrator.

When wrapping up the paper, I would want to beautify the axes generated by project1.ipynb and gather these into an illustration using Pylustrator in the figure1.py script. I can already use pickle to dump a matplotlib figure in the notebook, load the dump in the script, and manually adjust the figure. Ideally, I would like to edit the figure from with the Pylustrator GUI. To do that, I need to convert the matplotlib figure into a Pylustrator one.

I know you already support load('project1.ipynb') for a similar use case. But if the computations for analysis are long, it would be nice to separate both aspects. When the notebook has to be executed on a headless high performance computing cluster, pylustrator cannot be used at all.

Thanks again!


I tested a basic implementation of a convertFromPyplot function, copying what you have in loadFromFile(cache = True):

# helper_functions.py
def convertFromPyplot(old, new):

    w, h = old.get_size_inches()
    new.set_size_inches(w, h)

    str(new)  # important! (for some reason I don't know)
    for ax in old.axes:
       old.delaxes(ax)
        new._axstack.add(new._make_key(ax), ax)
        new.bbox._parents.update(old.bbox._parents)
        new.dpi_scale_trans._parents.update(old.dpi_scale_trans._parents)
        replace_all_refs(old.bbox, new.bbox)
        replace_all_refs(old.dpi_scale_trans, new.dpi_scale_trans)
        replace_all_refs(old.canvas, new.canvas)
        replace_all_refs(old, new)

and it appears to be working with this test script:

import matplotlib.pyplot as plt

fig, ax = plt.subplots(1)
ax.text(0.5, 0.5, 'Hello world!')

import pylustrator
pylustrator.start()

newfig = plt.figure()
pylustrator.convertFromPyplot(fig, newfig)
plt.show()

though I'm not sure how robust it is. If it looks good to you, I will be happy to open a pull request!

rgerum commented 4 years ago

hmm ok. So the best thing would be probably if pylustrator.start() "just" tries to grab all existing figures. Or that you can call pylustrator.start(fig) with an existing figure. I want to keep the API as small as possible.

rgerum commented 4 years ago

I added this code to pylustrator.start() to enable pylustartor for already existing figures. I hope this solves your problem!

remiberthoz commented 4 years ago

Hey, thanks for the implementation. That was fast :) !

It may have introduced a bug though... If I run:

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.text(0.5, 0.5, 'Hello World!')

fig, ax = plt.subplots()
ax.text(0.5, 0.5, 'Hey Earth!')

import pylustrator
pylustrator.start()

plt.show()

then I get a RuntimeError: OrderedDict mutated during iteration from:

  File "./pylustrator/pylustrator/QtGuiDrag.py", line 93, in initialize
    for fig_number in _pylab_helpers.Gcf.figs:

I can solve it using: for fig_number in _pylab_helpers.Gcf.figs.copy() instead ; then the same errors appears from line 111 and the same fix works. I'm not sure if this has side-effects though.

I'll test it more today and report back!

rgerum commented 4 years ago

Thank you for the test and suggestion to improve. I implemented your fix.