Skip to content

Commit b3f31bb

Browse files
authored
Merge pull request #741 from ocefpaf/TimeDynamicGeoJson
Time dynamic geo json
2 parents 1053a02 + 1785246 commit b3f31bb

File tree

8 files changed

+883
-4
lines changed

8 files changed

+883
-4
lines changed

.travis.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# http://lint.travis-ci.org/
2-
31
language: python
42

53
sudo: false

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Internal re-factor to reflect leaflet's organization (ocefpaf #725)
88
- Added `tooltip` support to `Marker`s (ocefpaf #724)
99
- Added `tooltip` support to all vector layers (ocefpaf #722)
10+
- Added `TimeSliderChoropleth` plugin (halfdanrump #736)
1011

1112
API changes
1213

examples/TilesExample.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@
343343
"name": "python",
344344
"nbconvert_exporter": "python",
345345
"pygments_lexer": "ipython3",
346-
"version": "3.6.2"
346+
"version": "3.6.3"
347347
}
348348
},
349349
"nbformat": 4,

examples/TimeSliderChoropleth.ipynb

Lines changed: 640 additions & 0 deletions
Large diffs are not rendered by default.

folium/plugins/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from folium.plugins.polyline_text_path import PolyLineTextPath
2323
from folium.plugins.scroll_zoom_toggler import ScrollZoomToggler
2424
from folium.plugins.terminator import Terminator
25+
from folium.plugins.time_slider_choropleth import TimeSliderChoropleth
2526
from folium.plugins.timestamped_geo_json import TimestampedGeoJson
2627
from folium.plugins.timestamped_wmstilelayer import TimestampedWmsTileLayers
2728

@@ -38,6 +39,7 @@
3839
'PolyLineTextPath',
3940
'ScrollZoomToggler',
4041
'Terminator',
42+
'TimeSliderChoropleth',
4143
'TimestampedGeoJson',
4244
'TimestampedWmsTileLayers',
4345
]
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from __future__ import (absolute_import, division, print_function)
4+
5+
import json
6+
7+
from branca.element import Figure, JavascriptLink
8+
9+
from folium.features import GeoJson
10+
11+
from jinja2 import Template
12+
13+
14+
class TimeSliderChoropleth(GeoJson):
15+
"""
16+
Creates a TimeSliderChoropleth plugin to append into a map with Map.add_child.
17+
18+
Parameters
19+
----------
20+
data: str
21+
geojson string
22+
styledict: dict
23+
A dictionary where the keys are the geojson feature ids and the values are
24+
dicts of `{time: style_options_dict}`
25+
26+
"""
27+
def __init__(self, data, styledict, name=None, overlay=True, control=True, **kwargs):
28+
super(TimeSliderChoropleth, self).__init__(data, name=name, overlay=overlay, control=control)
29+
if not isinstance(styledict, dict):
30+
raise ValueError('styledict must be a dictionary, got {!r}'.format(styledict))
31+
for val in styledict.values():
32+
if not isinstance(val, dict):
33+
raise ValueError('Each item in styledict must be a dictionary, got {!r}'.format(val))
34+
35+
# Make set of timestamps.
36+
timestamps = set()
37+
for feature in styledict.values():
38+
timestamps.update(set(feature.keys()))
39+
timestamps = sorted(list(timestamps))
40+
41+
self.timestamps = json.dumps(timestamps)
42+
self.styledict = json.dumps(styledict, sort_keys=True, indent=2)
43+
44+
self._template = Template(u"""
45+
{% macro script(this, kwargs) %}
46+
47+
var timestamps = {{ this.timestamps }};
48+
var styledict = {{ this.styledict }};
49+
var current_timestamp = timestamps[0];
50+
51+
// insert time slider
52+
d3.select("body").insert("p", ":first-child").append("input")
53+
.attr("type", "range")
54+
.attr("width", "100px")
55+
.attr("min", 0)
56+
.attr("max", timestamps.length - 1)
57+
.attr("value", 0)
58+
.attr("id", "slider")
59+
.attr("step", "1")
60+
.style('align', 'center');
61+
62+
// insert time slider output BEFORE time slider (text on top of slider)
63+
d3.select("body").insert("p", ":first-child").append("output")
64+
.attr("width", "100")
65+
.attr("id", "slider-value")
66+
.style('font-size', '18px')
67+
.style('text-align', 'center')
68+
.style('font-weight', '500%');
69+
70+
var datestring = new Date(parseInt(current_timestamp)*1000).toDateString();
71+
d3.select("output#slider-value").text(datestring);
72+
73+
fill_map = function(){
74+
for (var feature_id in styledict){
75+
let style = styledict[feature_id]//[current_timestamp];
76+
var fillColor = 'white';
77+
var opacity = 0;
78+
if (current_timestamp in style){
79+
fillColor = style[current_timestamp]['color'];
80+
opacity = style[current_timestamp]['opacity'];
81+
d3.selectAll('#feature-'+feature_id
82+
).attr('fill', fillColor)
83+
.style('fill-opacity', opacity);
84+
}
85+
}
86+
}
87+
88+
d3.select("#slider").on("input", function() {
89+
current_timestamp = timestamps[this.value];
90+
var datestring = new Date(parseInt(current_timestamp)*1000).toDateString();
91+
d3.select("output#slider-value").text(datestring);
92+
fill_map();
93+
});
94+
95+
{% if this.highlight %}
96+
{{this.get_name()}}_onEachFeature = function onEachFeature(feature, layer) {
97+
layer.on({
98+
mouseout: function(e) {
99+
if (current_timestamp in styledict[e.target.feature.id]){
100+
var opacity = styledict[e.target.feature.id][current_timestamp]['opacity'];
101+
d3.selectAll('#feature-'+e.target.feature.id).style('fill-opacity', opacity);
102+
}
103+
},
104+
mouseover: function(e) {
105+
if (current_timestamp in styledict[e.target.feature.id]){
106+
d3.selectAll('#feature-'+e.target.feature.id).style('fill-opacity', 1);
107+
}
108+
},
109+
click: function(e) {
110+
{{this._parent.get_name()}}.fitBounds(e.target.getBounds());
111+
}
112+
});
113+
};
114+
115+
{% endif %}
116+
117+
var {{this.get_name()}} = L.geoJson(
118+
{% if this.embed %}{{this.style_data()}}{% else %}"{{this.data}}"{% endif %}
119+
{% if this.smooth_factor is not none or this.highlight %}
120+
, {
121+
{% if this.smooth_factor is not none %}
122+
smoothFactor:{{this.smooth_factor}}
123+
{% endif %}
124+
125+
{% if this.highlight %}
126+
{% if this.smooth_factor is not none %}
127+
,
128+
{% endif %}
129+
onEachFeature: {{this.get_name()}}_onEachFeature
130+
{% endif %}
131+
}
132+
{% endif %}
133+
).addTo({{this._parent.get_name()}}
134+
);
135+
136+
{{this.get_name()}}.setStyle(function(feature) {feature.properties.style;});
137+
138+
{{ this.get_name() }}.eachLayer(function (layer) {
139+
layer._path.id = 'feature-' + layer.feature.id;
140+
});
141+
142+
d3.selectAll('path')
143+
.attr('stroke', 'white')
144+
.attr('stroke-width', 0.8)
145+
.attr('stroke-dasharray', '5,5')
146+
.attr('fill-opacity', 0);
147+
fill_map();
148+
149+
{% endmacro %}
150+
""")
151+
152+
def render(self, **kwargs):
153+
super(TimeSliderChoropleth, self).render(**kwargs)
154+
figure = self.get_root()
155+
assert isinstance(figure, Figure), ('You cannot render this Element '
156+
'if it is not in a Figure.')
157+
figure.header.add_child(JavascriptLink('https://d3js.org/d3.v4.min.js'), name='d3v4')

