Skip to content

Commit 4431da8

Browse files
authored
Multiple instances of TimeSliderChoropleth on a single map (#1749)
* TimeSliderChoropleth: allow multiple * fix behavior for show=False * restore highlight option * fix mypy * better docstring * update test
1 parent eba0fae commit 4431da8

2 files changed

Lines changed: 56 additions & 39 deletions

File tree

folium/plugins/time_slider_choropleth.py

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77

88
class TimeSliderChoropleth(JSCSSMixin, Layer):
99
"""
10-
Creates a TimeSliderChoropleth plugin to append into a map with Map.add_child.
10+
Create a choropleth with a timeslider for timestamped data.
11+
12+
Visualize timestamped data, allowing users to view the choropleth at
13+
different timestamps using a slider.
1114
1215
Parameters
1316
----------
@@ -16,6 +19,8 @@ class TimeSliderChoropleth(JSCSSMixin, Layer):
1619
styledict: dict
1720
A dictionary where the keys are the geojson feature ids and the values are
1821
dicts of `{time: style_options_dict}`
22+
highlight: bool, default False
23+
Whether to show a visual effect on mouse hover and click.
1924
name : string, default None
2025
The name of the Layer, as it will appear in LayerControls.
2126
overlay : bool, default False
@@ -34,65 +39,69 @@ class TimeSliderChoropleth(JSCSSMixin, Layer):
3439
_template = Template(
3540
"""
3641
{% macro script(this, kwargs) %}
37-
var timestamps = {{ this.timestamps|tojson }};
38-
var styledict = {{ this.styledict|tojson }};
39-
var current_timestamp = timestamps[{{ this.init_timestamp }}];
42+
{
43+
let timestamps = {{ this.timestamps|tojson }};
44+
let styledict = {{ this.styledict|tojson }};
45+
let current_timestamp = timestamps[{{ this.init_timestamp }}];
46+
47+
let slider_body = d3.select("body").insert("div", "div.folium-map")
48+
.attr("id", "slider_{{ this.get_name() }}");
49+
$("#slider_{{ this.get_name() }}").hide();
50+
// insert time slider label
51+
slider_body.append("output")
52+
.attr("width", "100")
53+
.style('font-size', '18px')
54+
.style('text-align', 'center')
55+
.style('font-weight', '500%')
56+
.style('margin', '5px');
4057
// insert time slider
41-
d3.select("body").insert("p", ":first-child").append("input")
58+
slider_body.append("input")
4259
.attr("type", "range")
4360
.attr("width", "100px")
4461
.attr("min", 0)
4562
.attr("max", timestamps.length - 1)
4663
.attr("value", {{ this.init_timestamp }})
47-
.attr("id", "slider")
4864
.attr("step", "1")
4965
.style('align', 'center');
5066
51-
// insert time slider output BEFORE time slider (text on top of slider)
52-
d3.select("body").insert("p", ":first-child").append("output")
53-
.attr("width", "100")
54-
.attr("id", "slider-value")
55-
.style('font-size', '18px')
56-
.style('text-align', 'center')
57-
.style('font-weight', '500%');
58-
59-
var datestring = new Date(parseInt(current_timestamp)*1000).toDateString();
60-
d3.select("output#slider-value").text(datestring);
67+
let datestring = new Date(parseInt(current_timestamp)*1000).toDateString();
68+
d3.select("#slider_{{ this.get_name() }} > output").text(datestring);
6169
62-
fill_map = function(){
70+
let fill_map = function(){
6371
for (var feature_id in styledict){
6472
let style = styledict[feature_id]//[current_timestamp];
6573
var fillColor = 'white';
6674
var opacity = 0;
6775
if (current_timestamp in style){
6876
fillColor = style[current_timestamp]['color'];
6977
opacity = style[current_timestamp]['opacity'];
70-
d3.selectAll('#feature-'+feature_id
78+
d3.selectAll('#{{ this.get_name() }}-feature-'+feature_id
7179
).attr('fill', fillColor)
7280
.style('fill-opacity', opacity);
7381
}
7482
}
7583
}
7684
77-
d3.select("#slider").on("input", function() {
85+
d3.select("#slider_{{ this.get_name() }} > input").on("input", function() {
7886
current_timestamp = timestamps[this.value];
7987
var datestring = new Date(parseInt(current_timestamp)*1000).toDateString();
80-
d3.select("output#slider-value").text(datestring);
88+
d3.select("#slider_{{ this.get_name() }} > output").text(datestring);
8189
fill_map();
8290
});
8391
92+
let onEachFeature;
8493
{% if this.highlight %}
85-
{{this.get_name()}}_onEachFeature = function onEachFeature(feature, layer) {
94+
onEachFeature = function(feature, layer) {
8695
layer.on({
8796
mouseout: function(e) {
8897
if (current_timestamp in styledict[e.target.feature.id]){
8998
var opacity = styledict[e.target.feature.id][current_timestamp]['opacity'];
90-
d3.selectAll('#feature-'+e.target.feature.id).style('fill-opacity', opacity);
99+
d3.selectAll('#{{ this.get_name() }}-feature-'+e.target.feature.id).style('fill-opacity', opacity);
91100
}
92101
},
93102
mouseover: function(e) {
94103
if (current_timestamp in styledict[e.target.feature.id]){
95-
d3.selectAll('#feature-'+e.target.feature.id).style('fill-opacity', 1);
104+
d3.selectAll('#{{ this.get_name() }}-feature-'+e.target.feature.id).style('fill-opacity', 1);
96105
}
97106
},
98107
click: function(e) {
@@ -103,8 +112,9 @@ class TimeSliderChoropleth(JSCSSMixin, Layer):
103112
{% endif %}
104113
105114
var {{ this.get_name() }} = L.geoJson(
106-
{{ this.data|tojson }}
107-
).addTo({{ this._parent.get_name() }});
115+
{{ this.data|tojson }},
116+
{onEachFeature: onEachFeature}
117+
);
108118
109119
{{ this.get_name() }}.setStyle(function(feature) {
110120
if (feature.properties.style !== undefined){
@@ -115,11 +125,13 @@ class TimeSliderChoropleth(JSCSSMixin, Layer):
115125
}
116126
});
117127
118-
function onOverlayAdd(e) {
128+
let onOverlayAdd = function(e) {
119129
{{ this.get_name() }}.eachLayer(function (layer) {
120-
layer._path.id = 'feature-' + layer.feature.id;
130+
layer._path.id = '{{ this.get_name() }}-feature-' + layer.feature.id;
121131
});
122132
133+
$("#slider_{{ this.get_name() }}").show();
134+
123135
d3.selectAll('path')
124136
.attr('stroke', 'white')
125137
.attr('stroke-width', 0.8)
@@ -128,13 +140,16 @@ class TimeSliderChoropleth(JSCSSMixin, Layer):
128140
129141
fill_map();
130142
}
131-
{{ this._parent.get_name() }}.on('overlayadd', onOverlayAdd);
132-
133-
onOverlayAdd(); // fill map as layer is loaded
134-
135-
{%- if not this.show %}
136-
{{ this.get_name() }}.remove();
143+
{{ this.get_name() }}.on('add', onOverlayAdd);
144+
{{ this.get_name() }}.on('remove', function() {
145+
$("#slider_{{ this.get_name() }}").hide();
146+
})
147+
148+
{%- if this.show %}
149+
{{ this.get_name() }}.addTo({{ this._parent.get_name() }});
150+
$("#slider_{{ this.get_name() }}").show();
137151
{%- endif %}
152+
}
138153
{% endmacro %}
139154
"""
140155
)
@@ -145,6 +160,7 @@ def __init__(
145160
self,
146161
data,
147162
styledict,
163+
highlight: bool = False,
148164
name=None,
149165
overlay=True,
150166
control=True,
@@ -153,6 +169,7 @@ def __init__(
153169
):
154170
super().__init__(name=name, overlay=overlay, control=control, show=show)
155171
self.data = GeoJson.process_data(GeoJson({}), data)
172+
self.highlight = highlight
156173

157174
if not isinstance(styledict, dict):
158175
raise ValueError(
@@ -165,13 +182,13 @@ def __init__(
165182
) # noqa
166183

167184
# Make set of timestamps.
168-
timestamps = set()
185+
timestamps_set = set()
169186
for feature in styledict.values():
170-
timestamps.update(set(feature.keys()))
187+
timestamps_set.update(set(feature.keys()))
171188
try:
172-
timestamps = sorted(timestamps, key=int)
189+
timestamps = sorted(timestamps_set, key=int)
173190
except (TypeError, ValueError):
174-
timestamps = sorted(timestamps)
191+
timestamps = sorted(timestamps_set)
175192

176193
self.timestamps = timestamps
177194
self.styledict = styledict

tests/plugins/test_time_slider_choropleth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ def norm(col):
8181

8282
# We verify that data has been inserted correctly
8383
expected_timestamps = sorted(dt_index, key=int) # numeric sort
84-
expected_timestamps = f"var timestamps = {expected_timestamps};"
84+
expected_timestamps = f"let timestamps = {expected_timestamps};"
8585
expected_timestamps = expected_timestamps.split(";")[0].strip().replace("'", '"')
86-
rendered_timestamps = rendered.split(";")[0].strip()
86+
rendered_timestamps = rendered.strip(" \n{").split(";")[0].strip()
8787
assert expected_timestamps == rendered_timestamps
8888

8989
expected_styledict = normalize(json.dumps(styledict, sort_keys=True))

0 commit comments

Comments
 (0)