Skip to content

Commit 1366528

Browse files
committed
Merge pull request #266 from BibMartin/choropleth
Replace GeoJsonStyle by kwarg style_function in GeoJson
2 parents f84de18 + 01ec8aa commit 1366528

6 files changed

Lines changed: 114 additions & 151 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,7 @@ nosetests.xml
4444
#Virtualenv
4545
ENV
4646
.env
47+
48+
# Tests products
49+
.cache
50+

folium/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
Popup, TileLayer)
1111

1212
from folium.features import (ClickForMarker, ColorScale, CustomIcon, DivIcon,
13-
GeoJson, GeoJsonStyle, ImageOverlay, LatLngPopup,
13+
GeoJson, ImageOverlay, LatLngPopup,
1414
MarkerCluster, MultiPolyLine, PolyLine, Vega,
1515
RegularPolygonMarker, TopoJson, WmsTileLayer)
1616

folium/features.py

Lines changed: 34 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -194,20 +194,22 @@ def render(self, **kwargs):
194194

195195

196196
class GeoJson(MacroElement):
197-
def __init__(self, data):
197+
def __init__(self, data, style_function=None):
198198
"""
199199
Creates a GeoJson plugin to append into a map with
200200
Map.add_plugin.
201201
202202
Parameters
203203
----------
204204
data: file, dict or str.
205-
The GeoJSON data you want to plot.
206-
* If file, then data will be read in the file and fully
207-
embedded in Leaflet's JavaScript.
208-
* If dict, then data will be converted to JSON and embedded
209-
in the JavaScript.
210-
* If str, then data will be passed to the JavaScript as-is.
205+
The GeoJSON data you want to plot.
206+
* If file, then data will be read in the file and fully
207+
embedded in Leaflet's JavaScript.
208+
* If dict, then data will be converted to JSON and embedded
209+
in the JavaScript.
210+
* If str, then data will be passed to the JavaScript as-is.
211+
style_function: function, default None
212+
A function mapping a GeoJson Feature to a style dict.
211213
212214
Examples
213215
--------
@@ -218,22 +220,43 @@ def __init__(self, data):
218220
>>> # Providing string.
219221
>>> GeoJson(open('foo.json').read())
220222
223+
>>> # Providing a style_function that put all states in green, but Alabama in blue.
224+
>>> style_function=lambda x: {'fillColor': '#0000ff' if x['properties']['name']=='Alabama' else '#00ff00'}
225+
>>> GeoJson(geojson, style_function=style_function)
221226
"""
222227
super(GeoJson, self).__init__()
223228
self._name = 'GeoJson'
224229
if 'read' in dir(data):
225-
self.data = data.read()
230+
self.data = json.load(data)
226231
elif type(data) is dict:
227-
self.data = json.dumps(data)
228-
else:
229232
self.data = data
233+
else:
234+
self.data = json.loads(data)
235+
236+
if 'features' not in self.data.keys():
237+
# Catch case when GeoJSON is just a single Feature or a geometry.
238+
if not (isinstance(self.data, dict) and 'geometry' in self.data.keys()):
239+
# Catch case when GeoJSON is just a geometry.
240+
self.data = {'type' : 'Feature', 'geometry' : self.data}
241+
self.data = {'type' : 'FeatureCollection', 'features' : [self.data]}
242+
243+
if style_function is None:
244+
style_function = lambda x: {}
245+
self.style_function = style_function
230246

231247
self._template = Template(u"""
232248
{% macro script(this, kwargs) %}
233-
var {{this.get_name()}} = L.geoJson({{this.data}}).addTo({{this._parent.get_name()}});
249+
var {{this.get_name()}} = L.geoJson({{this.style_data()}})
250+
.addTo({{this._parent.get_name()}})
251+
.setStyle(function(feature) {return feature.properties.style;});
234252
{% endmacro %}
235253
""") # noqa
236254

255+
def style_data(self):
256+
for feature in self.data['features']:
257+
feature.setdefault('properties',{}).setdefault('style',{}).update(
258+
self.style_function(feature))
259+
return json.dumps(self.data)
237260

238261
class TopoJson(MacroElement):
239262
def __init__(self, data, object_path):
@@ -273,81 +296,6 @@ def render(self, **kwargs):
273296
JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.9/topojson.min.js"), # noqa
274297
name='topojson')
275298