tests/plugins/test_fast_marker_cluster.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
"""
44
Test FastMarkerCluster
5-
------------------
5+
----------------------
66
"""
77

88
from __future__ import (absolute_import, division, print_function)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
tests TimeSliderChoropleth
3+
--------------------------
4+
5+
"""
6+
7+
from __future__ import (absolute_import, division, print_function)
8+
9+
import json
10+
11+
from branca.colormap import linear
12+
13+
import folium
14+
from folium.plugins import TimeSliderChoropleth
15+
16+
import geopandas as gpd
17+
18+
import numpy as np
19+
20+
import pandas as pd
21+
22+
23+
def test_timedynamic_geo_json():
24+
"""
25+
tests folium.plugins.TimeSliderChoropleth
26+
"""
27+
assert 'naturalearth_lowres' in gpd.datasets.available
28+
datapath = gpd.datasets.get_path('naturalearth_lowres')
29+
gdf = gpd.read_file(datapath)
30+
31+
n_periods = 3
32+
dt_index = pd.date_range('2016-1-1', periods=n_periods, freq='M').strftime('%s')
33+
34+
styledata = {}
35+
36+
for country in gdf.index:
37+
pdf = pd.DataFrame(
38+
{'color': np.random.normal(size=n_periods),
39+
'opacity': np.random.normal(size=n_periods)},
40+
index=dt_index)
41+
styledata[country] = pdf.cumsum()
42+
43+
max_color, min_color = 0, 0
44+
45+
for country, data in styledata.items():
46+
max_color = max(max_color, data['color'].max())
47+
min_color = min(max_color, data['color'].min())
48+
49+
cmap = linear.PuRd.scale(min_color, max_color)
50+
51+
# Define function to normalize column into range [0,1]
52+
def norm(col):
53+
return (col - col.min())/(col.max()-col.min())
54+
55+
for country, data in styledata.items():
56+
data['color'] = data['color'].apply(cmap)
57+
data['opacity'] = norm(data['opacity'])
58+
59+
styledict = {str(country): data.to_dict(orient='index') for
60+
country, data in styledata.items()}
61+
62+
m = folium.Map((0, 0), tiles='Stamen Watercolor', zoom_start=2)
63+
64+
time_slider_choropleth = TimeSliderChoropleth(
65+
gdf.to_json(),
66+
styledict
67+
)
68+
time_slider_choropleth.add_to(m)
69+
70+
rendered = time_slider_choropleth._template.module.script(time_slider_choropleth)
71+
72+
m._repr_html_()
73+
out = m._parent.render()
74+
assert '<script src="https://d3js.org/d3.v4.min.js"></script>' in out
75+
76+
# We verify that data has been inserted correctly
77+
expected_timestamps = """var timestamps = ["1454198400", "1456704000", "1459382400"];""" # noqa
78+
assert expected_timestamps.split(';')[0].strip() == rendered.split(';')[0].strip()
79+
80+
expected_styledict = json.dumps(styledict, sort_keys=True, indent=2)
81+
assert expected_styledict in rendered

0 commit comments

Comments
 (0)