411 lines
13 KiB
Python
411 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any, Literal
|
|
|
|
import jinja2
|
|
|
|
from altair.utils._importers import import_vl_convert, vl_version_for_vl_convert
|
|
|
|
TemplateName = Literal["standard", "universal", "inline", "olli"]
|
|
RenderMode = Literal["vega", "vega-lite"]
|
|
|
|
HTML_TEMPLATE = jinja2.Template(
|
|
"""
|
|
{%- if fullhtml -%}
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
{%- endif %}
|
|
<style>
|
|
#{{ output_div }}.vega-embed {
|
|
width: 100%;
|
|
display: flex;
|
|
}
|
|
|
|
#{{ output_div }}.vega-embed details,
|
|
#{{ output_div }}.vega-embed details summary {
|
|
position: relative;
|
|
}
|
|
</style>
|
|
{%- if not requirejs %}
|
|
<script type="text/javascript" src="{{ base_url }}/vega@{{ vega_version }}"></script>
|
|
{%- if mode == 'vega-lite' %}
|
|
<script type="text/javascript" src="{{ base_url }}/vega-lite@{{ vegalite_version }}"></script>
|
|
{%- endif %}
|
|
<script type="text/javascript" src="{{ base_url }}/vega-embed@{{ vegaembed_version }}"></script>
|
|
{%- endif %}
|
|
{%- if fullhtml %}
|
|
{%- if requirejs %}
|
|
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
|
|
<script>
|
|
requirejs.config({
|
|
"paths": {
|
|
"vega": "{{ base_url }}/vega@{{ vega_version }}?noext",
|
|
"vega-lib": "{{ base_url }}/vega-lib?noext",
|
|
"vega-lite": "{{ base_url }}/vega-lite@{{ vegalite_version }}?noext",
|
|
"vega-embed": "{{ base_url }}/vega-embed@{{ vegaembed_version }}?noext",
|
|
}
|
|
});
|
|
</script>
|
|
{%- endif %}
|
|
</head>
|
|
<body>
|
|
{%- endif %}
|
|
<div id="{{ output_div }}"></div>
|
|
<script>
|
|
{%- if requirejs and not fullhtml %}
|
|
requirejs.config({
|
|
"paths": {
|
|
"vega": "{{ base_url }}/vega@{{ vega_version }}?noext",
|
|
"vega-lib": "{{ base_url }}/vega-lib?noext",
|
|
"vega-lite": "{{ base_url }}/vega-lite@{{ vegalite_version }}?noext",
|
|
"vega-embed": "{{ base_url }}/vega-embed@{{ vegaembed_version }}?noext",
|
|
}
|
|
});
|
|
{% endif %}
|
|
{% if requirejs -%}
|
|
require(['vega-embed'],
|
|
{%- else -%}
|
|
(
|
|
{%- endif -%}
|
|
function(vegaEmbed) {
|
|
var spec = {{ spec }};
|
|
var embedOpt = {{ embed_options }};
|
|
|
|
function showError(el, error){
|
|
el.innerHTML = ('<div style="color:red;">'
|
|
+ '<p>JavaScript Error: ' + error.message + '</p>'
|
|
+ "<p>This usually means there's a typo in your chart specification. "
|
|
+ "See the javascript console for the full traceback.</p>"
|
|
+ '</div>');
|
|
throw error;
|
|
}
|
|
const el = document.getElementById('{{ output_div }}');
|
|
vegaEmbed("#{{ output_div }}", spec, embedOpt)
|
|
.catch(error => showError(el, error));
|
|
}){% if not requirejs %}(vegaEmbed){% endif %};
|
|
|
|
</script>
|
|
{%- if fullhtml %}
|
|
</body>
|
|
</html>
|
|
{%- endif %}
|
|
"""
|
|
)
|
|
|
|
|
|
HTML_TEMPLATE_UNIVERSAL = jinja2.Template(
|
|
"""
|
|
<style>
|
|
#{{ output_div }}.vega-embed {
|
|
width: 100%;
|
|
display: flex;
|
|
}
|
|
|
|
#{{ output_div }}.vega-embed details,
|
|
#{{ output_div }}.vega-embed details summary {
|
|
position: relative;
|
|
}
|
|
</style>
|
|
<div id="{{ output_div }}"></div>
|
|
<script type="text/javascript">
|
|
var VEGA_DEBUG = (typeof VEGA_DEBUG == "undefined") ? {} : VEGA_DEBUG;
|
|
(function(spec, embedOpt){
|
|
let outputDiv = document.currentScript.previousElementSibling;
|
|
if (outputDiv.id !== "{{ output_div }}") {
|
|
outputDiv = document.getElementById("{{ output_div }}");
|
|
}
|
|
|
|
const paths = {
|
|
"vega": "{{ base_url }}/vega@{{ vega_version }}?noext",
|
|
"vega-lib": "{{ base_url }}/vega-lib?noext",
|
|
"vega-lite": "{{ base_url }}/vega-lite@{{ vegalite_version }}?noext",
|
|
"vega-embed": "{{ base_url }}/vega-embed@{{ vegaembed_version }}?noext",
|
|
};
|
|
|
|
function maybeLoadScript(lib, version) {
|
|
var key = `${lib.replace("-", "")}_version`;
|
|
return (VEGA_DEBUG[key] == version) ?
|
|
Promise.resolve(paths[lib]) :
|
|
new Promise(function(resolve, reject) {
|
|
var s = document.createElement('script');
|
|
document.getElementsByTagName("head")[0].appendChild(s);
|
|
s.async = true;
|
|
s.onload = () => {
|
|
VEGA_DEBUG[key] = version;
|
|
return resolve(paths[lib]);
|
|
};
|
|
s.onerror = () => reject(`Error loading script: ${paths[lib]}`);
|
|
s.src = paths[lib];
|
|
});
|
|
}
|
|
|
|
function showError(err) {
|
|
outputDiv.innerHTML = `<div class="error" style="color:red;">${err}</div>`;
|
|
throw err;
|
|
}
|
|
|
|
function displayChart(vegaEmbed) {
|
|
vegaEmbed(outputDiv, spec, embedOpt)
|
|
.catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));
|
|
}
|
|
|
|
if(typeof define === "function" && define.amd) {
|
|
requirejs.config({paths});
|
|
let deps = ["vega-embed"];
|
|
require(deps, displayChart, err => showError(`Error loading script: ${err.message}`));
|
|
} else {
|
|
maybeLoadScript("vega", "{{vega_version}}")
|
|
.then(() => maybeLoadScript("vega-lite", "{{vegalite_version}}"))
|
|
.then(() => maybeLoadScript("vega-embed", "{{vegaembed_version}}"))
|
|
.catch(showError)
|
|
.then(() => displayChart(vegaEmbed));
|
|
}
|
|
})({{ spec }}, {{ embed_options }});
|
|
</script>
|
|
"""
|
|
)
|
|
|
|
|
|
# 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>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
#{{ output_div }}.vega-embed {
|
|
width: 100%;
|
|
display: flex;
|
|
}
|
|
|
|
#{{ output_div }}.vega-embed details,
|
|
#{{ output_div }}.vega-embed details summary {
|
|
position: relative;
|
|
}
|
|
</style>
|
|
<script type="text/javascript">
|
|
// vega-embed.js bundle with Vega-Lite version v{{ vegalite_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>
|
|
"""
|
|
)
|
|
|
|
|
|
HTML_TEMPLATE_OLLI = jinja2.Template(
|
|
"""
|
|
<style>
|
|
#{{ output_div }}.vega-embed {
|
|
width: 100%;
|
|
display: flex;
|
|
}
|
|
|
|
#{{ output_div }}.vega-embed details,
|
|
#{{ output_div }}.vega-embed details summary {
|
|
position: relative;
|
|
}
|
|
</style>
|
|
<div id="{{ output_div }}"></div>
|
|
<script type="text/javascript">
|
|
var VEGA_DEBUG = (typeof VEGA_DEBUG == "undefined") ? {} : VEGA_DEBUG;
|
|
(function(spec, embedOpt){
|
|
let outputDiv = document.currentScript.previousElementSibling;
|
|
if (outputDiv.id !== "{{ output_div }}") {
|
|
outputDiv = document.getElementById("{{ output_div }}");
|
|
}
|
|
const olliDiv = document.createElement("div");
|
|
const vegaDiv = document.createElement("div");
|
|
outputDiv.appendChild(vegaDiv);
|
|
outputDiv.appendChild(olliDiv);
|
|
outputDiv = vegaDiv;
|
|
|
|
const paths = {
|
|
"vega": "{{ base_url }}/vega@{{ vega_version }}?noext",
|
|
"vega-lib": "{{ base_url }}/vega-lib?noext",
|
|
"vega-lite": "{{ base_url }}/vega-lite@{{ vegalite_version }}?noext",
|
|
"vega-embed": "{{ base_url }}/vega-embed@{{ vegaembed_version }}?noext",
|
|
"olli": "{{ base_url }}/olli@{{ olli_version }}?noext",
|
|
"olli-adapters": "{{ base_url }}/olli-adapters@{{ olli_adapters_version }}?noext",
|
|
};
|
|
|
|
function maybeLoadScript(lib, version) {
|
|
var key = `${lib.replace("-", "")}_version`;
|
|
return (VEGA_DEBUG[key] == version) ?
|
|
Promise.resolve(paths[lib]) :
|
|
new Promise(function(resolve, reject) {
|
|
var s = document.createElement('script');
|
|
document.getElementsByTagName("head")[0].appendChild(s);
|
|
s.async = true;
|
|
s.onload = () => {
|
|
VEGA_DEBUG[key] = version;
|
|
return resolve(paths[lib]);
|
|
};
|
|
s.onerror = () => reject(`Error loading script: ${paths[lib]}`);
|
|
s.src = paths[lib];
|
|
});
|
|
}
|
|
|
|
function showError(err) {
|
|
outputDiv.innerHTML = `<div class="error" style="color:red;">${err}</div>`;
|
|
throw err;
|
|
}
|
|
|
|
function displayChart(vegaEmbed, olli, olliAdapters) {
|
|
vegaEmbed(outputDiv, spec, embedOpt)
|
|
.catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));
|
|
olliAdapters.VegaLiteAdapter(spec).then(olliVisSpec => {
|
|
const olliFunc = typeof olli === 'function' ? olli : olli.olli;
|
|
const olliRender = olliFunc(olliVisSpec);
|
|
olliDiv.append(olliRender);
|
|
});
|
|
}
|
|
|
|
if(typeof define === "function" && define.amd) {
|
|
requirejs.config({paths});
|
|
let deps = ["vega-embed", "olli", "olli-adapters"];
|
|
require(deps, displayChart, err => showError(`Error loading script: ${err.message}`));
|
|
} else {
|
|
maybeLoadScript("vega", "{{vega_version}}")
|
|
.then(() => maybeLoadScript("vega-lite", "{{vegalite_version}}"))
|
|
.then(() => maybeLoadScript("vega-embed", "{{vegaembed_version}}"))
|
|
.then(() => maybeLoadScript("olli", "{{olli_version}}"))
|
|
.then(() => maybeLoadScript("olli-adapters", "{{olli_adapters_version}}"))
|
|
.catch(showError)
|
|
.then(() => displayChart(vegaEmbed, olli, OlliAdapters));
|
|
}
|
|
})({{ spec }}, {{ embed_options }});
|
|
</script>
|
|
"""
|
|
)
|
|
|
|
|
|
TEMPLATES: dict[TemplateName, jinja2.Template] = {
|
|
"standard": HTML_TEMPLATE,
|
|
"universal": HTML_TEMPLATE_UNIVERSAL,
|
|
"inline": INLINE_HTML_TEMPLATE,
|
|
"olli": HTML_TEMPLATE_OLLI,
|
|
}
|
|
|
|
|
|
def spec_to_html(
|
|
spec: dict[str, Any],
|
|
mode: RenderMode,
|
|
vega_version: str | None,
|
|
vegaembed_version: str | None,
|
|
vegalite_version: str | None = None,
|
|
base_url: str = "https://cdn.jsdelivr.net/npm",
|
|
output_div: str = "vis",
|
|
embed_options: dict[str, Any] | None = None,
|
|
json_kwds: dict[str, Any] | None = None,
|
|
fullhtml: bool = True,
|
|
requirejs: bool = False,
|
|
template: jinja2.Template | TemplateName = "standard",
|
|
) -> str:
|
|
"""
|
|
Embed a Vega/Vega-Lite spec into an HTML page.
|
|
|
|
Parameters
|
|
----------
|
|
spec : dict
|
|
a dictionary representing a vega-lite plot spec.
|
|
mode : string {'vega' | 'vega-lite'}
|
|
The rendering mode. This value is overridden by embed_options['mode'],
|
|
if it is present.
|
|
vega_version : string
|
|
For html output, the version of vega.js to use.
|
|
vegalite_version : string
|
|
For html output, the version of vegalite.js to use.
|
|
vegaembed_version : string
|
|
For html output, the version of vegaembed.js to use.
|
|
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
|
|
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', 'inline'}. Otherwise, it
|
|
can be a jinja2.Template object containing a custom template.
|
|
|
|
Returns
|
|
-------
|
|
output : string
|
|
an HTML string for rendering the chart.
|
|
"""
|
|
embed_options = embed_options or {}
|
|
json_kwds = json_kwds or {}
|
|
|
|
mode = embed_options.setdefault("mode", mode)
|
|
|
|
if mode not in {"vega", "vega-lite"}:
|
|
msg = "mode must be either 'vega' or 'vega-lite'"
|
|
raise ValueError(msg)
|
|
|
|
if vega_version is None:
|
|
msg = "must specify vega_version"
|
|
raise ValueError(msg)
|
|
|
|
if vegaembed_version is None:
|
|
msg = "must specify vegaembed_version"
|
|
raise ValueError(msg)
|
|
|
|
if mode == "vega-lite" and vegalite_version is None:
|
|
msg = "must specify vega-lite version for mode='vega-lite'"
|
|
raise ValueError(msg)
|
|
|
|
render_kwargs = {}
|
|
if template == "inline":
|
|
vlc = import_vl_convert()
|
|
vl_version = vl_version_for_vl_convert()
|
|
render_kwargs["vegaembed_script"] = vlc.javascript_bundle(vl_version=vl_version)
|
|
elif template == "olli":
|
|
OLLI_VERSION = "2"
|
|
OLLI_ADAPTERS_VERSION = "2"
|
|
render_kwargs["olli_version"] = OLLI_VERSION
|
|
render_kwargs["olli_adapters_version"] = OLLI_ADAPTERS_VERSION
|
|
|
|
jinja_template = TEMPLATES.get(template, template) # type: ignore[arg-type]
|
|
if not hasattr(jinja_template, "render"):
|
|
msg = f"Invalid template: {jinja_template}"
|
|
raise ValueError(msg)
|
|
|
|
return jinja_template.render(
|
|
spec=json.dumps(spec, **json_kwds),
|
|
embed_options=json.dumps(embed_options),
|
|
mode=mode,
|
|
vega_version=vega_version,
|
|
vegalite_version=vegalite_version,
|
|
vegaembed_version=vegaembed_version,
|
|
base_url=base_url,
|
|
output_div=output_div,
|
|
fullhtml=fullhtml,
|
|
requirejs=requirejs,
|
|
**render_kwargs,
|
|
)
|