276-
277-
class GeoJsonStyle(MacroElement):
278-
def __init__(self, color_domain, color_code, color_data=None,
279-
key_on='feature.properties.color',
280-
weight=1, opacity=1, color='black',
281-
fill_opacity=0.6, dash_array=0):
282-
"""
283-
TODO docstring here
284-
285-
"""
286-
super(GeoJsonStyle, self).__init__()
287-
self._name = 'GeoJsonStyle'
288-
289-
self.color_domain = color_domain
290-
self.color_range = color_brewer(color_code, n=len(color_domain))
291-
self.color_data = json.dumps(color_data)
292-
self.key_on = key_on
293-
294-
self.weight = weight
295-
self.opacity = opacity
296-
self.color = color
297-
self.fill_color = color_code
298-
self.fill_opacity = fill_opacity
299-
self.dash_array = dash_array
300-
301-
self._template = Template(u"""
302-
{% macro script(this, kwargs) %}
303-
{% if not this.color_range %}
304-
var {{this.get_name()}} = {
305-
color_function : function(feature) {
306-
return '{{this.fill_color}}';
307-
},
308-
};
309-
{%else%}
310-
var {{this.get_name()}} = {
311-
color_scale : d3.scale.threshold()
312-
.domain({{this.color_domain}})
313-
.range({{this.color_range}}),
314-
color_data : {{this.color_data}},
315-
color_function : function(feature) {
316-
{% if this.color_data=='null' %}
317-
return this.color_scale({{this.key_on}});
318-
{% else %}
319-
return
320-
this.color_scale(this.color_data[{{this.key_on}}]);
321-
{% endif %}
322-
},
323-
};
324-
{%endif%}
325-
326-
{{this._parent.get_name()}}.setStyle(function(feature) {
327-
return {
328-
fillColor: {{this.get_name()}}.color_function(feature),
329-
weight: {{this.weight}},
330-
opacity: {{this.opacity}},
331-
color: '{{this.color}}',
332-
fillOpacity: {{this.fill_opacity}},
333-
dashArray: '{{this.dash_array}}'
334-
};
335-
});
336-
{% endmacro %}
337-
""")
338-
339-
def render(self, **kwargs):
340-
super(GeoJsonStyle, self).render(**kwargs)
341-
342-
figure = self.get_root()
343-
assert isinstance(figure, Figure), ("You cannot render this Element "
344-
"if it's not in a Figure.")
345-
346-
figure.header.add_children(
347-
JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"), # noqa
348-
name='d3')
349-
350-
351299
class ColorScale(MacroElement):
352300
def __init__(self, color_domain, color_code, caption=""):
353301
"""
@@ -505,7 +453,6 @@ def __init__(self, location, radius=500, color='black',
505453
{% endmacro %}
506454
""")
507455

