Skip to content

Commit

Permalink
Add inline argument to chart.save() for html export (#2807)
Browse files Browse the repository at this point in the history
* Add inline argument to chart.save()

This includes the Vega/Vega-Lite/Vega-Embed JavaScript source in the exported html file so that it works offline.  The logic is ported from altair_saver.

* Only override the `template` argument if `inline=True`

This way it's still possible to pass in custom templates

* Add 'pdf' to `format` argument docstring

* Another missed pdf format

* check for @5 instead of @5.2.0 for forward compatibility

Co-authored-by: Mattijn van Hoek <mattijn@gmail.com>

* Don't search for closing quote to allow for later versions in the future

Co-authored-by: Mattijn van Hoek <mattijn@gmail.com>
  • Loading branch information
jonmmease and mattijn authored Jan 8, 2023
1 parent 178e9d7 commit 291bcfc
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 2 deletions.
50 changes: 50 additions & 0 deletions altair/utils/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,42 @@
)


# This is like the HTML_TEMPLATE template, but includes vega javascript inline
# so that the resulting file is not dependent on external resources. This was
# ported over from altair_saver.
#
# implies requirejs=False and full_html=True
INLINE_HTML_TEMPLATE = jinja2.Template(
"""\
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
// vega.js v{{ vega_version }}
{{ vega_script }}
// vega-lite.js v{{ vegalite_version }}
{{ vegalite_script }}
// vega-embed.js v{{ vegaembed_version }}
{{ vegaembed_script }}
</script>
</head>
<body>
<div class="vega-visualization" id="{{ output_div }}"></div>
<script type="text/javascript">
const spec = {{ spec }};
const embedOpt = {{ embed_options }};
vegaEmbed('#{{ output_div }}', spec, embedOpt).catch(console.error);
</script>
</body>
</html>
"""
)


TEMPLATES = {
"standard": HTML_TEMPLATE,
"universal": HTML_TEMPLATE_UNIVERSAL,
"inline": INLINE_HTML_TEMPLATE,
}


Expand Down Expand Up @@ -218,6 +251,22 @@ def spec_to_html(
if mode == "vega-lite" and vegalite_version is None:
raise ValueError("must specify vega-lite version for mode='vega-lite'")

render_kwargs = dict()
if template == "inline":
try:
from altair_viewer import get_bundled_script
except ImportError:
raise ImportError(
"The altair_viewer package is required to convert to HTML with inline=True"
)
render_kwargs["vega_script"] = get_bundled_script("vega", vega_version)
render_kwargs["vegalite_script"] = get_bundled_script(
"vega-lite", vegalite_version
)
render_kwargs["vegaembed_script"] = get_bundled_script(
"vega-embed", vegaembed_version
)

template = TEMPLATES.get(template, template)
if not hasattr(template, "render"):
raise ValueError("Invalid template: {0}".format(template))
Expand All @@ -233,4 +282,5 @@ def spec_to_html(
output_div=output_div,
fullhtml=fullhtml,
requirejs=requirejs,
**render_kwargs,
)
17 changes: 15 additions & 2 deletions altair/utils/save.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import pathlib
import warnings

from .mimebundle import spec_to_mimebundle

Expand Down Expand Up @@ -27,11 +28,12 @@ def save(
webdriver=None,
scale_factor=1,
engine=None,
inline=False,
**kwargs,
):
"""Save a chart to file in a variety of formats
Supported formats are [json, html, png, svg]
Supported formats are [json, html, png, svg, pdf]
Parameters
----------
Expand All @@ -40,7 +42,7 @@ def save(
fp : string filename, pathlib.Path or file-like object
file to which to write the chart.
format : string (optional)
the format to write: one of ['json', 'html', 'png', 'svg'].
the format to write: one of ['json', 'html', 'png', 'svg', 'pdf'].
If not specified, the format will be determined from the filename.
mode : string (optional)
Either 'vega' or 'vegalite'. If not specified, then infer the mode from
Expand All @@ -64,6 +66,12 @@ def save(
scale_factor to use to change size/resolution of png or svg output
engine: string {'vl-convert', 'altair_saver'}
the conversion engine to use for 'png', 'svg', and 'pdf' formats
inline: bool (optional)
If False (default), the required JavaScript libraries are loaded
from a CDN location in the resulting html file.
If True, the required JavaScript libraries are inlined into the resulting
html file so that it will work without an internet connection.
The altair_viewer package is required if True.
**kwargs :
additional kwargs passed to spec_to_mimebundle.
"""
Expand Down Expand Up @@ -99,10 +107,15 @@ def save(
if mode == "vega-lite" and vegalite_version is None:
raise ValueError("must specify vega-lite version")

if format != "html" and inline:
warnings.warn("inline argument ignored for non HTML formats.")

if format == "json":
json_spec = json.dumps(spec, **json_kwds)
write_file_or_filename(fp, json_spec, mode="w")
elif format == "html":
if inline:
kwargs["template"] = "inline"
mimebundle = spec_to_mimebundle(
spec=spec,
format=format,
Expand Down
17 changes: 17 additions & 0 deletions tests/vegalite/v5/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,23 @@ def test_save(format, engine, basic_chart):
os.remove(fp)


@pytest.mark.parametrize("inline", [False, True])
def test_save_html(basic_chart, inline):
out = io.StringIO()
basic_chart.save(out, format="html", inline=inline)
out.seek(0)
content = out.read()

assert content.startswith("<!DOCTYPE html>")

if inline:
assert '<script type="text/javascript">' in content
else:
assert 'src="https://cdn.jsdelivr.net/npm/vega@5' in content
assert 'src="https://cdn.jsdelivr.net/npm/vega-lite@5' in content
assert 'src="https://cdn.jsdelivr.net/npm/vega-embed@6' in content


def test_facet_basic():
# wrapped facet
chart1 = (
Expand Down

0 comments on commit 291bcfc

Please sign in to comment.