ipywidgets
ipywidgets copied to clipboard
interactive_output widgets for various matplotlib backends
Problem
While matplotlib is not directly related to ipywidgets, it is a major use case but also a major headache. Plots appear, but in the wrong place, without what you drew, multiple times, don't update, etc.
Furthermore, there is a distinct lack of (authorative) examples in the documentation on how this can be accomplished without aforementioned problems. So when you google, you find a bunch of people with similar problems, and always different suggestions on how to fix it. Among them are usages of:
- matplotlib.pyplot.ioff() / matplotlib.pyplot.ion()
- IPython.display.display(fig)
- IPython.display.display(fig.canvas)
- fig.canvas.draw()
- fig.canvas.flush_events()
- ipywidgets.interaction.show_inline_matplotlib_plots()
- ipykernel.pylab.backend_inline.flush_figures()
I found it tedious (a lot of trial and error) to get interactive_output working with one matplotlib backend (ipympl/widget). Worse still, it then does not work in the standard inline backend, which is even more tedious and headache-inducing to get working and there are almost endless combinations to try if you do not know how everything works behind the scenes and maybe even want it to work for multiple backends.
In the end I got the following working, and I will put it here for posterity so maybe it can benefit someone else. My conclusion is that it is not possible to have the same codepath for inline and ipympl backends, and that I have to rerun display() every time the inline figure needs to be updated. Please correct me if this is wrong.
Python package versions
- Python 3.9
- jupyterlab 3.2.8
- ipywidgets 7.6.5
- ipympl 0.8.2
- ipython 7.14.0
- matplotlib 3.5.0
- ipykernel 5.2.1
import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np
from ipykernel.pylab.backend_inline import flush_figures
ipympl backend
Relatively straight forward once you realize you have to use plt.ioff()
ipympl code
(plus `display(fig.canvas)` in the `ipywidgets.Output` context to place at the desired position in the widget setup)%matplotlib widget
out = widgets.Output()
x = np.linspace(0, 10, 200)
with plt.ioff():
fig, ax = plt.subplots()
with out:
display(fig.canvas)
lines, = ax.plot(x, x**2)
control = widgets.IntSlider(0, -10, 10)
full_widget = widgets.VBox([control, out], layout={'border': '1px solid black'})
def func(param):
lines.set_data([x, (x-param)**2])
widgets.interactive_output(func, {'param': control})
display(full_widget)
inline backend
This one is a bit more tricky. In the initial output using display(fig.canvas) or display(fig) does not work, unlike other answers I found online claimed. Instead, you have to use show_inline_matplotlib_plots()/flush_figures() here. However, in the callback function we do have to use display(fig), and not show_inline_matplotlib_plots(), while simultaneously re-capturing the output. The plt.ioff() context does not seem to be required in the initial figure creation, but it doesn't hurt either.
inline code
%matplotlib inline
out = widgets.Output()
x = np.linspace(0, 10, 200)
fig, ax = plt.subplots()
with out:
flush_figures()
lines, = ax.plot(x, x**2)
control = widgets.IntSlider(0, -10, 10)
full_widget = widgets.VBox([control, out], layout={'border': '1px solid black'})
@out.capture(clear_output=True, wait=True)
def func(param):
lines.set_data([x, (x-param)**2])
display(fig)
widgets.interactive_output(func, {'param': control})
display(full_widget)
Differences
--- ipympl
+++ inline
@@ -1,18 +1,19 @@
out = widgets.Output()
x = np.linspace(0, 10, 200)
-with plt.ioff():
- fig, ax = plt.subplots()
+fig, ax = plt.subplots()
with out:
- display(fig.canvas)
+ flush_figures()
lines, = ax.plot(x, x**2)
control = widgets.IntSlider(0, -10, 10)
full_widget = widgets.VBox([control, out], layout={'border': '1px solid black'})
[email protected](clear_output=True, wait=True)
def func(param):
lines.set_data([x, (x-param)**2])
+ display(fig)
widgets.interactive_output(func, {'param': control})
display(full_widget)
Functional example working for both backends leveraging _ipython_display_:
This was my ultimate goal, to display a rich interactive widget plot representing an object.
Code
def backend():
back_d = {'module://ipympl.backend_nbagg': 'ipympl',
'module://ipykernel.pylab.backend_inline': 'inline'}
return back_d[plt.get_backend()]
class Quadratic(object):
def _ipython_display_(self):
be = backend()
out = widgets.Output()
x = np.linspace(0, 10, 200)
with plt.ioff():
fig, ax = plt.subplots()
with out:
if be == 'ipympl':
display(fig.canvas)
elif be == 'inline':
flush_figures()
lines, = ax.plot(x, x**2)
control = widgets.IntSlider(0, -10, 10)
full_widget = widgets.VBox([control, out], layout={'border': '1px solid black'})
def func(param):
lines.set_data([x, (x-param)**2])
if be == 'inline':
out.clear_output(wait=True)
with out:
display(fig)
widgets.interactive_output(func, {'param': control})
display(full_widget)
Suggested Improvement
Provide some simple examples for common matplotlib + ipywidget use cases for different backends, so you can get started using matplotlib without having to understand in detail how ipywidget and matplotlib interact.
Thanks a lot! :D This saved my day (or week ;) )... You described my situation pretty well - plots were showing up at various different locations, were not updated but replotted elsewhere. But the above code works for me now! Really a pity that one cannot use both backends with the same code but has to differentiate between both. Also the fact that ipywidgets and matplotlib are interfering here... I just hope that this code will continue to work when the code base of either the two changes. And hopefully in future we can reach the same (intuitive) functionality without these tweaks....