Skip to content

Use newer vega versions for altair compatibility #959

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

Merged
108 changes: 89 additions & 19 deletions folium/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from __future__ import (absolute_import, division, print_function)

import json
import os
import warnings

from branca.colormap import LinearColormap, StepColormap
Expand Down Expand Up @@ -112,7 +113,8 @@ def render(self, **kwargs):
'if it is not in a Figure.')

figure.header.add_child(
JavascriptLink('https://cdnjs.cloudflare.com/ajax/libs/leaflet-dvf/0.3.0/leaflet-dvf.markers.min.js'), # noqa
JavascriptLink('https://cdnjs.cloudflare.com/ajax/libs/leaflet-dvf/0.3.0/leaflet-dvf.markers.min.js'),
# noqa
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep this all in one line.

name='dvf_js')


Expand Down Expand Up @@ -208,6 +210,10 @@ def render(self, **kwargs):
name='vega_parse')


def get_vega_versions(spec):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used somewhere?

pass


class VegaLite(Element):
"""
Creates a Vega-Lite chart element.
Expand Down Expand Up @@ -241,7 +247,7 @@ class VegaLite(Element):

def __init__(self, data, width=None, height=None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're only updating VegaLite it seems, why not Vega also?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, will do

left='0%', top='0%', position='relative'):
super(VegaLite, self).__init__()
super(self.__class__, self).__init__()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep this PR clean and don't change this here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I somehow got a strange error at some point, when I used VegaLite instead of self.__class__, but I can revert that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the error?

self._name = 'VegaLite'
self.data = data.to_json() if hasattr(data, 'to_json') else data
if isinstance(self.data, text_type) or isinstance(data, binary_type):
Expand All @@ -260,20 +266,12 @@ def render(self, **kwargs):
"""Renders the HTML representation of the element."""
self.json = json.dumps(self.data)

vegalite_major_version = self._get_vegalite_major_versions(self.data)

self._parent.html.add_child(Element(Template("""
<div id="{{this.get_name()}}"></div>
""").render(this=self, kwargs=kwargs)), name=self.get_name())

self._parent.script.add_child(Element(Template("""
var embedSpec = {
mode: "vega-lite",
spec: {{this.json}}
};
vg.embed(
{{this.get_name()}}, embedSpec, function(error, result) {}
);
""").render(this=self)), name=self.get_name())

figure = self.get_root()
assert isinstance(figure, Figure), ('You cannot render this Element '
'if it is not in a Figure.')
Expand All @@ -288,20 +286,88 @@ def render(self, **kwargs):
</style>
""").render(this=self, **kwargs)), name=self.get_name())

if vegalite_major_version == '1':
self._embed_vegalite_v1(figure)
elif vegalite_major_version == '2':
self._embed_vegalite_v2(figure)
elif vegalite_major_version == '3':
self._embed_vegalite_v3(figure)
else:
self._embed_vegalite_v2(figure)

def _get_vegalite_major_versions(self, spec):
try:
schema = spec['$schema']
version = os.path.splitext(os.path.split(schema)[1])[0].lstrip('v')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need os to split schema here? I'm just thinking that we can avoid one extra import.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one could solve it just with str.split, but the os functions more direct and have somewhat speaking names. I also think importing os does not harm as it is in the standard library.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm +0.5 on that. Indeed os is standard lib, but it is an extra import that is used for a simple task. I would also argue that .split('/') is more readble than .path.splitext which is intended for file system paths.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convinced! Even needs one method call less this way.

major_version = version.split('.')[0]
except KeyError:
major_version = None

return major_version

def _embed_vegalite_v3(self, figure):
self.vega_embed()

figure.header.add_child(
JavascriptLink('https://cdn.jsdelivr.net/npm/vega@4'),
name='vega')

figure.header.add_child(
JavascriptLink('https://cdn.jsdelivr.net/npm/vega-lite@3'),
name='vega-lite')

figure.header.add_child(
JavascriptLink('https://cdn.jsdelivr.net/npm/vega-embed@3'),
name='vega-embed')

def _embed_vegalite_v2(self, figure):
self.vega_embed()

