diff --git a/altair/jupyter/js/index.js b/altair/jupyter/js/index.js index 779514f86..57e8aa800 100644 --- a/altair/jupyter/js/index.js +++ b/altair/jupyter/js/index.js @@ -1,6 +1,11 @@ import vegaEmbed from "https://esm.sh/vega-embed@6?deps=vega@5&deps=vega-lite@5.16.3"; import lodashDebounce from "https://esm.sh/lodash-es@4.17.21/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; diff --git a/altair/jupyter/jupyter_chart.py b/altair/jupyter/jupyter_chart.py index cb5b8e0f6..0331c9820 100644 --- a/altair/jupyter/jupyter_chart.py +++ b/altair/jupyter/jupyter_chart.py @@ -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 */ @@ -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, diff --git a/altair/vegalite/v5/display.py b/altair/vegalite/v5/display.py index 0b0a5b66a..e3733adcf 100644 --- a/altair/vegalite/v5/display.py +++ b/altair/vegalite/v5/display.py @@ -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_ diff --git a/doc/releases/changes.rst b/doc/releases/changes.rst index 432975514..d065b28bf 100644 --- a/doc/releases/changes.rst +++ b/doc/releases/changes.rst @@ -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 ` which have support for Altair (#3299) - Support restrictive FIPS-compliant environment (#3291) diff --git a/doc/user_guide/display_frontends.rst b/doc/user_guide/display_frontends.rst index bb488986a..06c689011 100644 --- a/doc/user_guide/display_frontends.rst +++ b/doc/user_guide/display_frontends.rst @@ -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"`` diff --git a/doc/user_guide/jupyter_chart.rst b/doc/user_guide/jupyter_chart.rst index d2bcaa63a..d00327f3d 100644 --- a/doc/user_guide/jupyter_chart.rst +++ b/doc/user_guide/jupyter_chart.rst @@ -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. +.. _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.