From 291bcfc376b6954a09e2c4292b0b7ba13868b108 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 8 Jan 2023 11:13:54 -0500 Subject: [PATCH] Add inline argument to chart.save() for html export (#2807) * 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 * Don't search for closing quote to allow for later versions in the future Co-authored-by: Mattijn van Hoek --- altair/utils/html.py | 50 +++++++++++++++++++++++++++++ altair/utils/save.py | 17 ++++++++-- tests/vegalite/v5/tests/test_api.py | 17 ++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/altair/utils/html.py b/altair/utils/html.py index de2b421f1..6be18139c 100644 --- a/altair/utils/html.py +++ b/altair/utils/html.py @@ -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( + """\ + + + + + + +
+ + + +""" +) + + TEMPLATES = { "standard": HTML_TEMPLATE, "universal": HTML_TEMPLATE_UNIVERSAL, + "inline": INLINE_HTML_TEMPLATE, } @@ -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)) @@ -233,4 +282,5 @@ def spec_to_html( output_div=output_div, fullhtml=fullhtml, requirejs=requirejs, + **render_kwargs, ) diff --git a/altair/utils/save.py b/altair/utils/save.py index fd6fccb5d..3ba845b0a 100644 --- a/altair/utils/save.py +++ b/altair/utils/save.py @@ -1,5 +1,6 @@ import json import pathlib +import warnings from .mimebundle import spec_to_mimebundle @@ -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 ---------- @@ -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 @@ -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. """ @@ -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, diff --git a/tests/vegalite/v5/tests/test_api.py b/tests/vegalite/v5/tests/test_api.py index eb7cdb61c..b2e5aa159 100644 --- a/tests/vegalite/v5/tests/test_api.py +++ b/tests/vegalite/v5/tests/test_api.py @@ -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("") + + if inline: + assert '