ipympl
ipympl copied to clipboard
What prevents a figure showing before notebook cell completion?
My issue boils down to:
"Why does the figure not show immediately, but instead waits until the cell has completed running?"
%matplotlib widget
import matplotlib.pyplot as plt
from time import sleep
fig = plt.figure()
plt.plot([1,2,3])
sleep(2)
This behaviour is preventing fun uses, like dynamic plotting in a single cell:
%matplotlib widget
from matplotlib import pyplot as plt
from time import sleep
import numpy as np
fig = plt.figure()
for i in range(10):
x, y = np.random.random(2)
plt.scatter(x, y)
fig.canvas.draw()
sleep(0.1)
The previous code works if one splits it into two cells, placing the figure() call in the first one:
# Cell 1
%matplotlib widget
from matplotlib import pyplot as plt
from time import sleep
import numpy as np
fig = plt.figure()
# Cell 2
for i in range(10):
x, y = np.random.random(2)
plt.scatter(x, y)
fig.canvas.draw()
sleep(0.1)
Edit: I also note that in the qt backend, there figure and plot are shown immediately, in contrast to the ipympl behaviour.
For anyone who is looking for a temporary solution, I'll add that for my project, where I am generating figures to be able to keep track of whether an image correction algorithm is proceeding in the right direction, I'm currently creating a figure in the notebook, and then getting hold of that in my code library with plt.gcf(), so the general workflow is:
# Cell 1
plt.figure(figsize=some_size_for_a_nice_aspect_ratio)
# Cell 2
while not_converged:
# (...)
fig = plt.gcf()
fig.clear()
ax = fig.add_subplot()
ax.plot(...)
My understanding of this is that:
- Running python code blocks the processing of comm messages
- At least one incoming comm message needs to be processed by the python side in order for the js side to finish initialization (waits for a draw message)
- ergo python execution must finish to allow the comms and the backend to reply to the frontend.
In particular the issue is due to the dpi handling that's done in the initialization method on js side: https://github.com/matplotlib/ipympl/blob/ac0a7c332a9cede1994516a954cc589402454222/js/src/mpl_widget.js#L72-L81
A "fix" for this is to just pretend that the frontend sent the messages early by adding these lines
canvas._handle_message(canvas, {'type': 'send_image_mode'}, [])
canvas._handle_message(canvas, {'type':'refresh'}, [])
canvas._handle_message(canvas,{'type': 'initialized'},[])
canvas._handle_message(canvas,{'type': 'draw'},[])
to https://github.com/matplotlib/ipympl/blob/ac0a7c332a9cede1994516a954cc589402454222/ipympl/backend_nbagg.py#L281-L288
just after the manager is defined. This results in a figure that appears and updates immediately, but may have the incorrect DPI ratio.
That set that set of handle_message that I force is pretty much when the frontend asks for at the beginning of it's init. So the thought is that even though the figure isn't ready to be initialized yet you can just plop the messages on to the queue of outgoing comms bc they're garunteed (i think) to arrive after the messages that create the frontend object. (serious tbd on this, would need to ask someone more knowledgeable about the jupyter comms).
Fixing this may be more critical than I thought at first. I think that resolving this would also go a good ways towards fixing the issue of plots persisting in closed notebooks. Currently if you:
- Make a plot
- Save the widget state in the notebook
- Close the notebook + kill kernel
- reopen the notebook
then the plots will show up like this:

which I think is the javascript waiting on a draw message.
More information on the order of comms processing on gitter here: https://gitter.im/jupyter-widgets/Lobby?at=6011c15255359c58bf048c31
I wonder if we could us https://github.com/Kirill888/jupyter-ui-poll#jupyter-ui-poll and maybe eventually use https://github.com/davidbrochart/akernel
Also this is the same as https://github.com/matplotlib/matplotlib/issues/18596 and #258
I tried the approach you suggested with
canvas._handle_message(canvas, {'type': 'send_image_mode'}, [])
canvas._handle_message(canvas, {'type':'refresh'}, [])
canvas._handle_message(canvas,{'type': 'initialized'},[])
canvas._handle_message(canvas,{'type': 'draw'},[])
And it seems to work fine. The first animation won't have the correct DPI, but the next ones will have the right one (because we now save the DPI as a static Canvas property)
And it seems to work fine. The first animation won't have the correct DPI, but the next ones will have the right one (because we now save the DPI as a static Canvas property)
I think guessing a DPI of 1 is a reasonable strategy? and then once the communication of the true DPI happens we can immediately redraw in the case that the DPI is different. Although faking the messages like this may not be strictly optimal instead of just calling the underlying methods.
A workaround that works with the current version is:
%matplotlib widget
from matplotlib import pyplot as plt
from time import sleep
import numpy as np
def display_immediately(fig):
canvas = fig.canvas
display(canvas)
canvas._handle_message(canvas, {'type': 'send_image_mode'}, [])
canvas._handle_message(canvas, {'type':'refresh'}, [])
canvas._handle_message(canvas,{'type': 'initialized'},[])
canvas._handle_message(canvas,{'type': 'draw'},[])
with plt.ioff():
fig = plt.figure()
display_immediately(fig)
for i in range(10):
x, y = np.random.random(2)
plt.scatter(x, y)
fig.canvas.draw()
sleep(0.1)
Is there a possibility to make that the default behavior? and is there a reason not to?
@shaielc I think the plan is to make it the default. The reason it hasn't been done yet is that no one has had the time to implement. Though I'd happily review a PR implementing it.
First, thx for this thread, solved a longstanding problem of mine! One thing I noticed: in VSCode jupyter, the size of the plot inside the figure seems to be by a factor of 0.85 wrong, until the cell execution completes. This is insfofar annoying, as the axes are larger than the displayed figure window. This seems not to be the case in the browser. Has this something to do with the wrong dpi setting? Workaround is to scale the axis a bit smaller before the first display, and only do so if the environment is VSCode.