Skip to content
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

Adding popup_attribute to the GeoJson feature #376

Closed
wants to merge 10 commits into from
309 changes: 208 additions & 101 deletions examples/GeoJSON and choropleth.ipynb

Large diffs are not rendered by default.

148 changes: 117 additions & 31 deletions examples/GeoPandas.ipynb

Large diffs are not rendered by default.

71 changes: 68 additions & 3 deletions folium/features.py
Expand Up @@ -5,6 +5,8 @@

Extra features Elements.
"""
from types import FunctionType

from jinja2 import Template
import json

Expand Down Expand Up @@ -267,6 +269,11 @@ class GeoJson(Layer):
* 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.
popup_function: string or function, default None
The popup value for the feature
* If string, then an attribute to use as a popup value for the feature
* If function, a function taking a GeoJson Feature and returning a
html string to be used as the popup
name : string, default None
The name of the Layer, as it will appear in LayerControls
overlay : bool, default False
Expand All @@ -290,9 +297,17 @@ class GeoJson(Layer):
... x['properties']['name']=='Alabama' else
... '#00ff00'}
>>> GeoJson(geojson, style_function=style_function)

>>> # Provide a popup using the name attribute of each feature.
>>> GeoJson(geojson, popup_function='name')

>>> # Provide a popup based on the output of a function on each feature
>>> GeoJson(
... geojson,
... popup_function=lambda feautre: feautre['properties']['name'])
"""
def __init__(self, data, style_function=None, name=None,
overlay=True, control=True):
def __init__(self, data, style_function=None, popup_function=None,
name=None, overlay=True, control=True):
super(GeoJson, self).__init__(name=name, overlay=overlay,
control=control)
self._name = 'GeoJson'
Expand Down Expand Up @@ -328,15 +343,58 @@ def style_function(x):
return {}
self.style_function = style_function

self._popup_function = None
self.popup_attribute = None
self.popup_function = popup_function # Set the popup function

self._template = Template(u"""
{% macro script(this, kwargs) %}
{% if this.popup_attribute or this.popup_function %}
function {{this.get_name()}}_EachFeature(feature, layer) {
if (feature.properties && feature.properties.{{this.popup_attribute}}) {
layer.bindPopup(feature.properties.{{this.popup_attribute}});
}
}
{% endif %}
var {{this.get_name()}} = L.geoJson(
{% if this.embed %}{{this.style_data()}}{% else %}"{{this.data}}"{% endif %})
{% if this.embed %}{{this.style_data()}}{% else %}"{{this.data}}"{% endif %},
{{'{'}}
{% if this.popup_attribute or this.popup_function %}
onEachFeature: {{this.get_name()}}_EachFeature
{% endif %}
{{'}'}}
)
.addTo({{this._parent.get_name()}});
{{this.get_name()}}.setStyle(function(feature) {return feature.properties.style;});
{% endmacro %}
""") # noqa

@property
def popup_function(self):
return self._popup_function

@popup_function.setter
def popup_function(self, func):
"""
Use a setter so that if the dev sets the property function after the
fact the `popup_attribute` is set correctly.

Parameters
----------
func : srting, function or None
The popup value for the feature
* If string, then an attribute to use as a popup value for the feature

Choose a reason for hiding this comment

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

E501 line too long (82 > 79 characters)

* If function, a function taking a GeoJson Feature and returning a
html string to be used as the popup
"""
if isinstance(func, FunctionType):
self.popup_attribute = "_popupContent"
elif isinstance(func, text_type):
self.popup_attribute = func
elif isinstance(func, binary_type):
self.popup_attribute = text_type(func, 'utf8')
self._popup_function = func

Copy link
Contributor

Choose a reason for hiding this comment

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

