Skip to content

Commit

Permalink
Merge branch 'altair-viz:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
franzhaas authored Jan 16, 2024
2 parents 179667c + 5e03173 commit 6a17d55
Show file tree
Hide file tree
Showing 14 changed files with 79,835 additions and 99,314 deletions.
9 changes: 8 additions & 1 deletion 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 Expand Up @@ -30,10 +35,11 @@ export async function render({ model, el }) {
model.save_changes();
return;
}
let embedOptions = structuredClone(model.get("embed_options")) ?? undefined;

let api;
try {
api = await vegaEmbed(el, spec);
api = await vegaEmbed(el, spec, embedOptions);
} catch (error) {
showError(error)
return;
Expand Down Expand Up @@ -134,6 +140,7 @@ export async function render({ model, el }) {
}

model.on('change:spec', reembed);
model.on('change:embed_options', reembed);
model.on('change:debounce_wait', reembed);
model.on('change:max_wait', reembed);
await reembed();
Expand Down
65 changes: 63 additions & 2 deletions altair/jupyter/jupyter_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import anywidget
import traitlets
import pathlib
from typing import Any, Set
from typing import Any, Set, Optional

import altair as alt
from altair.utils._vegafusion_data import (
Expand Down 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 All @@ -109,6 +113,7 @@ class JupyterChart(anywidget.AnyWidget):
max_wait = traitlets.Bool(default_value=True).tag(sync=True)
local_tz = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
debug = traitlets.Bool(default_value=False)
embed_options = traitlets.Dict(default_value=None, allow_none=True).tag(sync=True)

# Internal selection traitlets
_selection_types = traitlets.Dict()
Expand All @@ -123,12 +128,64 @@ 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,
debounce_wait: int = 10,
max_wait: bool = True,
debug: bool = False,
embed_options: Optional[dict] = None,
**kwargs: Any,
):
"""
Expand All @@ -148,6 +205,9 @@ def __init__(
sent until chart interactions have completed.
debug: bool
If True, debug messages will be printed
embed_options: dict
Options to pass to vega-embed.
See https://github.com/vega/vega-embed?tab=readme-ov-file#options
"""
self.params = Params({})
self.selections = Selections({})
Expand All @@ -156,6 +216,7 @@ def __init__(
debounce_wait=debounce_wait,
max_wait=max_wait,
debug=debug,
embed_options=embed_options,
**kwargs,
)

Expand Down
2 changes: 1 addition & 1 deletion altair/vegalite/v5/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ def param(
name=parameter.name,
bind=bind,
value=value,
expr=expr, # type: ignore[arg-type]
expr=expr,
**kwds,
)
parameter.param_type = "variable"
Expand Down
15 changes: 13 additions & 2 deletions altair/vegalite/v5/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,24 @@ def svg_renderer(spec: dict, **metadata) -> Dict[str, str]:
)


def jupyter_renderer(spec: dict):
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_
# conditionally defined in AnyWidget
return JupyterChart(chart=Chart.from_dict(spec))._repr_mimebundle_() # type: ignore[attr-defined]
return JupyterChart(
chart=Chart.from_dict(spec), embed_options=embed_options
)._repr_mimebundle_() # type: ignore[attr-defined]


html_renderer = HTMLRenderer(
Expand Down
Loading

0 comments on commit 6a17d55

Please sign in to comment.