vega / altair

Declarative visualization library for Python
https://altair-viz.github.io/
BSD 3-Clause "New" or "Revised" License
9.41k stars 795 forks source link

Altair's Behavior with ipywidgets Output widget #2107

Open chapmanbe opened 4 years ago

chapmanbe commented 4 years ago

I am having an issue with Altair in Jupyterlab that I was hoping people could provide some help on. I confess that I have little understanding of JavaScript and the overall construction of the ecosystems. I have created several notebooks that provide interactive interrogation of a data set. Based on web searches, in order to facilitate the interaction I use the following code to display the Altair chart:

@em_plots_out1.capture(clear_output=True)
def draw_plot(c):

    with io.StringIO() as f:
        c.save(f, format="html")
        f.seek(0)
        html1 = f.read()
    display(HTML(html1))

Where em_plots_out is an instance of ipywidgets.Output

I have several notebooks that use similar code. However, I cannot have the notebooks open at the same time. If I open a new notebook, I see an error message displayed below each widget: Error displaying widget: model not found and the JavaScript console shows errors of the following sort: renderer.js:45 Error: widget model not found at k.get_model (manager.js:375) at h.renderModel (renderer.js:38)

If I close all other notebooks with similar code and shutdown all the kernels, then opening any individual notebook and restarting the kernel works fine. If I open a second notebook, then the first notebook will start using the Output widget for the second notebook. I have seen the output from code in one notebook being drawn in second notebook even when there is no Python kernel for the second notebook.

I have also tried putting the widgets in the same notebook. Similar behavior. When I've only run the cell with the first output widget defined, everything seems to work fine, although the JavaScript console shows this error:

VM64094:17 Uncaught ReferenceError: vegaEmbed is not defined at <anonymous>:17:8 at t.attachWidget (panellayout.js:215) at t.insertWidget (panellayout.js:118) at k._insertOutput (widget.js:392) at k.onModelChanged (widget.js:216) at m (index.js:478) at Object.c [as emit] (index.js:435) at e.emit (index.js:108) at f._onListChanged (model.js:231) at m (index.js:478) (anonymous) @ VM64094:17 t.attachWidget @ panellayout.js:215 t.insertWidget @ panellayout.js:118 _insertOutput @ widget.js:392 onModelChanged @ widget.js:216 m @ index.js:478 c @ index.js:435 e.emit @ index.js:108 _onListChanged @ model.js:231 m @ index.js:478 c @ index.js:435 e.emit @ index.js:108 push @ observablelist.js:135 _add @ model.js:207 add @ model.js:128 add @ output.js:62 _msgHook @ output.js:17 process @ future.js:338 async function (async) process @ future.js:321 _handleIOPub @ future.js:212 handleMsg @ future.js:186 _handleMessage @ default.js:1087 (anonymous) @ default.js:94 Promise.then (async) _onWSMessage @ default.js:91 VM64095 vega-embed@6:26 Uncaught TypeError: Cannot read property 'version' of undefined at VM64095 vega-embed@6:26 at VM64095 vega-embed@6:1 at VM64095 vega-embed@6:1

When I run the code with the object (an ipywidget Box containing multiple widgets) containing the second Output widget, the second object "takes control" of the first Output widget and draws all its output in the first output. Both objects work, fine they just both use the Output that was activated first.

This seems to be Altair specific because each object/widget.Box contains Output widgets for displaying Pandas DataFrames slices, both of which work fine.

Environment: This behavior has been seen on two different systems.

First system:

jupyterlab=='2.1.0' ipywidgets=='7.5.1' altair=='4.1.0' @jupyter-widgets/jupyterlab-manager v2.0.0 enabled OK

Second system:

jupyterlab=='1.2.5' ipywidgets=='7.5.1' altair=='4.1.0' @jupyter-widgets/jupyterlab-manager v2.0.0 enabled OK @jupyter-widgets/jupyterlab-manager v1.1.0 enabled OK

Thanks,

Brian

jakevdp commented 4 years ago

I believe that altair rendering in ipywidgets only works with the mimetype renderer; see https://altair-viz.github.io/user_guide/display_frontends.html#displaying-in-jupyterlab

chapmanbe commented 4 years ago

Thanks. That fixes the issue with jupyterlab 2.1.0. I'm currently failing in getting the vega5 extension to install on my jupyterlab 1.25 system. Wish I could just update it to 2.10!

Brian

andyljones commented 4 years ago

EDIT: Better, HTML-renderer-using solution below.

For anyone else who Googles this up: in Notebook, the trick is to

All together,

jupyter nbextension install vega --py --sys-prefix
jupyter nbextension enable vega --py --sys-prefix
pip install altair_saver
import altair as alt
import ipywidgets as widgets
from vega_datasets import data
alt.renderers.enable('notebook', embed_options={'actions': False})

cars = data.cars()

children = [widgets.Output() for _ in range(3)]
for out in children:
    with out:
        c = alt.Chart(cars).mark_point().encode(
            x='Horsepower',
            y='Miles_per_Gallon',
            color='Origin')
        c.display()

widgets.HBox(children)

image

Interactivity is a bit laggy, seemingly because of the ye olde notebook renderer as opposed to the modern, default html one.

andyljones commented 4 years ago

Actually, turns out the HTML renderer works too! This comment put me onto the solution: you need to display the widget before .display()ing the plot:

import altair as alt
import ipywidgets as widgets
from vega_datasets import data
from IPython.display import display

cars = data.cars()

children = [widgets.Output() for _ in range(3)]
box = widgets.HBox(children)
display(box)

for out in children:
    with out:
        c = alt.Chart(cars).mark_point().encode(
            x='Horsepower',
            y='Miles_per_Gallon',
            color='Origin').interactive()
        c.display()

Bit prescriptive but hey, it works.

sbrugman commented 4 years ago

@andyljones That last snippet was really helpful. I almost ended up with manually taking the javascript out in something like:

from IPython.display import HTML, display
from IPython.utils import io

def chart_to_widget(chart):
    with io.capture_output(display=True) as captured:
        chart.display()

    html = captured.outputs[0].data['text/html']
    html, js = html.strip().split("\n", maxsplit=1)    
    return widgets.HTML(html), js

chart = plot_example('blue')
h1, js1 = chart_to_widget(chart)

chart = plot_example('green')
h2, js2 = chart_to_widget(chart)

tab = widgets.Tab()
tab.children = [h1, h2]
tab.set_title(0, 'figure 1')
tab.set_title(1, 'figure 2')
display(tab)

display(HTML(js1 + js2))

@jakevdp Do you think is useful adding @andyljones' snippet to the docs the help users of ipywidgets work with altair?