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

Refactor HTML output methods #1123

Merged
merged 6 commits into from
Sep 4, 2018
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
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