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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions altair/jupyter/js/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import vegaEmbed from "https://esm.sh/vega-embed@6?deps=vega@5&[email protected]";
import lodashDebounce from "https://esm.sh/[email protected]/debounce";

// Note: For offline support, the import lines above are removed and the remaining script
// is bundled using vl-convert's javascript_bundle function. See the documentation of
// the javascript_bundle function for details on the available imports and their names.
// If an additional import is required in the future, it will need to be added to vl-convert
// in order to preserve offline support.
export async function render({ model, el }) {
let finalize;

Expand Down
57 changes: 56 additions & 1 deletion altair/jupyter/jupyter_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,12 @@ def _set_value(self, key, value):
self.observe(self._make_read_only, names=key)


def load_js_src() -> str:
return (_here / "js" / "index.js").read_text()


class JupyterChart(anywidget.AnyWidget):
_esm = (_here / "js" / "index.js").read_text()
_esm = load_js_src()
_css = r"""
.vega-embed {
/* Make sure action menu isn't cut off */
Expand Down Expand Up @@ -124,6 +128,57 @@ class JupyterChart(anywidget.AnyWidget):
_js_to_py_updates = traitlets.Any(allow_none=True).tag(sync=True)
_py_to_js_updates = traitlets.Any(allow_none=True).tag(sync=True)

# Track whether charts are configured for offline use
_is_offline = False

@classmethod
def enable_offline(cls, offline: bool = True):
"""
Configure JupyterChart's offline behavior

Parameters
----------
offline: bool
If True, configure JupyterChart to operate in offline mode where JavaScript
dependencies are loaded from vl-convert.
If False, configure it to operate in online mode where JavaScript dependencies
are loaded from CDN dynamically. This is the default behavior.
"""
from altair.utils._importers import import_vl_convert, vl_version_for_vl_convert

if offline:
if cls._is_offline:
# Already offline
return

vlc = import_vl_convert()

src_lines = load_js_src().split("\n")

# Remove leading lines with only whitespace, comments, or imports
while src_lines and (
len(src_lines[0].strip()) == 0
or src_lines[0].startswith("import")
or src_lines[0].startswith("//")
):
src_lines.pop(0)

src = "\n".join(src_lines)

# vl-convert's javascript_bundle function creates a self-contained JavaScript bundle
# for JavaScript snippets that import from a small set of dependencies that
# vl-convert includes. To see the available imports and their imported names, run
# import vl_convert as vlc
# help(vlc.javascript_bundle)
bundled_src = vlc.javascript_bundle(
src, vl_version=vl_version_for_vl_convert()
)
cls._esm = bundled_src
cls._is_offline = True
else:
cls._esm = load_js_src()
cls._is_offline = False

def __init__(
self,
chart: TopLevelSpec,
Expand Down
7 changes: 7 additions & 0 deletions altair/vegalite/v5/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ def jupyter_renderer(spec: dict, **metadata):
"""Render chart using the JupyterChart Jupyter Widget"""
from altair import Chart, JupyterChart

# Configure offline mode
offline = metadata.get("offline", False)

# mypy doesn't see the enable_offline class method for some reason
JupyterChart.enable_offline(offline=offline) # type: ignore[attr-defined]

# propagate embed options
embed_options = metadata.get("embed_options", None)

# Need to ignore attr-defined mypy rule because mypy doesn't see _repr_mimebundle_
Expand Down
2 changes: 2 additions & 0 deletions doc/releases/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Version 5.3.0 (unreleased month day, year)
Enhancements
~~~~~~~~~~~~
- Add "jupyter" renderer which uses JupyterChart for rendering (#3283). See :ref:`renderers` for more information.
- Add offline support for JupyterChart and the new "jupyter" renderer. See :ref:`user-guide-jupyterchart-offline`
for more information.
- Docs: Add :ref:`section on dashboards <display_dashboards>` which have support for Altair (#3299)
- Support restrictive FIPS-compliant environment (#3291)

Expand Down
5 changes: 5 additions & 0 deletions doc/user_guide/display_frontends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ The most used built-in renderers are:
object explicitly following the instructions in the :ref:`user-guide-jupyterchart`
documentation.

``alt.renderers.enable("jupyter", offline=True)``
*(added in version 5.3):* Same as the ``"jupyter"`` renderer above, but loads JavaScript
dependencies from the ``vl-convert-python`` package (rather than from an online CDN)
so that an internet connection is not required.

In addition, Altair includes the following renderers:

- ``"default"``, ``"colab"``, ``"kaggle"``, ``"zeppelin"``: identical to ``"html"``
Expand Down
30 changes: 24 additions & 6 deletions doc/user_guide/jupyter_chart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -426,15 +426,33 @@ is used to combine the chart and HTML table in a column layout.
Your browser does not support the video tag.
</video>

.. _user-guide-jupyterchart-offline:

Offline Usage
-------------
By default, the ``JupyterChart`` widget loads its JavaScript dependencies dynamically from a CDN
location, which requires an active internet connection. Starting in Altair 5.3, JupyterChart supports
loading its JavaScript dependencies from the ``vl-convert-python`` package, which enables offline usage.

Offline mode is enabled using the ``JupyterChart.enable_offline`` class method.

.. code-block:: python

import altair as alt
alt.JupyterChart.enable_offline()

This only needs to be called once, after which all displayed JupyterCharts will operate in offline mode.

Offline mode can be disabled by passing ``offline=False`` to this same method.

.. code-block:: python

import altair as alt
alt.JupyterChart.enable_offline(offline=False)

Limitations
-----------

Setting Selections
~~~~~~~~~~~~~~~~~~
It's not currently possible to set selection states from Python.

Internet Connection Required
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The JupyterChart class currently loads its JavaScript dependencies dynamically from a CDN location.
This keeps the ``altair`` package small, but it means that an internet connection is required
to display JupyterChart instances. In the future, we would like to provide optional offline support.