matplotlib / ipympl

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

Plot output with ipywidgets formatting incorrectly #436

Open drs378 opened 2 years ago

drs378 commented 2 years ago

Describe the issue

I built a small GUI within a notebook and had two figures in an ipywidgets HBox object. the latest version of ipympl made it so I could not have both figures in an HBox and generally disrupted the formatting of the object. I was able to fix the issue by reverting both matplotlib and the ipympl versions.

a short script that generates the broken output is as follows

%matplotlib widget
from IPython.display import display, HTML
display(HTML("<style>.container { width:100%!important;}</style>"))

import ipywidgets as ipy
import matplotlib.pyplot as plt
import numpy as np
out1 = ipy.Output()
b1 = ipy.Button(description = 'button1')
b2 = ipy.Button(description = 'button2')
x = np.linspace(0,7,21)
y = np.sin(x)
with out1:
    fig,ax = plt.subplots()
    ax.plot(x,y)

display(ipy.HBox([out1,ipy.VBox([b1,b2])]))

Versions

(working version)

ipympl == 0.5.8
matplotlib == 3.3.3

(broken version)

matplotlib == 3.5.1
mklingn commented 2 years ago

I am also seeing this behaviour with ipympl 0.9.2 and matplotlib 3.6.2.: The plot does not get put into the Output widget.

This breaks the top search result for "ipywidgets plot": https://kapernikov.com/ipywidgets-with-matplotlib/

@drs378 : Did you check 0.5.8 was the last version that worked or did you just happen to have it at hand?

ekimd commented 1 year ago

Just want to confirm the same problem for me with ipympl 0.9.2 and matplotlib 3.6.2

ianhi commented 1 year ago

I take that the issue is the ordering of the plot relative to the buttons?

This behavior changed slightly as part of https://github.com/matplotlib/ipympl/pull/343. While unfortunate that this changed it was necessary as part of fixing the longstanding and important issue of plot. The core of what's happening here is that the plot is being displayed before the output widget has an opportunity to capture it. (sidenote @martinRenou is that actually expected, it would maybe be nice if output widgets captured the plots).

Fortunately there is a workaround - which is to be more explicit about what you are doing. You can read more about it in the docs here: https://matplotlib.org/ipympl/examples/full-example.html#interactions-with-other-widgets-and-layouting

But the short of it is:

  1. Wrap the plt.subplots call in an ioff block
  2. Don't capture with an output block
  3. instead embed fig.canvas into the widget layout.

Combining those, you should make the final lines like so:

with plt.ioff():
    fig, ax = plt.subplots()
ax.plot(x,y)

display(ipy.HBox([fig.canvas,ipy.VBox([b1,b2])]))
ekimd commented 1 year ago

Thanks, @ianhi ! That works as expected. I have a separate but related question. I used to be able to create a pop-out browser window of a figure via the following code, but now it doesn't work anymore and I'm guessing a similar small change is necessary with the final two output lines. Thanks in advance.

    def _popout_fig(fig, name="name"):
        buf = BytesIO()
        fig.savefig(buf, format="png")
        fig_size_pix = fig.get_size_inches() * fig.dpi
        s = '<script type="text/Javascript">'
        s += f'var win = window.open("", "{name}", "toolbar=no, location=no, status=no, menubar=no, '\
             f'scrollbars=yes, resizable=yes, width={1.12*fig_size_pix[0]}, height={1.12*fig_size_pix[1]}, '\
             'top="+(screen.height-400)+", left="+(screen.width-840));'  # This part sets where it shows on screen
        s += f'win.document.title = "{name}";'
        s += 'win.document.body.innerHTML = \''
        img = base64.b64encode(buf.getbuffer()).decode('ascii')
        s += f'<img src="data:image/png;base64,{img}"/>'
        s += '\';'
        s += '</script>'
        with widgets.Output():
            IPython.display.display(IPython.display.HTML(s))  # noqa
ekimd commented 1 year ago

Never mind, I solved it. I still need a widgets.Output object that's a child of the original displayed widget, and then use it in place of the with widgets.Output(): above.

All my concerns are now resolved.