matplotlib / ipympl

Matplotlib Jupyter Integration
https://matplotlib.org/ipympl/
BSD 3-Clause "New" or "Revised" License
1.59k stars 225 forks source link

Need to create and update figures in seperate cells? #425

Open henrypinkard opened 2 years ago

henrypinkard commented 2 years ago

Something I've seen in previous versions as well as the current version: In order to have a figure that dynamically updates while some code is executing (e.g. tracking the loss function of an optimization), I have to create the figure in a separate jupyter notebook cell and run it before a I run the code that updates. If I don't do this, then a blank figure shows and never updates. I'm using Jupyter lab. Wondering if I'm doing something wrong here? It would make the code cleaner and easier to run if it could all be put in a single cell.

Example:

Using the following function:

def update_function(fig, ax):
   while True:
       # Do some computation
       ax.clear() #clear previously drawn
       ax.plot(some_data)
       fig.canvas.draw() #force it to update

This works:

# cell 1
fig, ax = plt.subplots()
# cell 2
update_function(fig, ax)

But this does not:

# one combined cell
fig, ax = plt.subplots()
update_function(fig, ax)
ianhi commented 2 years ago

Hi @henrypinkard this is unfortunately a known issue. There is some discussion of it and potential solutions here https://github.com/matplotlib/ipympl/issues/290

ianhi commented 2 years ago

ahh and it turns out that there is a pretty simple workaround for the meantime

%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)
henrypinkard commented 2 years ago

Got it, thanks. I'll give that a shot.

Awesome library overall!!

ianhi commented 2 years ago

as an aside I would recommend instead of clearing and redrawing every time you should use the set_data methods. e.g

line = ax.plot(some_data)[0]

# for updating
line.set_data(new_data)
fig.canvas.draw()
henrypinkard commented 2 years ago

Yeah I think I saw that way in the example, but I opted for clear() because I'm not always calling plot. Sometimes imshow, scatter, bar, etc. As far as I can tell, the objects returned don't always have a set_data method. Thoughts?

ianhi commented 2 years ago

Yeah I think I saw that way in the example, but I opted for clear() because I'm not always calling plot. Sometimes imshow, scatter, bar, etc. As far as I can tell, the objects returned don't always have a set_data method. Thoughts?

Many (but not all artists) do have set_data or equivalent, though they are not always documented and there is no central example (https://github.com/matplotlib/matplotlib/issues/19520).

In my mpl-interactions pcakage I've done a lot of the work of figuring that out and if you look through this file https://github.com/ianhi/mpl-interactions/blob/master/mpl_interactions/pyplot.py then you can find the techniques for updating various functions. There's a bit of extra stuff around the framework for buliding sliders (and also because I make every argument adjustable) but for scatter for example looking at https://github.com/ianhi/mpl-interactions/blob/0517177b687722f8d4ecf9fd89910986be3377b3/mpl_interactions/pyplot.py#L530 you can see that the steps are:

scatter = ax.scatter(x, y)

scatter.set_offsets(np.column_stack([new_x, new_y]))

and then some trickiness with a helper function for updating the axis limits automatically (though there are simpler ways)

Actually I'll bet that it wouldn't be too difficult to use mpl-interactions as an engine for updating plots in cases like this. Just would need to make a widget that doesn't get rendered anywhere and update it's value. But that's probably best discussed not on this issue

ianhi commented 2 years ago

Complete example of using mpl-interactions to auto update plots in a loop all in one cell can be found here: https://github.com/ianhi/mpl-interactions/issues/234