Skip to content

Time dynamic geo json #741

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
merged 35 commits into from
Oct 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8e28713
Patches map.py and feature.GeoJson to have a time slider
halfdanrump Jun 23, 2017
3b006f0
moves map.py and features.py to correct directory
halfdanrump Jun 26, 2017
724f83a
Sets mouseout opacity to 0.8
halfdanrump Jun 26, 2017
4b2565e
Create TimeDynamicGeoJson class and moves slider code from map.py
halfdanrump Jun 26, 2017
99acd56
Restores features.py to unmodified version before branching (commit 2…
halfdanrump Jun 26, 2017
0211cfe
TimeDynamicGeoJson can now be used to create a time dynamic choropleth
halfdanrump Jun 28, 2017
6233e23
Removes infodict
halfdanrump Aug 2, 2017
1a03f7f
Removes unused d3 imports
Sep 27, 2017
dbe24d8
Several bugfixes && adds output for dispaying current value of slider
Sep 27, 2017
57e9d11
adds import of TimeDynamicGeoJson in plugins package init
Sep 27, 2017
24de854
Adds TimeDynamicGeoJson to __all__
Sep 27, 2017
9987e09
Cleans up map.py
Sep 27, 2017
6be6733
Removes a few lines spaces for the purpose of intense beauty
Sep 27, 2017
491c188
Fixes several linter warnings
halfdanrump Sep 28, 2017
3a26638
removed empty line whitespaces
halfdanrump Sep 28, 2017
4e631f7
Changes import from relative to absolute
Oct 3, 2017
a00c311
TimeDynamicGeoJson now inherets from GeoJson instead of Layer
Oct 3, 2017
32e1409
Adds notebook demonstrating the use of the TimeDynamicGeoJson plugin
Oct 3, 2017
9d69e7f
Adds assertions for checking styledict
Oct 3, 2017
a164ea0
Moves d3 import to plugin
Oct 4, 2017
5a99226
Reverts map.py and plugins/__init__.py to 2171e11f1b75313b69d642c99b8…
Oct 4, 2017
b717780
Merge remote-tracking branch 'upstream/master'
Oct 4, 2017
d598e51
Adds plugin import in init file
Oct 4, 2017
0ffcdfa
Updates changelog
Oct 4, 2017
247496b
removes unused imports and minor changes to make pylint happy
Oct 4, 2017
dd32b63
Adds tests for TimeDynamicGeoJson
Oct 4, 2017
0549185
Uses https to fetch d3 script
Oct 4, 2017
933609e
Removes output to .html file
Oct 4, 2017
854755d
minor changes and pylint pleasing
Oct 4, 2017
d1ff191
Temporarily removes some assertions to verify that test passes on CI …
Oct 5, 2017
7bfabf3
Puts back in assertion of styledict
Oct 5, 2017
4601883
rename TimeDynamicGeoJson to TimeSliderChoropleth and fix some minor …
ocefpaf Oct 5, 2017
8af9205
remove outdated link
ocefpaf Oct 22, 2017
dda6884
fix lints
ocefpaf Oct 23, 2017
1785246
docstring
ocefpaf Oct 24, 2017
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
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# http://lint.travis-ci.org/

language: python

sudo: false
Expand Down
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Internal re-factor to reflect leaflet's organization (ocefpaf #725)
- Added `tooltip` support to `Marker`s (ocefpaf #724)
- Added `tooltip` support to all vector layers (ocefpaf #722)
- Added `TimeSliderChoropleth` plugin (halfdanrump #736)

API changes

Expand Down
2 changes: 1 addition & 1 deletion examples/TilesExample.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.2"
"version": "3.6.3"
}
},
"nbformat": 4,
Expand Down
640 changes: 640 additions & 0 deletions examples/TimeSliderChoropleth.ipynb

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions folium/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from folium.plugins.polyline_text_path import PolyLineTextPath
from folium.plugins.scroll_zoom_toggler import ScrollZoomToggler
from folium.plugins.terminator import Terminator
from folium.plugins.time_slider_choropleth import TimeSliderChoropleth
from folium.plugins.timestamped_geo_json import TimestampedGeoJson
from folium.plugins.timestamped_wmstilelayer import TimestampedWmsTileLayers

Expand All @@ -38,6 +39,7 @@
'PolyLineTextPath',
'ScrollZoomToggler',
'Terminator',
'TimeSliderChoropleth',
'TimestampedGeoJson',
'TimestampedWmsTileLayers',
]
157 changes: 157 additions & 0 deletions folium/plugins/time_slider_choropleth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-

from __future__ import (absolute_import, division, print_function)

import json

from branca.element import Figure, JavascriptLink

from folium.features import GeoJson

from jinja2 import Template


class TimeSliderChoropleth(GeoJson):
"""
Creates a TimeSliderChoropleth plugin to append into a map with Map.add_child.

Parameters
----------
data: str
geojson string
styledict: dict
A dictionary where the keys are the geojson feature ids and the values are
dicts of `{time: style_options_dict}`

"""
def __init__(self, data, styledict, name=None, overlay=True, control=True, **kwargs):
super(TimeSliderChoropleth, self).__init__(data, name=name, overlay=overlay, control=control)
if not isinstance(styledict, dict):
raise ValueError('styledict must be a dictionary, got {!r}'.format(styledict))
for val in styledict.values():
if not isinstance(val, dict):
raise ValueError('Each item in styledict must be a dictionary, got {!r}'.format(val))

# Make set of timestamps.
timestamps = set()
for feature in styledict.values():
timestamps.update(set(feature.keys()))
timestamps = sorted(list(timestamps))

self.timestamps = json.dumps(timestamps)
self.styledict = json.dumps(styledict, sort_keys=True, indent=2)

self._template = Template(u"""
{% macro script(this, kwargs) %}

var timestamps = {{ this.timestamps }};
var styledict = {{ this.styledict }};
var current_timestamp = timestamps[0];

// insert time slider
d3.select("body").insert("p", ":first-child").append("input")
.attr("type", "range")
.attr("width", "100px")
.attr("min", 0)
.attr("max", timestamps.length - 1)
.attr("value", 0)
.attr("id", "slider")
.attr("step", "1")
.style('align', 'center');

// insert time slider output BEFORE time slider (text on top of slider)
d3.select("body").insert("p", ":first-child").append("output")
.attr("width", "100")
.attr("id", "slider-value")
.style('font-size', '18px')
.style('text-align', 'center')
.style('font-weight', '500%');

var datestring = new Date(parseInt(current_timestamp)*1000).toDateString();
d3.select("output#slider-value").text(datestring);

fill_map = function(){
for (var feature_id in styledict){
let style = styledict[feature_id]//[current_timestamp];
var fillColor = 'white';
var opacity = 0;
if (current_timestamp in style){
fillColor = style[current_timestamp]['color'];
opacity = style[current_timestamp]['opacity'];
d3.selectAll('#feature-'+feature_id
).attr('fill', fillColor)
.style('fill-opacity', opacity);
}
}
}

d3.select("#slider").on("input", function() {
current_timestamp = timestamps[this.value];
var datestring = new Date(parseInt(current_timestamp)*1000).toDateString();
d3.select("output#slider-value").text(datestring);
fill_map();
});

{% if this.highlight %}
{{this.get_name()}}_onEachFeature = function onEachFeature(feature, layer) {
layer.on({
mouseout: function(e) {
if (current_timestamp in styledict[e.target.feature.id]){
var opacity = styledict[e.target.feature.id][current_timestamp]['opacity'];
d3.selectAll('#feature-'+e.target.feature.id).style('fill-opacity', opacity);
}
},
mouseover: function(e) {
if (current_timestamp in styledict[e.target.feature.id]){
d3.selectAll('#feature-'+e.target.feature.id).style('fill-opacity', 1);
}
},
click: function(e) {
{{this._parent.get_name()}}.fitBounds(e.target.getBounds());
}
});
};

{% endif %}

var {{this.get_name()}} = L.geoJson(
{% if this.embed %}{{this.style_data()}}{% else %}"{{this.data}}"{% endif %}
{% if this.smooth_factor is not none or this.highlight %}
, {
{% if this.smooth_factor is not none %}
smoothFactor:{{this.smooth_factor}}
{% endif %}

{% if this.highlight %}
{% if this.smooth_factor is not none %}
,
{% endif %}
onEachFeature: {{this.get_name()}}_onEachFeature
{% endif %}
}
{% endif %}
).addTo({{this._parent.get_name()}}
);

{{this.get_name()}}.setStyle(function(feature) {feature.properties.style;});

{{ this.get_name() }}.eachLayer(function (layer) {
layer._path.id = 'feature-' + layer.feature.id;
});

d3.selectAll('path')
.attr('stroke', 'white')
.attr('stroke-width', 0.8)
.attr('stroke-dasharray', '5,5')
.attr('fill-opacity', 0);
fill_map();

{% endmacro %}
""")

def render(self, **kwargs):
super(TimeSliderChoropleth, self).render(**kwargs)
figure = self.get_root()
assert isinstance(figure, Figure), ('You cannot render this Element '
'if it is not in a Figure.')
figure.header.add_child(JavascriptLink('https://d3js.org/d3.v4.min.js'), name='d3v4')
2 changes: 1 addition & 1 deletion tests/plugins/test_fast_marker_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

"""
Test FastMarkerCluster
------------------
----------------------
"""

from __future__ import (absolute_import, division, print_function)
Expand Down
81 changes: 81 additions & 0 deletions tests/plugins/test_time_slider_choropleth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""
tests TimeSliderChoropleth
--------------------------

"""

from __future__ import (absolute_import, division, print_function)

import json

from branca.colormap import linear

import folium
from folium.plugins import TimeSliderChoropleth

import geopandas as gpd

import numpy as np

import pandas as pd


def test_timedynamic_geo_json():
"""
tests folium.plugins.TimeSliderChoropleth
"""
assert 'naturalearth_lowres' in gpd.datasets.available
datapath = gpd.datasets.get_path('naturalearth_lowres')
gdf = gpd.read_file(datapath)

n_periods = 3
dt_index = pd.date_range('2016-1-1', periods=n_periods, freq='M').strftime('%s')

styledata = {}

for country in gdf.index:
pdf = pd.DataFrame(
{'color': np.random.normal(size=n_periods),
'opacity': np.random.normal(size=n_periods)},
index=dt_index)
styledata[country] = pdf.cumsum()

max_color, min_color = 0, 0

for country, data in styledata.items():
max_color = max(max_color, data['color'].max())
min_color = min(max_color, data['color'].min())

cmap = linear.PuRd.scale(min_color, max_color)

# Define function to normalize column into range [0,1]
def norm(col):
return (col - col.min())/(col.max()-col.min())

for country, data in styledata.items():
data['color'] = data['color'].apply(cmap)
data['opacity'] = norm(data['opacity'])

styledict = {str(country): data.to_dict(orient='index') for
country, data in styledata.items()}

m = folium.Map((0, 0), tiles='Stamen Watercolor', zoom_start=2)

time_slider_choropleth = TimeSliderChoropleth(
gdf.to_json(),
styledict
)
time_slider_choropleth.add_to(m)

rendered = time_slider_choropleth._template.module.script(time_slider_choropleth)

m._repr_html_()
out = m._parent.render()
assert '<script src="https://d3js.org/d3.v4.min.js"></script>' in out

# We verify that data has been inserted correctly
expected_timestamps = """var timestamps = ["1454198400", "1456704000", "1459382400"];""" # noqa
assert expected_timestamps.split(';')[0].strip() == rendered.split(';')[0].strip()

expected_styledict = json.dumps(styledict, sort_keys=True, indent=2)
assert expected_styledict in rendered