Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add offline support to JupyterChart and "jupyter" renderer #3305

Merged
merged 7 commits into from
Jan 6, 2024

Conversation

jonmmease
Copy link
Contributor

This PR adds optional offline support for JupyterChart and the "jupyter" renderer.

How to enable offline support

Offline support is enabled on JupyterChart directly by calling the JupyterChart.enable_offline() class method.

alt.JupyterChart.enable_offline()

Offline support is enabled for the "jupyter" renderer by activating the renderer with the offline kwarg:

alt.renderers.enable("jupyter", offline=True)

How it works

In order to support offline usage, the AnyWidget documentation describes how to create JS bundles using bundlers like esbuild: https://anywidget.dev/en/bundling/#bundler-guides. This is the approach I used in the very first JupyterChart PR (#3108).

There are two main downsides of this approach:

  • It complicates the development setup by requiring an additional build tool and build step
  • It increases the Altair package size by close to 1MB.

Because of these downsides, we opted to start with an online-only version in #3119.

When vl-convert added support for HTML export, it added a javascript_bundle function that performs bundling (using deno_emit). Unlike a regular bundler that supports all JavaScript packages and downloads them from the internet during bundling, vl-convert's bundler only supports a couple of packages (vegaEmbed and lodashDebounce in particular) but these packages are vendored in the vl-convert executable so no internet connection is required to perform bundling.

The JupyterChart.enable_offline method calls vl-convert's javascript_bundle function to generate a bundle for the widget code that includes all dependencies, including the correct version of Vega-Lite. This takes around 1 second to run on my machine, which I think is well worth it to overcome both of the downsides above.

Docs

I added documentation in both the JupyterChart and "jupyter" renderer sections

Copy link
Contributor

@binste binste left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Tested locally. Thank you @jonmmease, another great feature :) Looking forward to releasing this into the wild.

@joelostblom
Copy link
Contributor

This looks really neat, thank you @jonmmease ! I was initially going to suggest that maybe offline=True could be the default for the jupyter renderer, but I realize that then we would need to use vl-convert as a required package instead of an optional one, so I guess it is safer to leave it as is.

@mattijn
Copy link
Contributor

mattijn commented Jan 6, 2024

This:

alt.JupyterChart.enable_offline()

Works on a single chart object? Or will this render all JupyterCharts offline from the moment this line is called?

In case of the first, could it be an argument instead of a method call?

alt.JupyterChart(spec, offline=True)

@mattijn
Copy link
Contributor

mattijn commented Jan 6, 2024

Oh I see, it's written in the docs. When called, it renders all charts offline.

For my understanding, does this work globally on a JupyterLab session level? Or is this JavaScript bundle injected in each chart object?

Like, if a notebook has 50 charts, it will inject this bundle 50 times or one time?

@jonmmease
Copy link
Contributor Author

For my understanding, does this work globally on a JupyterLab session level? Or is this JavaScript bundle injected in each chart object?

This is a good question. My assumption is that this would inject the bundle for each displayed widget, but I haven't been able to get JupyterLab to actually save the widget state to the resulting notebook file, so I haven't been able to inspect how this works. Even when I check "Settings -> Save Widget State Automatically" I'm not seeing the widget state saved to the ipynb file.

@manzt, if you have a moment, could you comment on whether the JavaScript bundle associated with an AnyWidget is injected/saved once per widget instance, or once per session? Thanks!

@manzt
Copy link
Contributor

manzt commented Jan 6, 2024

The widget ESM for anywidget is injected once per widget instance, not per session. Therefore, if there are multiple views of the same widget instance, there's only one ESM, but separate instances mean separate injections.

but I haven't been able to get JupyterLab to actually save the widget state to the resulting notebook file

Not seeing the widget state saved is due to how ipywidgets handles embedding, by default omitting the "default" trait values (which _esm is treated as).

One option would be to publish the final ESM bundle to a CDN or have a HTML-bloat-friendly version of the ESM, enabled by a flag. It's really tricky to implement depulication of ESM for anywidget, since it would require anywidget keep track of a registry of ESM and be aware of widgets removed and added. I've avoided implementing for simplicity.

Open to discussing further solutions.

@jonmmease
Copy link
Contributor Author

Thanks for the context and clarifications @manzt.

@mattijn, so as things stand right now, widgets based on AnyWidget don't have their source saved in the notebook file. This means that these charts won't appear when opening a notebook until they are re-run. But also means that this PR doesn't make the situation worse in terms of notebook file size.

I'm going to go ahead and merge. Thanks for the reviews all.

@jonmmease jonmmease merged commit 1115731 into main Jan 6, 2024
20 checks passed
@mattijn
Copy link
Contributor

mattijn commented Jan 6, 2024

Thanks for the clarification!

@manzt
Copy link
Contributor

manzt commented Feb 8, 2024

I just had an idea on this front and think I have a way to de-doupe _esm in the front end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants