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

Use vl-convert for offline html export #3251

Merged
merged 6 commits into from
Nov 9, 2023
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
12 changes: 10 additions & 2 deletions altair/utils/_importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def import_vegafusion() -> ModuleType:


def import_vl_convert() -> ModuleType:
min_version = "1.0.0"
min_version = "1.0.1"
try:
version = importlib_version("vl-convert-python")
if Version(version) < Version(min_version):
Expand All @@ -42,7 +42,7 @@ def import_vl_convert() -> ModuleType:
return vlc
except ImportError as err:
raise ImportError(
f"The vl-convert Vega-Lite compiler and image export feature requires\n"
f"The vl-convert Vega-Lite compiler and file export feature requires\n"
f"version {min_version} or greater of the 'vl-convert-python' package. \n"
f"This can be installed with pip using:\n"
f' pip install "vl-convert-python>={min_version}"\n'
Expand All @@ -52,6 +52,14 @@ def import_vl_convert() -> ModuleType:
) from err


def vl_version_for_vl_convert() -> str:
from ..vegalite import SCHEMA_VERSION

# Compute VlConvert's vl_version string (of the form 'v5_2')
# from SCHEMA_VERSION (of the form 'v5.2.0')
return "_".join(SCHEMA_VERSION.split(".")[:2])


def import_pyarrow_interchange() -> ModuleType:
min_version = "11.0.0"
try:
Expand Down
25 changes: 6 additions & 19 deletions altair/utils/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import jinja2

from altair.utils._importers import import_vl_convert, vl_version_for_vl_convert

HTML_TEMPLATE = jinja2.Template(
"""
Expand Down Expand Up @@ -182,11 +183,7 @@
}
</style>
<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 }}
// vega-embed.js bundle with Vega-Lite version v{{ vegalite_version }}
{{ vegaembed_script }}
</script>
</head>
Expand Down Expand Up @@ -256,7 +253,7 @@ def spec_to_html(
tags. If True, then load libraries using requirejs
template : jinja2.Template or string (optional)
Specify the template to use (default = 'standard'). If template is a
string, it must be one of {'universal', 'standard'}. Otherwise, it
string, it must be one of {'universal', 'standard', 'inline'}. Otherwise, it
can be a jinja2.Template object containing a custom template.

Returns
Expand All @@ -283,19 +280,9 @@ def spec_to_html(

render_kwargs = {}
if template == "inline":
try:
from altair_viewer import get_bundled_script
except ImportError as err:
raise ImportError(
"The altair_viewer package is required to convert to HTML with inline=True"
) from err
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
)
vlc = import_vl_convert()
vl_version = vl_version_for_vl_convert()
render_kwargs["vegaembed_script"] = vlc.javascript_bundle(vl_version=vl_version)

jinja_template = TEMPLATES.get(template, template)
if not hasattr(jinja_template, "render"):
Expand Down
8 changes: 2 additions & 6 deletions altair/utils/mimebundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from .deprecation import AltairDeprecationWarning
from .html import spec_to_html
from ._importers import import_vl_convert
from ._importers import import_vl_convert, vl_version_for_vl_convert
import struct
import warnings

Expand Down Expand Up @@ -116,11 +116,7 @@ def _spec_to_mimebundle_with_engine(

if normalized_engine == "vlconvert":
vlc = import_vl_convert()
from ..vegalite import SCHEMA_VERSION

# Compute VlConvert's vl_version string (of the form 'v5_2')
# from SCHEMA_VERSION (of the form 'v5.2.0')
vl_version = "_".join(SCHEMA_VERSION.split(".")[:2])
vl_version = vl_version_for_vl_convert()
if format == "vega":
if mode == "vega":
vg = spec
Expand Down
4 changes: 2 additions & 2 deletions altair/utils/save.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def save(
Additional keyword arguments are passed to the output method
associated with the specified format.
webdriver : string {'chrome' | 'firefox'} (optional)
Webdriver to use for png or svg output
Webdriver to use for png, svg, or pdf output when using altair_saver engine
scale_factor : float (optional)
scale_factor to use to change size/resolution of png or svg output
engine: string {'vl-convert', 'altair_saver'}
Expand All @@ -124,7 +124,7 @@ def save(
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.
The vl-convert-python package is required if True.
**kwargs :
additional kwargs passed to spec_to_mimebundle.
"""
Expand Down
83 changes: 77 additions & 6 deletions altair/vegalite/v5/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1040,7 +1040,43 @@ def to_html(
json_kwds: Optional[dict] = None,
fullhtml: bool = True,
requirejs: bool = False,
inline: bool = False,
**kwargs,
) -> str:
"""Embed a Vega/Vega-Lite spec into an HTML page

Parameters
----------
base_url : string (optional)
The base url from which to load the javascript libraries.
output_div : string (optional)
The id of the div element where the plot will be shown.
embed_options : dict (optional)
Dictionary of options to pass to the vega-embed script. Default
entry is {'mode': mode}.
json_kwds : dict (optional)
Dictionary of keywords to pass to json.dumps().
fullhtml : boolean (optional)
If True (default) then return a full html page. If False, then return
an HTML snippet that can be embedded into an HTML page.
requirejs : boolean (optional)
If False (default) then load libraries from base_url using <script>
tags. If True, then load libraries using requirejs
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 vl-convert-python package is required if True.
**kwargs :
additional kwargs passed to spec_to_html.
Returns
-------
output : string
an HTML string for rendering the chart.
"""
if inline:
kwargs["template"] = "inline"
return utils.spec_to_html(
self.to_dict(),
mode="vega-lite",
Expand All @@ -1053,6 +1089,7 @@ def to_html(
json_kwds=json_kwds,
fullhtml=fullhtml,
requirejs=requirejs,
**kwargs,
)

def to_url(self, *, fullscreen: bool = False) -> str:
Expand Down Expand Up @@ -1080,9 +1117,15 @@ def save(
format: Optional[Literal["json", "html", "png", "svg", "pdf"]] = None,
override_data_transformer: bool = True,
scale_factor: float = 1.0,
mode: Optional[str] = None,
vegalite_version: str = VEGALITE_VERSION,
vega_version: str = VEGA_VERSION,
vegaembed_version: str = VEGAEMBED_VERSION,
embed_options: Optional[dict] = None,
json_kwds: Optional[dict] = None,
webdriver: Optional[str] = None,
engine: Optional[str] = None,
inline=False,
**kwargs,
) -> None:
"""Save a chart to file in a variety of formats
Expand All @@ -1101,14 +1144,36 @@ def save(
If True (default), then the save action will be done with
the MaxRowsError disabled. If False, then do not change the data
transformer.
scale_factor : float
For svg or png formats, scale the image by this factor when saving.
This can be used to control the size or resolution of the output.
Default is 1.0
**kwargs :
scale_factor : float (optional)
scale_factor to use to change size/resolution of png or svg output
mode : string (optional)
Must be 'vega-lite'. If not specified, then infer the mode from
the '$schema' property of the spec, or the ``opt`` dictionary.
If it's not specified in either of those places, then use 'vega-lite'.
vegalite_version : string (optional)
For html output, the version of vegalite.js to use
vega_version : string (optional)
For html output, the version of vega.js to use
vegaembed_version : string (optional)
For html output, the version of vegaembed.js to use
embed_options : dict (optional)
The vegaEmbed options dictionary. Default is {}
(See https://github.com/vega/vega-embed for details)
json_kwds : dict (optional)
Additional keyword arguments are passed to the output method
associated with the specified format.

webdriver : string {'chrome' | 'firefox'} (optional)
Webdriver to use for png, svg, or pdf output when using altair_saver engine
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 vl-convert-python package is required if True.
**kwargs :
additional kwargs passed to spec_to_mimebundle.
"""
from ...utils.save import save

Expand All @@ -1117,9 +1182,15 @@ def save(
fp=fp,
format=format,
scale_factor=scale_factor,
mode=mode,
vegalite_version=vegalite_version,
vega_version=vega_version,
vegaembed_version=vegaembed_version,
embed_options=embed_options,
json_kwds=json_kwds,
webdriver=webdriver,
engine=engine,
inline=inline,
**kwargs,
)

Expand Down
1 change: 1 addition & 0 deletions doc/releases/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Version 5.2.0 (unreleased month date, year)

Enhancements
~~~~~~~~~~~~
- Support offline HTML export using vl-convert (#3251)
- Support saving charts as PDF files using the vl-convert export engine (#3244)
- Support converting charts to sharable Vega editor URLs with ``chart.to_url()`` (#3252)

Expand Down
18 changes: 18 additions & 0 deletions doc/user_guide/saving_charts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,22 @@ change to ``svg`` rendering, use the ``embed_options`` as such:
This is not the same as ``alt.renderers.enable('svg')``, what renders the
chart as a static ``svg`` image within a Jupyter notebook.


Offline HTML support
^^^^^^^^^^^^^^^^^^^^
By default, an HTML file generated by ``chart.save('chart.html')`` loads the necessary JavaScript dependencies from an online CDN location. This results in a small HTML file, but it means that an active internet connection is required in order to display the chart.

As an alternative, the ``inline=True`` keyword argument may be provided to ``chart.save`` to generate an HTML file that includes all necessary JavaScript dependencies inline. This results in a larger file size, but HTML files generated this way do not require an active internet connection to display.

.. code-block:: python

chart.save('chart.html', inline=True)

.. note::

Calling ``chart.save`` with ``inline=True`` requires the :ref:`install-vl-convert` package.


.. _saving-png:

PNG, SVG, and PDF format
Expand All @@ -161,6 +177,8 @@ javascript code necessary to interpret the Vega-Lite specification and output
it in the form of an image. There are two packages that can be used to enable
image export: vl-convert_ or altair_saver_.

.. _install-vl-convert:

vl-convert
^^^^^^^^^^
The vl-convert_ package can be installed with::
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ dev = [
"pytest-cov",
"m2r",
"vega_datasets",
"vl-convert-python>=1.0.0",
"vl-convert-python>=1.0.1",
"mypy",
"pandas-stubs",
"types-jsonschema",
Expand Down