508-
509456
class LatLngPopup(MacroElement):
510457
def __init__(self):
511458
"""

folium/folium.py

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .map import Map as _Map
1717
from .map import Icon, Marker, Popup, FitBounds
1818
from .features import (WmsTileLayer, RegularPolygonMarker, Vega, GeoJson,
19-
GeoJsonStyle, CircleMarker, LatLngPopup,
19+
CircleMarker, LatLngPopup,
2020
ClickForMarker, ColorScale, TopoJson, PolyLine,
2121
MultiPolyLine, ImageOverlay)
2222
from .utilities import color_brewer, write_png
@@ -442,10 +442,17 @@ def add_plugin(self, plugin):
442442
#
443443
# self.template_vars.update({'fit_bounds': fit_bounds_str.strip()})
444444

445-
def geo_json(self, geo_path=None, geo_str=None, data_out='data.json',
445+
def geo_json(self, *args, **kwargs):
446+
"""This method is deprecated and will be removed in v0.2.1. See
447+
`Map.choropleth` instead.
448+
"""
449+
warnings.warn('This method is deprecated. Please use Map.choropleth instead.')
450+
return self.choropleth(*args, **kwargs)
451+
452+
def choropleth(self, geo_path=None, geo_str=None, data_out='data.json',
446453
data=None, columns=None, key_on=None, threshold_scale=None,
447454
fill_color='blue', fill_opacity=0.6, line_color='black',
448-
line_weight=1, line_opacity=1, legend_name=None,
455+
line_weight=1, line_opacity=1, legend_name="",
449456
topojson=None, reset=False):
450457
"""Apply a GeoJSON overlay to the map.
451458
@@ -506,8 +513,8 @@ def geo_json(self, geo_path=None, geo_str=None, data_out='data.json',
506513
GeoJSON geopath line weight.
507514
line_opacity: float, default 1
508515
GeoJSON geopath line opacity, range 0-1.
509-
legend_name: string, default None
510-
Title for data legend. If not passed, defaults to columns[1].
516+
legend_name: string, default empty string
517+
Title for data legend.
511518
topojson: string, default None
512519
If using a TopoJSON, passing "objects.yourfeature" to the topojson
513520
keyword argument will enable conversion to GeoJSON.
@@ -520,17 +527,14 @@ def geo_json(self, geo_path=None, geo_str=None, data_out='data.json',
520527
521528
Example
522529
-------
523-
>>> m.geo_json(geo_path='us-states.json', line_color='blue',
530+
>>> m.choropleth(geo_path='us-states.json', line_color='blue',
524531
line_weight=3)
525-
>>> m.geo_json(geo_path='geo.json', data=df,
532+
>>> m.choropleth(geo_path='geo.json', data=df,
526533
columns=['Data 1', 'Data 2'],
527534
key_on='feature.properties.myvalue', fill_color='PuBu',
528535
threshold_scale=[0, 20, 30, 40, 50, 60])
529-
>>> m.geo_json(geo_path='countries.json', topojson='objects.countries')
536+
>>> m.choropleth(geo_path='countries.json', topojson='objects.countries')
530537
"""
531-
warnings.warn("%s is deprecated. Use %s instead" %
532-
("geo_json", "add_children(GeoJson)"),
533-
FutureWarning, stacklevel=2)
534538

535539
if threshold_scale and len(threshold_scale) > 6:
536540
raise ValueError
@@ -546,11 +550,6 @@ def geo_json(self, geo_path=None, geo_str=None, data_out='data.json',
546550
else:
547551
geo_data = {}
548552

549-
if topojson:
550-
geo_json = TopoJson(geo_data, topojson)
551-
else:
552-
geo_json = GeoJson(geo_data)
553-
554553
# Create color_data dict
555554
if hasattr(data, 'set_index'):
556555
# This is a pd.DataFrame
@@ -587,20 +586,38 @@ def geo_json(self, geo_path=None, geo_str=None, data_out='data.json',
587586
color_domain = [data_min+i*(data_max-data_min)*1./nb_class
588587
for i in range(1+nb_class)]
589588
else:
590-
color_domain = [-1, 1]
589+
color_domain = None
590+
591+
if color_domain and key_on:
592+
key_on = key_on[8:] if key_on.startswith('feature.') else key_on
593+
color_range = color_brewer(fill_color, n=len(color_domain))
594+
get_by_key = lambda obj,key : (obj.get(key,None) if len(key.split('.'))<=1
595+
else get_by_key(obj.get(key.split('.')[0],None),
596+
'.'.join(key.split('.')[1:])))
597+
color_scale_fun = lambda x: color_range[len([u for u in color_domain
598+
if u<=color_data[get_by_key(x,key_on)]])]
599+
else:
600+
color_scale_fun = lambda x: fill_color
591601

592-
# Create GeoJsonStyle.
593-
geo_json_style = GeoJsonStyle(
594-
color_domain, fill_color, color_data=color_data, key_on=key_on,
595-
weight=line_weight, opacity=line_opacity, color=line_color,
596-
fill_opacity=fill_opacity)
602+
style_function = lambda x: {
603+
"weight" : line_weight,
604+
"opactiy": line_opacity,
605+
"color" : line_color,
606+
"fillOpacity" : fill_opacity,
607+
"fillColor" : color_scale_fun(x)
608+
}
597609

598-
# Create ColorScale
599-
color_scale = ColorScale(color_domain, fill_color, caption=legend_name)
610+
if topojson:
611+
geo_json = TopoJson(geo_data, topojson)
612+
else:
613+
geo_json = GeoJson(geo_data, style_function=style_function)
600614

601-
geo_json.add_children(geo_json_style)
602615
self.add_children(geo_json)
603-
self.add_children(color_scale)
616+
617+
# Create ColorScale.
618+
if color_domain:
619+
color_scale = ColorScale(color_domain, fill_color, caption=legend_name)
620+
self.add_children(color_scale)
604621

605622
def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0,
606623
min_lon=-180.0, max_lon=180.0, origin='upper',

tests/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Tests products
2+
data.png
3+
map.html

0 commit comments

Comments
 (0)