Skip to content

Commit

Permalink
Merge pull request #1123 from jakevdp/html-opts
Browse files Browse the repository at this point in the history
Refactor HTML output methods
  • Loading branch information
jakevdp authored Sep 4, 2018
2 parents 3ee1530 + d3f179e commit 1dc66d1
Show file tree
Hide file tree
Showing 18 changed files with 272 additions and 88 deletions.
4 changes: 2 additions & 2 deletions altair/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@
use_signature,
update_subtraits,
update_nested,
write_file_or_filename,
display_traceback,
SchemaBase,
Undefined
)
from .html import spec_to_html
from .plugin_registry import PluginRegistry


__all__ = (
'infer_vegalite_type',
'sanitize_dataframe',
'spec_to_html',
'parse_shorthand',
'use_signature',
'update_subtraits',
'update_nested',
'write_file_or_filename',
'display_traceback',
'SchemaBase',
'Undefined',
Expand Down
9 changes: 0 additions & 9 deletions altair/utils/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,15 +386,6 @@ def update_nested(original, update, copy=False):
return original


def write_file_or_filename(fp, content, mode='w'):
"""Write content to fp, whether fp is a string or a file-like object"""
if isinstance(fp, six.string_types):
with open(fp, mode) as f:
f.write(content)
else:
fp.write(content)


def display_traceback(in_ipython=True):
exc_info = sys.exc_info()

Expand Down
20 changes: 20 additions & 0 deletions altair/utils/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from jsonschema import validate

from .plugin_registry import PluginRegistry
from .mimebundle import spec_to_mimebundle


# ==============================================================================
Expand Down Expand Up @@ -100,3 +101,22 @@ def json_renderer_base(spec, str_repr, **options):
"""
return default_renderer_base(spec, mime_type='application/json',
str_repr=str_repr, **options)


class HTMLRenderer(object):
"""Object to render charts as HTML, with a unique output div each time"""
def __init__(self, output_div='altair-viz-{}', **kwargs):
self._output_div = output_div
self._output_count = 0
self.kwargs = kwargs

@property
def output_div(self):
self._output_count += 1
return self._output_div.format(self._output_count)

def __call__(self, spec, **metadata):
kwargs = self.kwargs.copy()
kwargs.update(metadata)
return spec_to_mimebundle(spec, format='html',
output_div=self.output_div, **kwargs)
133 changes: 82 additions & 51 deletions altair/utils/html.py
Original file line number Diff line number Diff line change
@@ -1,76 +1,103 @@
from __future__ import unicode_literals
import json
import jinja2


TOP = """
HTML_TEMPLATE = jinja2.Template("""
{%- if fullhtml -%}
<!DOCTYPE html>
<html>
<head>
{%- endif %}
<style>
.vega-actions a {{
.vega-actions a {
margin-right: 12px;
color: #757575;
font-weight: normal;
font-size: 13px;
}}
.error {{
}
.error {
color: red;
}}
}
</style>
"""

VEGA_SCRIPTS = """
<script src="{base_url}/vega@{vega_version}"></script>
<script src="{base_url}/vega-embed@{vegaembed_version}"></script>
"""

VEGALITE_SCRIPTS = """
<script src="{base_url}/vega@{vega_version}"></script>
<script src="{base_url}/vega-lite@{vegalite_version}"></script>
<script src="{base_url}/vega-embed@{vegaembed_version}"></script>
"""

BOTTOM = """
{%- 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>
<div id="{output_div}"></div>
<script type="text/javascript">
var spec = {spec};
var embed_opt = {embed_opt};
{%- endif %}
<div id="{{ output_div }}"></div>
<script>
{%- if requirejs %}
{%- if 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 %}
require(['vega-embed'], function(vegaEmbed){
{%- endif %}
var spec = {{ spec }};
var embedOpt = {{ embed_options }};
function showError(el, error){
el.innerHTML = ('<div class="error" 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 requirejs %}
});
{%- endif %}
function showError(el, error){{
el.innerHTML = ('<div class="error">'
+ '<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, embed_opt)
.catch(error => showError(el, error));
</script>
{%- if fullhtml %}
</body>
</html>
{%- endif %}
"""


HTML_TEMPLATE = {
'vega-lite': TOP + VEGALITE_SCRIPTS + BOTTOM,
'vega': TOP + VEGA_SCRIPTS + BOTTOM
}
)


def spec_to_html(spec, mode,
vega_version, vegaembed_version, vegalite_version=None,
base_url="https://cdn.jsdelivr.net/npm/",
output_div='vis', embed_options=None, json_kwds=None):
output_div='vis', embed_options=None, json_kwds=None,
fullhtml=True, requirejs=False):
"""Embed a Vega/Vega-Lite spec into an HTML page
Parameters
----------
spec : dict
a dictionary representing a vega-lite plot spec.
fullhtml : boo
mode : string {'vega' | 'vega-lite'}
The rendering mode. This value is overridden by embed_options['mode'],
if it is present.
Expand All @@ -88,11 +115,17 @@ def spec_to_html(spec, mode,
Dictionary of options to pass to the vega-embed script.
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
Returns
-------
output : dict
a mime-bundle representing the image
output : string
an HTML string for rendering the chart.
"""
embed_options = embed_options or {}
json_kwds = json_kwds or {}
Expand All @@ -109,15 +142,13 @@ def spec_to_html(spec, mode,
raise ValueError("must specify vegaembed_version")

if mode == 'vega-lite' and vegalite_version is None:
raise ValueError("must specify vega-lite version")

template = HTML_TEMPLATE[mode]
raise ValueError("must specify vega-lite version for mode='vega-lite'")

spec_html = template.format(spec=json.dumps(spec, **json_kwds),
embed_opt=json.dumps(embed_options),
return HTML_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)
return spec_html
base_url=base_url, output_div=output_div,
fullhtml=fullhtml, requirejs=requirejs)
10 changes: 9 additions & 1 deletion altair/utils/save.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@

import six

from .core import write_file_or_filename
from .mimebundle import spec_to_mimebundle


def write_file_or_filename(fp, content, mode='w'):
"""Write content to fp, whether fp is a string or a file-like object"""
if isinstance(fp, six.string_types):
with open(fp, mode) as f:
f.write(content)
else:
fp.write(content)


def save(chart, fp, vega_version, vegaembed_version,
format=None, mode=None, vegalite_version=None,
embed_options=None, json_kwds=None, webdriver='chrome',
Expand Down
48 changes: 48 additions & 0 deletions altair/utils/tests/test_html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import pytest

from ..html import spec_to_html


@pytest.fixture
def spec():
return {
'data': {'url': 'data.json'},
'mark': 'point',
'encoding': {
'x': {'field': 'x', 'type': 'quantitative'},
'y': {'field': 'y', 'type': 'quantitative'}
}
}


@pytest.mark.parametrize('requirejs', [True, False])
@pytest.mark.parametrize('fullhtml', [True, False])
def test_spec_to_html(requirejs, fullhtml, spec):
# We can't test that the html actually renders, but we'll test aspects of
# it to make certain that the keywords are respected.
vegaembed_version="3.12",
vegalite_version="3.0",
vega_version="4.0"

html = spec_to_html(spec, mode='vega-lite',
requirejs=requirejs, fullhtml=fullhtml,
vegalite_version=vegalite_version,
vegaembed_version=vegaembed_version,
vega_version=vega_version)
html = html.strip()

if fullhtml:
assert html.startswith("<!DOCTYPE html>")
assert html.endswith("</html>")
else:
assert html.startswith("<style>")
assert html.endswith("</script>")

if requirejs:
assert "require(" in html
else:
assert "require(" not in html

assert "vega-lite@{}".format(vegalite_version) in html
assert "vega@{}".format(vega_version) in html
assert "vega-embed@{}".format(vegaembed_version) in html
5 changes: 3 additions & 2 deletions altair/vega/display.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from ..utils.display import Displayable, default_renderer_base, json_renderer_base
from ..utils.display import MimeBundleType, RendererType
from ..utils.display import MimeBundleType, RendererType, HTMLRenderer


__all__ = (
"Displayable",
"default_renderer_base",
"json_renderer_base",
"MimeBundleType",
"RendererType"
"RendererType",
"HTMLRenderer",
)
20 changes: 20 additions & 0 deletions altair/vega/v2/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
from ..display import default_renderer_base
from ..display import json_renderer_base
from ..display import RendererType
from ..display import HTMLRenderer

from .schema import SCHEMA_VERSION
VEGA_VERSION = SCHEMA_VERSION.lstrip('v')
VEGAEMBED_VERSION = '3'



Expand Down Expand Up @@ -44,9 +49,24 @@ def json_renderer(spec):
return json_renderer_base(spec, DEFAULT_DISPLAY)


colab_renderer = HTMLRenderer(mode='vega',
fullhtml=True, requirejs=False,
output_div='altair-viz',
vega_version=VEGA_VERSION,
vegaembed_version=VEGAEMBED_VERSION)


kaggle_renderer = HTMLRenderer(mode='vega',
fullhtml=False, requirejs=True,
vega_version=VEGA_VERSION,
vegaembed_version=VEGAEMBED_VERSION)


renderers.register('default', default_renderer)
renderers.register('jupyterlab', default_renderer)
renderers.register('nteract', default_renderer)
renderers.register('colab', colab_renderer)
renderers.register('kaggle', kaggle_renderer)
renderers.register('json', json_renderer)
renderers.enable('default')

Expand Down
Loading

0 comments on commit 1dc66d1

Please sign in to comment.