This is great, but it seems complicated to a dummy programmer like me.
Can we simplify this in having a single attibute popup (playing the role of both popup_function and popup_attribute and in testing in the rendering whether it's a function or a string ?
It's a question (I try to keep the code as simple as possible) but your PR is ok anyway.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, no reason not to. I'll update the code. It's easy to move the check into the render function.

def style_data(self):
"""
Applies `self.style_function` to each feature of `self.data` and
Expand All @@ -352,6 +410,13 @@ def style_data(self):

for feature in self.data['features']:
feature.setdefault('properties', {}).setdefault('style', {}).update(self.style_function(feature)) # noqa

try:
for feature in self.data["features"]:
feature["properties"]["_popupContent"] = self.popup_function(feature) # noqa
except TypeError:
pass

Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need a try ?

Copy link
Author

Choose a reason for hiding this comment

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

Since the checks on the function are outside the render operation, I was being a bit slack and letting the function fail when it wasn't a function. I'll move checks inside the loop.

return json.dumps(self.data, sort_keys=True)

def _get_self_bounds(self):
Expand Down
9 changes: 7 additions & 2 deletions folium/folium.py
Expand Up @@ -510,7 +510,7 @@ def choropleth(self, geo_path=None, geo_str=None, data_out='data.json',
data=None, columns=None, key_on=None, threshold_scale=None,
fill_color='blue', fill_opacity=0.6, line_color='black',
line_weight=1, line_opacity=1, legend_name="",
topojson=None, reset=False):
topojson=None, popup_function=None, reset=False):
"""
Apply a GeoJSON overlay to the map.

Expand Down Expand Up @@ -576,6 +576,10 @@ def choropleth(self, geo_path=None, geo_str=None, data_out='data.json',
topojson: string, default None
If using a TopoJSON, passing "objects.yourfeature" to the topojson
keyword argument will enable conversion to GeoJSON.
popup_function: string or function, default None
If using GeoJson a string or function to pass through to the
map to create popups based on feature attributes. Ignored for
topojson.
reset: boolean, default False
Remove all current geoJSON layers, start with new layer

Expand Down Expand Up @@ -677,7 +681,8 @@ def style_function(x):
if topojson:
geo_json = TopoJson(geo_data, topojson, style_function=style_function) # noqa
else:
geo_json = GeoJson(geo_data, style_function=style_function)
geo_json = GeoJson(geo_data, style_function=style_function,
popup_function=popup_function)

self.add_children(geo_json)

Expand Down
91 changes: 89 additions & 2 deletions tests/test_folium.py
Expand Up @@ -370,12 +370,16 @@ def test_geo_json_str(self):

# Verify the geo_json object
obj_temp = jinja2.Template("""
var {{ this.get_name() }} = L.geoJson({{ this.style_data() }})
var {{ this.get_name() }} = L.geoJson(
{{ this.style_data() }},
{{'{'}}
{{'}'}}
)
.addTo({{ this._parent.get_name() }});
{{ this.get_name() }}.setStyle(function(feature) {return feature.properties.style;});
""") # noqa
obj = obj_temp.render(this=geo_json, json=json)
assert ''.join(obj.split())[:-1] in out
assert ''.join(obj.split())[:-1] in out, out

bounds = self.map.get_bounds()
assert bounds == [[18.948267, -171.742517],
Expand Down Expand Up @@ -452,6 +456,89 @@ def test_geo_json_data_binding(self):
assert bounds == [[18.948267, -171.742517],
[71.285909, -66.979601]], bounds

def test_geo_json_popup(self):
"""Test geojson popup as attribute name."""

path = os.path.join(rootpath, 'us-counties.json')
data = json.load(open(path))
gj = folium.GeoJson(data, popup_function='name')

self.map = folium.Map([43, -100], zoom_start=4)
gj.add_to(self.map)
self.map._parent.render()

assert gj.popup_attribute == 'name', gj.popup_attribute

bounds = self.map.get_bounds()
assert bounds == [[18.948267, -171.742517],
[71.285909, -66.979601]], bounds

def test_geo_json_popup_function(self):
"""Test geojson popup as function."""

path = os.path.join(rootpath, 'us-counties.json')
data = json.load(open(path))
gj = folium.GeoJson(
data,
popup_function=lambda feature: feature['properties']['name']
)

self.map = folium.Map([43, -100], zoom_start=4)
gj.add_to(self.map)
self.map._parent.render()

assert gj.popup_attribute == '_popupContent', gj.popup_attribute

# Test each feature to guarantee popupContent set correctly
for feature in gj.data['features']:
assert feature['properties']['_popupContent'] == \
feature['properties']['name']

bounds = self.map.get_bounds()
assert bounds == [[18.948267, -171.742517],
[71.285909, -66.979601]], bounds

def test_geo_json_popup_afterwards(self):
"""Test geojson popup as attribute name that is set after the fact"""

path = os.path.join(rootpath, 'us-counties.json')
data = json.load(open(path))
gj = folium.GeoJson(data)
gj.popup_attribute = 'name'

self.map = folium.Map([43, -100], zoom_start=4)
gj.add_to(self.map)
self.map._parent.render()

assert gj.popup_attribute == 'name', gj.popup_attribute

bounds = self.map.get_bounds()
assert bounds == [[18.948267, -171.742517],
[71.285909, -66.979601]], bounds

def test_geo_json_popup_function_afterwards(self):
"""Test geojson popup as function set after the fact."""

path = os.path.join(rootpath, 'us-counties.json')
data = json.load(open(path))
gj = folium.GeoJson(data)
gj.popup_function = lambda feature: feature['properties']['name']

self.map = folium.Map([43, -100], zoom_start=4)
gj.add_to(self.map)
self.map._parent.render()

assert gj.popup_attribute == '_popupContent', gj.popup_attribute

# Test each feature to guarantee popupContent set correctly
for feature in gj.data['features']:
assert feature['properties']['_popupContent'] == \
feature['properties']['name']

bounds = self.map.get_bounds()
assert bounds == [[18.948267, -171.742517],
[71.285909, -66.979601]], bounds

def test_topo_json(self):
"""Test geojson method."""

Expand Down