ipympl icon indicating copy to clipboard operation
ipympl copied to clipboard

What prevents a figure showing before notebook cell completion?

Open thomasaarholt opened this issue 4 years ago • 14 comments

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)
Figure only showing at end of sleep/cell

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)
Dynamic plotting if using two cells

Edit: I also note that in the qt backend, there figure and plot are shown immediately, in contrast to the ipympl behaviour.

thomasaarholt avatar Jan 06 '21 11:01 thomasaarholt

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

thomasaarholt avatar Jan 06 '21 13:01 thomasaarholt

My understanding of this is that:

  1. Running python code blocks the processing of comm messages
  2. 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)
  3. 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).

ianhi avatar Jan 06 '21 15:01 ianhi

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:

  1. Make a plot
  2. Save the widget state in the notebook
  3. Close the notebook + kill kernel
  4. reopen the notebook

then the plots will show up like this: image

which I think is the javascript waiting on a draw message.

ianhi avatar Jan 27 '21 19:01 ianhi

More information on the order of comms processing on gitter here: https://gitter.im/jupyter-widgets/Lobby?at=6011c15255359c58bf048c31

ianhi avatar Jan 27 '21 19:01 ianhi

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

ianhi avatar Sep 22 '21 18:09 ianhi

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)

martinRenou avatar Sep 23 '21 07:09 martinRenou

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.

ianhi avatar Sep 23 '21 15:09 ianhi

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)

ianhi avatar Feb 03 '22 17:02 ianhi

Is there a possibility to make that the default behavior? and is there a reason not to?

shaielc avatar Feb 23 '22 08:02 shaielc

@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.

ianhi avatar Mar 01 '22 01:03 ianhi

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.

ghost avatar Dec 01 '23 21:12 ghost