Skip to content

TimeDynamicGeoJson plugin #736

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 31 commits into from
Oct 24, 2017
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
31 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
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
7 changes: 7 additions & 0 deletions folium/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@

ENV = Environment(loader=PackageLoader('folium', 'templates'))

_d3_js = [
('d3', 'http://d3js.org/d3.v4.min.js'),
Copy link
Member

Choose a reason for hiding this comment

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

This should be in the plugin and not in the global map.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright.

]

_default_js = [
('leaflet',
'https://unpkg.com/[email protected]/dist/leaflet.js'),
Expand All @@ -37,6 +41,9 @@
'https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.0.0/leaflet.markercluster.js'), # noqa
]

for js in _d3_js:
_default_js.append(js)

_default_css = [
('leaflet_css',
'https://unpkg.com/[email protected]/dist/leaflet.css'),
Expand Down
2 changes: 2 additions & 0 deletions folium/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .terminator import Terminator
from .boat_marker import BoatMarker
from .timestamped_geo_json import TimestampedGeoJson
from .timedynamic_geo_json import TimeDynamicGeoJson
from .heat_map import HeatMap
from .image_overlay import ImageOverlay
from .fullscreen import Fullscreen
Expand All @@ -23,6 +24,7 @@
'Terminator',
'BoatMarker',
'TimestampedGeoJson',
'TimeDynamicGeoJson',
'HeatMap',
'ImageOverlay',
'Fullscreen',
Expand Down
257 changes: 257 additions & 0 deletions folium/plugins/timedynamic_geo_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import json
from ..map import Layer
Copy link
Member

Choose a reason for hiding this comment

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

Please use absolute imports to keep it consistent with the rest of the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK

from jinja2 import Template
from branca.utilities import none_min, none_max, iter_points
Copy link
Member

Choose a reason for hiding this comment

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

These are now in folium.utilities.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it.

from six import text_type, binary_type


class TimeDynamicGeoJson(Layer):
"""
Creates a GeoJson object for plotting into a Map.

Parameters
----------
data: file, dict or str.
The GeoJSON data you want to plot.
* If file, then data will be read in the file and fully
embedded in Leaflet's JavaScript.
* If dict, then data will be converted to JSON and embedded
in the JavaScript.
* If str, then data will be passed to the JavaScript as-is.
style_function: function, default None
A function mapping a GeoJson Feature to a style dict.
name : string, default None
The name of the Layer, as it will appear in LayerControls
overlay : bool, default False
Adds the layer as an optional overlay (True) or the base layer (False).
control : bool, default True
Whether the Layer will be included in LayerControls
smooth_factor: float, default None
How much to simplify the polyline on each zoom level. More means
better performance and smoother look, and less means more accurate
representation. Leaflet defaults to 1.0.
Copy link
Member

Choose a reason for hiding this comment

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

You can probably remove all that, refer to the GeoJson docs, and add only what is different about this plugin.


Examples
--------
>>> # Providing file that shall be embedded.
>>> GeoJson(open('foo.json'))
>>> # Providing filename that shall not be embedded.
>>> GeoJson('foo.json')
>>> # Providing dict.
>>> GeoJson(json.load(open('foo.json')))
>>> # Providing string.
>>> GeoJson(open('foo.json').read())

>>> # Provide a style_function that color all states green but Alabama.
>>> style_function = lambda x: {'fillColor': '#0000ff' if
... x['properties']['name']=='Alabama' else
... '#00ff00'}
>>> GeoJson(geojson, style_function=style_function)

"""
def __init__(self, data, styledict, style_function=None, name=None,

Choose a reason for hiding this comment

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

C901 'TimeDynamicGeoJson.init' is too complex (13)

overlay=True, control=True, smooth_factor=None,
highlight_function=None):
super(TimeDynamicGeoJson, self).__init__(name=name, overlay=overlay,
control=control)

Choose a reason for hiding this comment

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

E128 continuation line under-indented for visual indent

self._name = 'GeoJson'
if hasattr(data, 'read'):
self.embed = True
self.data = json.load(data)
elif isinstance(data, dict):
self.embed = True
self.data = data
elif isinstance(data, text_type) or isinstance(data, binary_type):
if data.lstrip()[0] in '[{': # This is a GeoJSON inline string
self.embed = True
self.data = json.loads(data)
else: # This is a filename
self.embed = False
self.data = data
elif data.__class__.__name__ in ['GeoDataFrame', 'GeoSeries']:
self.embed = True
if hasattr(data, '__geo_interface__'):
# We have a GeoPandas 0.2 object.
self.data = json.loads(json.dumps(data.to_crs(epsg='4326').__geo_interface__)) # noqa
elif hasattr(data, 'columns'):
# We have a GeoDataFrame 0.1
self.data = json.loads(data.to_crs(epsg='4326').to_json())
else:
msg = 'Unable to transform this object to a GeoJSON.'
raise ValueError(msg)
else:
raise ValueError('Unhandled object {!r}.'.format(data))

self.styledict = styledict

Choose a reason for hiding this comment

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

E303 too many blank lines (2)


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

if style_function is None:
def style_function(x):
return {}

self.style_function = style_function

self.highlight = highlight_function is not None

if highlight_function is None:
def highlight_function(x):
return {}

self.highlight_function = highlight_function

self.smooth_factor = smooth_factor

self._template = Template(u"""
Copy link
Member

Choose a reason for hiding this comment

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

You can probably inherit from the GeoJson class and save a lot of the duplication by only changing the template.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ocefpaf Good idea!

{% macro script(this, kwargs) %}

Choose a reason for hiding this comment

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

W293 blank line contains whitespace

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")

Choose a reason for hiding this comment

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

W291 trailing whitespace

.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")

Choose a reason for hiding this comment

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

W291 trailing whitespace

.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();
});

Choose a reason for hiding this comment

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

W291 trailing whitespace


{% if this.highlight %}
Copy link
Member

Choose a reason for hiding this comment

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

Note to self: unfortunately this repetition is needed b/c of the current design we cannot extend the jinja template.

{{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()}}
);

Choose a reason for hiding this comment

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

W293 blank line contains whitespace

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

Choose a reason for hiding this comment

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

E101 indentation contains mixed spaces and tabs
W191 indentation contains tabs

Choose a reason for hiding this comment

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

E101 indentation contains mixed spaces and tabs
W191 indentation contains tabs

Copy link
Member

Choose a reason for hiding this comment

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

Mix of space and tabs are a no-no. Can you try to find the tabs and make everything spaces?


Choose a reason for hiding this comment

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

W293 blank line contains whitespace

{{ 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);

Choose a reason for hiding this comment

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

E501 line too long (143 > 120 characters)

fill_map();

Choose a reason for hiding this comment

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

W293 blank line contains whitespace

{% endmacro %}
""") # noqa

def style_data(self):
"""
Applies `self.style_function` to each feature of `self.data` and
returns a corresponding JSON output.
"""
if 'features' not in self.data.keys():
# Catch case when GeoJSON is just a single Feature or a geometry.
if not (isinstance(self.data, dict) and 'geometry' in self.data.keys()): # noqa
# Catch case when GeoJSON is just a geometry.
self.data = {'type': 'Feature', 'geometry': self.data}
self.data = {'type': 'FeatureCollection', 'features': [self.data]}

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
return json.dumps(self.data, sort_keys=True)

def _get_self_bounds(self):
"""
Computes the bounds of the object itself (not including it's children)
in the form [[lat_min, lon_min], [lat_max, lon_max]]

"""
if not self.embed:
raise ValueError('Cannot compute bounds of non-embedded GeoJSON.')

if 'features' not in self.data.keys():
# Catch case when GeoJSON is just a single Feature or a geometry.
if not (isinstance(self.data, dict) and 'geometry' in self.data.keys()): # noqa
# Catch case when GeoJSON is just a geometry.
self.data = {'type': 'Feature', 'geometry': self.data}
self.data = {'type': 'FeatureCollection', 'features': [self.data]}

bounds = [[None, None], [None, None]]
for feature in self.data['features']:
for point in iter_points(feature.get('geometry', {}).get('coordinates', {})): # noqa
bounds = [
[
none_min(bounds[0][0], point[1]),
none_min(bounds[0][1], point[0]),
],
[
none_max(bounds[1][0], point[1]),
none_max(bounds[1][1], point[0]),
],
]
return bounds