2

Introduction and examples.

It is a widely known best practice to close matplotlib figures after opening them to avoid consuming too much memory. In a standalone Python script, for example, I might do something like this:

fig, ax = plt.subplots()
ax.plot(x, y1);
plt.show() # stop here and wait for user to finish
plt.close(fig)

Similarly, for a Jupyter notebook it's a common pattern to create a figure in one cell and then close it in the next cell. A minimal example might look something like this, where each blank line is a new cell:

%matplotlib notebook

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)

y1 = np.sin(x)

fig, ax = plt.subplots()
ax.plot(x, y1);

plt.close(fig)

However, this has a distinct disadvantage: the figure is blank when run with "Kernel" -> "Restart & Run All" or "Cell" -> "Run All". That is, the figure doesn't display until all cells finish evaluating, but the figure doesn't have time to render since we are using plt.close right afterward. Here's an example screenshot based on the example above:

shows blank figure

This is different from running each cell individually: as long as I run each cell slowly enough, I can get each figure to display:

shows figure rendered and visible

Full Jupyter notebook .ipynb files are available here:

https://github.com/nbeaver/jupyter-figure-rendering-tests

Inadequate workarounds.

The simplest workaround is start from the top and manually step through each cell, wait for it to render, and then move onto the next cell. I don't consider this an acceptable workaround, as it becomes impractically laborious for large notebooks and is generally contrary to the purpose of an executable notebook environment.

Another workaround is to simply never call plt.close(fig). This is not a good option for several reasons:

  1. It results in excess memory usage, as evidenced by warnings about opening too many figures. On some machines or resource-constrained environments this may prevent the notebook execution from completing at all.

  2. Even in environments with abundant memory, cursor tracking and interactive functionality like zoom or pan becomes very slow when many figures are open at once.

  3. As mentioned above, in general none of the figures will display until the final cell has finished executing, which is undesirable for notebooks where execution time may be long for certain cells further down.

Another workaround is to use %matplotlib inline instead of %matplotlib notebook. This is unsatisfactory also:

  1. The inline mode does not permit interactive inspection of the figure such as cursor position values or pan and zoom. This functionality may be desirable or essential for analysis.

  2. The inline and notebook settings cannot in general be toggled on a per-cell basis, so effectively this is an all-or-nothing setting.

  3. Even if it were possible to toggle between inline and notebook, I would prefer to only use notebook and then close the figure in a subsequent cell, so that I can return to the cell later and re-run it to get the interactive controls without needing to edit the cell.

An analogous workaround is to call savefig for each cell with a figure and then browse the generated images with an external image viewing program. While this allows limited zooming and panning, it doesn't give cursor positions and it's really not comparable to the interactive notebook plots.

Criteria and current workaround.

Here are my requirements:

  • The effect of "Restart & Run All" must render all figures eventually; no figures can be left blank.

  • Use %matplotlib notebook for all cells so that I can re-run the cell later and inspect the figure interactively.

  • Allow plt.close(fig) after each cell so that notebook resources can be conserved.

Essentially, I would like a way to force the kernel to render the current figure before proceeding on to plt.close(fig). My hope is that this is a well-known behavior and that I've simply missed something. Here's what I have tried so far that didn't help at all:

  • plt.show() at the end of a cell or between cells.

  • fig.show() at the end of a cell or between cells.

  • plt.ioff() at the end of a cell or between cells.

  • time.sleep(1) at the end of a cell or between cells.

  • plt.pause(1) at the end of a cell or between cells.

  • fig.canvas.draw_idle() at the end of a cell or between cells.

  • Doing from IPython.display import display and then display(fig) at the end of a cell or between cells.

  • calling plt.close('all') at end of notebook instead of between each cell.

Currently, the best I've been able to do is call fig.canvas.draw() in a separate cell between the figure and the cell with plt.close(fig). Using the example above, here's what this looks like:

%matplotlib notebook

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)

y1 = np.sin(x)

fig, ax = plt.subplots()
ax.plot(x, y1);

fig.canvas.draw()

plt.close(fig)

This works reliably in smaller notebooks, and for a while I thought this had solved the problem. However, this doesn't always work; particularly in large notebooks with many figures, some of them still come out blank, suggesting that fig.canvas.draw() adds some delay but is not always sufficient, perhaps due to a race condition. Since fig.canvas.draw() is not a documented method for making Jupyter notebooks render the current figure, I would hesitate to describe this as a bug, although it seems to be the closely related to this matplotlib issue, which ultimately seems to be a Jupyter bug:

The simplest work around may be to put the input() call in the next cell or to add plt.gcf().canvas.draw() above the input call. This will still result in "dead" figures (which may not have caught up to the hi-dpi settings of your browser), but they will at least show.

I've observed this behavior in many combinations of matplotlib and Jupyter, including matplotlib version 2.1.1 and 3.5.1 and Jupyter version 4.4.0 and 6.4.8. I've also observed it in both Google Chrome 99.0.4844.51 and Firefox 97.0.2 and on both Windows 10 and Ubuntu 18.04.6.

Related questions (not duplicates):

  • Can you just run `plt.close()` *before* constructing the *next* figure? you could even do, `def close_open(*args, **kwargs): plt.close('all'); return plt.subplots(*args, **kwargs)` – DilithiumMatrix Mar 11 '22 at 16:09
  • @DilithiumMatrix Apologies for not being clear, that is currently what I am doing. I just put the most minimal example in this post, so there's only one figure, but you can see two figures separated by `plt.close()` in the example Jupyter notebooks: [#1](https://github.com/nbeaver/jupyter-figure-rendering-tests/blob/main/example.ipynb) [#2](https://github.com/nbeaver/jupyter-figure-rendering-tests/blob/main/example-canvas-draw.ipynb) – Nathaniel M. Beaver Mar 17 '22 at 23:51

0 Answers0