Blit-ready Jupyter notebook backend based on webagg?

For a long time I have been struggling to get blitting to work properly inside a jupyter notebook so EOmaps can be used nicely…

The 2 main problems I had so far with ipympl (e.g. %matplotlib widget) are:

Now, webagg does not suffer from the first problem, and the second problem can be addressed in webagg via a (draft) PR I’ve submitted a while ago (Avoid event accumulation in webagg backend. by raphaelquast · Pull Request #27160 · matplotlib/matplotlib · GitHub)

So now I had a look how to use webagg inside jupyter notebooks… and it seems that there is in fact a really simple way to do and it and it works just fine out of the box!

Here’s a monkey-patch example of what I have in mind:

import matplotlib.pyplot as plt
from ipywidgets import HTML, Layout
from contextlib import redirect_stdout
import io

plt.rcParams["webagg.open_in_browser"] = False
plt.switch_backend("webagg")

def show_widget(self):
    if not hasattr(self, "_jupyter_widget"):
        with io.StringIO() as tmp, redirect_stdout(tmp):    
            # Trigger webagg figure creation
            plt.show()
            # Obtain the adddress of the figure
            webagg_address = tmp.getvalue().removeprefix("To view figure, visit ").strip()
    
        self._jupyter_widget = HTML(
            f"<iframe src={webagg_address}/{self.number} "
            "style='width: 95%; height: 95%; border:none;' scrolling='no' allowtransparency='true'></iframe>"
        )
    
        # Add a callback to dynamically adjust the widget layout to fit the size of the figure
        def cb(event):
            self._jupyter_widget.layout.width = f"{1.1 * event.width:.0f}px"
            self._jupyter_widget.layout.height = f"{(1.1 * event.height + 100):.0f}px"
        self.canvas.mpl_connect("resize_event", cb)
    return self._jupyter_widget

plt.Figure.show_widget = show_widget

With this, running something like

f, ax = plt.subplots()
ax.plot([1,2,3])
f.show_widget()

in a jupyter notebook will put a nice webagg based widget into the notebook :partying_face:

… and with EOmaps we get a nice lag-free performance inside jupyter notebooks!
eomaps_webagg_jupyter


Now there are a few things that are ugly in the above example and that I can’t figure out a better solution so far:

  1. The only way I could imagine to figure out the actual port used by webagg was to capture the stdout of plt.show() … this is of course awful… Is there a nice clean way to do this?

    • Side-note: plt.rcParams["webagg.port"] is only used as a starting-point… if the port is not accessible, a random-port algorithm kicks in and another port is used… as far as I see the resulting port number is not accessible from the figure-instance…
  2. The size of the figure is not the size required to fit the whole “widget”… There’s a header and the toolbar etc. … Is there a way to get the actual size required to embed the whole figure?

    • (right now 10% extra space (+100px for the toolbar) is used which is again ugly…)
  3. Is there an easy-way to only add a few lines of css to the single-figure page ?
    (in my opinion they might be nice as default in webagg as well…)

    those lines to be precise...
    • hide the titlebar
    • make the toolbar only visible on hover
    • make the body transparent (so the jupyter dark-theme works as well)
     div.ui-dialog-titlebar {
     	display:none;
       }
    
     div.mpl-toolbar {
     	transition:0.5s opacity;
     	opacity: 0.0;
     	max-height: 10px;
     	}
    
     body:hover div.mpl-toolbar {
     	transition: 1s opacity;
     	opacity: 1.0;
     	}
     	
     body {
     	background: none transparent;
     }
    

Any help / comments / suggestions on the approach would be highly appreciated!
Personally I am excited that this finally seems to do the job really well out of the box.