figure.header.add_child(
JavascriptLink('https://cdn.jsdelivr.net/npm/vega@3'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this kind of cdn url

name='vega')

figure.header.add_child(
JavascriptLink('https://cdn.jsdelivr.net/npm/vega-lite@2'),
name='vega-lite')

figure.header.add_child(
JavascriptLink('https://cdn.jsdelivr.net/npm/vega-embed@3'),
name='vega-embed')

def vega_embed(self):
self._parent.script.add_child(Element(Template("""
const spec = {{this.json}};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec here will need a unique name or we won't be able to display more than 1 object.

Copy link
Contributor Author

@JarnoRFB JarnoRFB Dec 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a simple fix by just avoiding using the spec variable. However, we should test against this case for the future. Unfortunately I have no idea how to do this, as the error is happening on the JS side. Just executing a test with two VegaLite elements does not throw an error.
Putting an example in the example notebooks might be a way, but could look a bit awkward and is also not caught by pytest.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a simple fix by just avoiding using the spec variable.

Great!

However, we should test against this case for the future. Unfortunately I have no idea how to do this, as the error is happening on the JS side. Just executing a test with two VegaLite elements does not throw an error.
Putting an example in the example notebooks might be a way, but could look a bit awkward and is also not caught by pytest.

Yeah, that is hard. We can create a map and compare an expected output but that may not catch errors like that. We do build all the maps in the example gallery and check them always (sometimes) before merging a PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe one could start a headless selenium browser and check if the map and the charts are there. But that would be maybe a bit two much for this PR. I can easily provide a notebook example for now and try to tackle more elaborate testing in a separate PR if that is fine for you.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That kind of testing is in our TODO list for a long time and I agree, it is something for another PR.

This one is good to go, just waiting for @Conengmo last 👍 to merge it.

vegaEmbed({{this.get_name()}}, spec)
.then(function(result) {})
.catch(console.error);
""").render(this=self)), name=self.get_name())

def _embed_vegalite_v1(self, figure):
self._parent.script.add_child(Element(Template("""
var embedSpec = {
mode: "vega-lite",
spec: {{this.json}}
};
vg.embed(
{{this.get_name()}}, embedSpec, function(error, result) {}
);
""").render(this=self)), name=self.get_name())

figure.header.add_child(
JavascriptLink('https://d3js.org/d3.v3.min.js'),
name='d3')

figure.header.add_child(
JavascriptLink('https://cdnjs.cloudflare.com/ajax/libs/vega/2.6.5/vega.min.js'), # noqa
JavascriptLink('https://cdnjs.cloudflare.com/ajax/libs/vega/2.6.5/vega.js'), # noqa
name='vega')

figure.header.add_child(
JavascriptLink('https://cdnjs.cloudflare.com/ajax/libs/vega-lite/1.3.1/vega-lite.min.js'), # noqa
JavascriptLink('https://cdnjs.cloudflare.com/ajax/libs/vega-lite/1.3.1/vega-lite.js'), # noqa
name='vega-lite')

figure.header.add_child(
JavascriptLink('https://cdnjs.cloudflare.com/ajax/libs/vega-embed/2.2.0/vega-embed.min.js'), # noqa
JavascriptLink('https://cdnjs.cloudflare.com/ajax/libs/vega-embed/2.2.0/vega-embed.js'), # noqa
name='vega-embed')


Expand Down Expand Up @@ -444,7 +510,8 @@ def style_data(self):

for feature in self.data['features']:
feature.setdefault('properties', {}).setdefault('style', {}).update(self.style_function(feature)) # noqa
feature.setdefault('properties', {}).setdefault('highlight', {}).update(self.highlight_function(feature)) # noqa
feature.setdefault('properties', {}).setdefault('highlight', {}).update(
self.highlight_function(feature)) # noqa
return json.dumps(self.data, sort_keys=True)

def _get_self_bounds(self):
Expand Down Expand Up @@ -559,11 +626,13 @@ def style_data(self):
a corresponding JSON output.

"""

def recursive_get(data, keys):
if len(keys):
return recursive_get(data.get(keys[0]), keys[1:])
else:
return data

geometries = recursive_get(self.data, self.object_path.split('.'))['geometries'] # noqa
for feature in geometries:
feature.setdefault('properties', {}).setdefault('style', {}).update(self.style_function(feature)) # noqa
Expand Down Expand Up @@ -700,8 +769,8 @@ def __init__(self, fields, aliases=None, labels=True,
' the same length.'
assert isinstance(labels, bool), 'labels requires a boolean value.'
assert isinstance(localize, bool), 'localize must be bool.'
assert 'permanent' not in kwargs, 'The `permanent` option does not ' \
'work with GeoJsonTooltip.'
assert 'permanent' not in kwargs, 'The `permanent` option does not ' \
'work with GeoJsonTooltip.'

self.fields = fields
self.aliases = aliases
Expand Down Expand Up @@ -851,7 +920,7 @@ class Choropleth(FeatureGroup):
... highlight=True)
"""

def __init__(self, geo_data, data=None, columns=None, key_on=None, # noqa
def __init__(self, geo_data, data=None, columns=None, key_on=None, # noqa
bins=6, fill_color='blue', nan_fill_color='black',
fill_opacity=0.6, nan_fill_opacity=None, line_color='black',
line_weight=1, line_opacity=1, name=None, legend_name='',
Expand Down Expand Up @@ -1197,6 +1266,7 @@ class ColorLine(FeatureGroup):
A ColorLine object that you can `add_to` a Map.

"""

def __init__(self, positions, colors, colormap=None, nb_steps=12,
weight=None, opacity=None, **kwargs):
super(ColorLine, self).__init__(**kwargs)
Expand Down
83 changes: 83 additions & 0 deletions tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,89 @@ def test_color_line():
m._repr_html_()


def test_get_vegalite_major_version():
spec_v2 = {'$schema': 'https://vega.github.io/schema/vega-lite/v2.6.0.json',
'config': {'view': {'height': 300, 'width': 400}},
'data': {'name': 'data-aac17e868e23f98b5e0830d45504be45'},
'datasets': {'data-aac17e868e23f98b5e0830d45504be45': [{'folium usage': 0,
'happiness': 1.0},
{'folium usage': 1,
'happiness': 2.718281828459045},
{'folium usage': 2,
'happiness': 7.38905609893065},
{'folium usage': 3,
'happiness': 20.085536923187668},
{'folium usage': 4,
'happiness': 54.598150033144236},
{'folium usage': 5,
'happiness': 148.4131591025766},
{'folium usage': 6,
'happiness': 403.4287934927351},
{'folium usage': 7,
'happiness': 1096.6331584284585},
{'folium usage': 8,
'happiness': 2980.9579870417283},
{'folium usage': 9,
'happiness': 8103.083927575384}]},
'encoding': {'x': {'field': 'folium usage', 'type': 'quantitative'},
'y': {'field': 'happiness', 'type': 'quantitative'}},
'mark': 'point'}

vegalite_v2 = folium.features.VegaLite(spec_v2)

assert vegalite_v2._get_vegalite_major_versions(spec_v2) == '2'

spec_v1 = {'$schema': 'https://vega.github.io/schema/vega-lite/v1.3.1.json',
'data': {'values': [{'folium usage': 0, 'happiness': 1.0},
{'folium usage': 1, 'happiness': 2.718281828459045},
{'folium usage': 2, 'happiness': 7.38905609893065},
{'folium usage': 3, 'happiness': 20.085536923187668},
{'folium usage': 4, 'happiness': 54.598150033144236},
{'folium usage': 5, 'happiness': 148.4131591025766},
{'folium usage': 6, 'happiness': 403.4287934927351},
{'folium usage': 7, 'happiness': 1096.6331584284585},
{'folium usage': 8, 'happiness': 2980.9579870417283},
{'folium usage': 9, 'happiness': 8103.083927575384}]},
'encoding': {'x': {'field': 'folium usage', 'type': 'quantitative'},
'y': {'field': 'happiness', 'type': 'quantitative'}},
'height': 300,
'mark': 'point',
'width': 400}

vegalite_v1 = folium.features.VegaLite(spec_v1)

assert vegalite_v1._get_vegalite_major_versions(spec_v1) == '1'

spec_no_version = {'config': {'view': {'height': 300, 'width': 400}},
'data': {'name': 'data-aac17e868e23f98b5e0830d45504be45'},
'datasets': {'data-aac17e868e23f98b5e0830d45504be45': [{'folium usage': 0,
'happiness': 1.0},
{'folium usage': 1,
'happiness': 2.718281828459045},
{'folium usage': 2,
'happiness': 7.38905609893065},
{'folium usage': 3,
'happiness': 20.085536923187668},
{'folium usage': 4,
'happiness': 54.598150033144236},
{'folium usage': 5,
'happiness': 148.4131591025766},
{'folium usage': 6,
'happiness': 403.4287934927351},
{'folium usage': 7,
'happiness': 1096.6331584284585},
{'folium usage': 8,
'happiness': 2980.9579870417283},
{'folium usage': 9,
'happiness': 8103.083927575384}]},
'encoding': {'x': {'field': 'folium usage', 'type': 'quantitative'},
'y': {'field': 'happiness', 'type': 'quantitative'}},
'mark': 'point'}

vegalite_no_version = folium.features.VegaLite(spec_no_version)

assert vegalite_no_version._get_vegalite_major_versions(spec_no_version) is None

# GeoJsonTooltip GeometryCollection
def test_geojson_tooltip():
m = folium.Map([30.5, -97.5], zoom_start=10)
Expand Down