Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What prevents a figure showing before notebook cell completion? #290

Open
thomasaarholt opened this issue Jan 6, 2021 · 11 comments
Open

Comments

@thomasaarholt
Copy link
Contributor

thomasaarholt commented Jan 6, 2021

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
Copy link
Contributor Author

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

@ianhi
Copy link
Collaborator

ianhi commented Jan 6, 2021

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:

send_initialization_message() {
if (this.ratio != 1) {
this.send_message('set_dpi_ratio', { dpi_ratio: this.ratio });
}
this.send_message('send_image_mode');
this.send_message('refresh');
this.send_message('initialized');
}

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

canvas = Canvas(figure)
if 'nbagg.transparent' in rcParams and rcParams['nbagg.transparent']:
figure.patch.set_alpha(0)
manager = FigureManager(canvas, num)
if is_interactive():
manager.show()
figure.canvas.draw_idle()

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
Copy link
Collaborator

ianhi commented Jan 27, 2021

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
Copy link
Collaborator

ianhi commented Jan 27, 2021

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

@ianhi
Copy link
Collaborator

ianhi commented Sep 22, 2021

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 matplotlib/matplotlib#18596 and #258

@martinRenou
Copy link
Member

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)

@ianhi
Copy link
Collaborator

ianhi commented Sep 23, 2021

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
Copy link
Collaborator

ianhi commented Feb 3, 2022

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)

@shaielc
Copy link

shaielc commented Feb 23, 2022

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

@ianhi
Copy link
Collaborator

ianhi commented Mar 1, 2022

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

@ghost
Copy link

ghost commented Dec 1, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants