Skip to content

Commit 4b393d0

Browse files
handle numeric key_on values for Choropleth (#1193)
* handle numeric key_on values for Choropleth This addresses a bug where geometries and data coming from GeoDataFrames were not joined. This was caused by the index being casted to string during the conversion to GeoJSON. The new behaviour is the following: - try the join as it is - if index contained numeric value, cast to str and try again This commit includes 3 new test cases where: - 1st and 2nd were failing under old behaviour - 3rd passes under both behaviours * fix formatting * restore formatting * edit comments * simplify logic * fix error type Co-authored-by: Alessio Arena <are014@csiro.au> Co-authored-by: Frank <33519926+Conengmo@users.noreply.github.com>
1 parent 9d31525 commit 4b393d0

3 files changed

Lines changed: 124 additions & 9 deletions

File tree

folium/features.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,10 +1269,20 @@ def color_scale_fun(x):
12691269
if key_of_x is None:
12701270
raise ValueError("key_on `{!r}` not found in GeoJSON.".format(key_on))
12711271

1272-
if key_of_x not in color_data.keys():
1273-
return nan_fill_color, nan_fill_opacity
1272+
try:
1273+
value_of_x = color_data[key_of_x]
1274+
except KeyError:
1275+
try:
1276+
# try again but match str to int and vice versa
1277+
if isinstance(key_of_x, int):
1278+
value_of_x = color_data[str(key_of_x)]
1279+
elif isinstance(key_of_x, str):
1280+
value_of_x = color_data[int(key_of_x)]
1281+
else:
1282+
return nan_fill_color, nan_fill_opacity
1283+
except (KeyError, ValueError):
1284+
return nan_fill_color, nan_fill_opacity
12741285

1275-
value_of_x = color_data[key_of_x]
12761286
if np.isnan(value_of_x):
12771287
return nan_fill_color, nan_fill_opacity
12781288

tests/geo_grid.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{"type": "FeatureCollection", "features": [
2-
{"id": "0", "type": "Feature", "properties": {"idx": 0}, "geometry": {"type": "Polygon", "coordinates": [[[479978.6866107661, 6676603.690234357], [491978.6866107661, 6676603.690234357], [491978.6866107661, 6664603.690234357], [479978.6866107661, 6664603.690234357], [479978.6866107661, 6676603.690234357]]]}},
3-
{"id": "1", "type": "Feature", "properties": {"idx": 1}, "geometry": {"type": "Polygon", "coordinates": [[[479978.6866107661, 6664603.690234357], [491978.6866107661, 6664603.690234357], [491978.6866107661, 6652603.690234357], [479978.6866107661, 6652603.690234357], [479978.6866107661, 6664603.690234357]]]}},
4-
{"id": "2", "type": "Feature", "properties": {"idx": 2}, "geometry": {"type": "Polygon", "coordinates": [[[479978.6866107661, 6652603.690234357], [491978.6866107661, 6652603.690234357], [491978.6866107661, 6640603.690234357], [479978.6866107661, 6640603.690234357], [479978.6866107661, 6652603.690234357]]]}},
5-
{"id": "3", "type": "Feature", "properties": {"idx": 3}, "geometry": {"type": "Polygon", "coordinates": [[[491978.6866107661, 6676603.690234357], [503978.6866107661, 6676603.690234357], [503978.6866107661, 6664603.690234357], [491978.6866107661, 6664603.690234357], [491978.6866107661, 6676603.690234357]]]}},
6-
{"id": "4", "type": "Feature", "properties": {"idx": 4}, "geometry": {"type": "Polygon", "coordinates": [[[491978.6866107661, 6664603.690234357], [503978.6866107661, 6664603.690234357], [503978.6866107661, 6652603.690234357], [491978.6866107661, 6652603.690234357], [491978.6866107661, 6664603.690234357]]]}},
7-
{"id": "5", "type": "Feature", "properties": {"idx": 5}, "geometry": {"type": "Polygon", "coordinates": [[[491978.6866107661, 6652603.690234357], [503978.6866107661, 6652603.690234357], [503978.6866107661, 6640603.690234357], [491978.6866107661, 6640603.690234357], [491978.6866107661, 6652603.690234357]]]}}
2+
{"id": "0", "type": "Feature", "properties": {"idx": 0, "value": 78.0}, "geometry": {"type": "Polygon", "coordinates": [[[479978.6866107661, 6676603.690234357], [491978.6866107661, 6676603.690234357], [491978.6866107661, 6664603.690234357], [479978.6866107661, 6664603.690234357], [479978.6866107661, 6676603.690234357]]]}},
3+
{"id": "1", "type": "Feature", "properties": {"idx": 1, "value": 39.0}, "geometry": {"type": "Polygon", "coordinates": [[[479978.6866107661, 6664603.690234357], [491978.6866107661, 6664603.690234357], [491978.6866107661, 6652603.690234357], [479978.6866107661, 6652603.690234357], [479978.6866107661, 6664603.690234357]]]}},
4+
{"id": "2", "type": "Feature", "properties": {"idx": 2, "value": 0.0}, "geometry": {"type": "Polygon", "coordinates": [[[479978.6866107661, 6652603.690234357], [491978.6866107661, 6652603.690234357], [491978.6866107661, 6640603.690234357], [479978.6866107661, 6640603.690234357], [479978.6866107661, 6652603.690234357]]]}},
5+
{"id": "3", "type": "Feature", "properties": {"idx": 3, "value": 81.0}, "geometry": {"type": "Polygon", "coordinates": [[[491978.6866107661, 6676603.690234357], [503978.6866107661, 6676603.690234357], [503978.6866107661, 6664603.690234357], [491978.6866107661, 6664603.690234357], [491978.6866107661, 6676603.690234357]]]}},
6+
{"id": "4", "type": "Feature", "properties": {"idx": 4, "value": 42.0}, "geometry": {"type": "Polygon", "coordinates": [[[491978.6866107661, 6664603.690234357], [503978.6866107661, 6664603.690234357], [503978.6866107661, 6652603.690234357], [491978.6866107661, 6652603.690234357], [491978.6866107661, 6664603.690234357]]]}},
7+
{"id": "5", "type": "Feature", "properties": {"idx": 5, "value": 68.0}, "geometry": {"type": "Polygon", "coordinates": [[[491978.6866107661, 6652603.690234357], [503978.6866107661, 6652603.690234357], [503978.6866107661, 6640603.690234357], [491978.6866107661, 6640603.690234357], [491978.6866107661, 6652603.690234357]]]}}
88
]}

tests/test_folium.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import numpy as np
2121
import pandas as pd
22+
import geopandas as gpd
2223

2324
import pytest
2425

@@ -247,6 +248,110 @@ def test_choropleth_key_on(self):
247248
fill_color=fill_color,
248249
columns=columns)
249250

251+
def test_choropleth_geopandas_numeric(self):
252+
"""Test to make sure that Choropleth function does complete the lookup
253+
between a GeoJSON generated from a GeoDataFrame and data from the GeoDataFrame itself.
254+
255+
key_on field is dtype = str, while column 0 is dtype = int
256+
All geometries have matching values (no nan_fill_color allowed)
257+
"""
258+
self.setup()
259+
260+
with open(os.path.join(rootpath, 'geo_grid.json')) as f:
261+
geo_data = json.load(f)
262+
263+
geo_data_frame = gpd.GeoDataFrame.from_features(geo_data['features'])
264+
geo_data_frame.crs = {'init': 'epsg:4326'}
265+
fill_color = 'BuPu'
266+
key_on = 'feature.id'
267+
268+
Choropleth(
269+
geo_data=geo_data_frame.geometry,
270+
data=geo_data_frame.value,
271+
key_on=key_on,
272+
fill_color=fill_color,
273+
fill_opacity=0.543212345,
274+
nan_fill_color='a_random_color',
275+
nan_fill_opacity=0.123454321).add_to(self.m)
276+
277+
out = self.m._parent.render()
278+
out_str = ''.join(out.split())
279+
280+
assert '"fillColor":"a_random_color","fillOpacity":0.123454321' not in out_str
281+
assert '"fillOpacity":0.543212345' in out_str
282+
283+
def test_choropleth_geopandas_mixed(self):
284+
"""Test to make sure that Choropleth function does complete the lookup
285+
between a GeoJSON generated from a GeoDataFrame and data from a DataFrame.
286+
287+
key_on field is dtype = str, while column 0 is dtype = object (mixed int and str)
288+
All geometries have matching values (no nan_fill_color allowed)
289+
"""
290+
self.setup()
291+
292+
with open(os.path.join(rootpath, 'geo_grid.json')) as f:
293+
geo_data = json.load(f)
294+
295+
geo_data_frame = gpd.GeoDataFrame.from_features(geo_data['features'])
296+
geo_data_frame.crs = {'init': 'epsg:4326'}
297+
data = pd.DataFrame({'idx': {'0': 0, '1': '1', '2': 2, '3': 3, '4': 4, '5': 5},
298+
'value': {'0': 78.0, '1': 39.0, '2': 0.0, '3': 81.0, '4': 42.0, '5': 68.0}})
299+
fill_color = 'BuPu'
300+
columns = ['idx', 'value']
301+
key_on = 'feature.id'
302+
303+
Choropleth(
304+
geo_data=geo_data_frame.geometry,
305+
data=data,
306+
key_on=key_on,
307+
columns=columns,
308+
fill_color=fill_color,
309+
fill_opacity=0.543212345,
310+
nan_fill_color='a_random_color',
311+
nan_fill_opacity=0.123454321).add_to(self.m)
312+
313+
out = self.m._parent.render()
314+
out_str = ''.join(out.split())
315+
316+
assert '"fillColor":"a_random_color","fillOpacity":0.123454321' not in out_str
317+
assert '"fillOpacity":0.543212345' in out_str
318+
319+
def test_choropleth_geopandas_str(self):
320+
"""Test to make sure that Choropleth function does complete the lookup
321+
between a GeoJSON generated from a GeoDataFrame and data from a DataFrame.
322+
323+
key_on field and column 0 from data are both strings.
324+
All geometries have matching values (no nan_fill_color allowed)
325+
"""
326+
self.setup()
327+
328+
with open(os.path.join(rootpath, 'geo_grid.json')) as f:
329+
geo_data = json.load(f)
330+
331+
geo_data_frame = gpd.GeoDataFrame.from_features(geo_data['features'])
332+
geo_data_frame.crs = {'init': 'epsg:4326'}
333+
data = pd.DataFrame({'idx': {'0': '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5'},
334+
'value': {'0': 78.0, '1': 39.0, '2': 0.0, '3': 81.0, '4': 42.0, '5': 68.0}})
335+
fill_color = 'BuPu'
336+
columns = ['idx', 'value']
337+
key_on = 'feature.id'
338+
339+
Choropleth(
340+
geo_data=geo_data_frame.geometry,
341+
data=data,
342+
key_on=key_on,
343+
columns=columns,
344+
fill_color=fill_color,
345+
fill_opacity=0.543212345,
346+
nan_fill_color='a_random_color',
347+
nan_fill_opacity=0.123454321).add_to(self.m)
348+
349+
out = self.m._parent.render()
350+
out_str = ''.join(out.split())
351+
352+
assert '"fillColor":"a_random_color","fillOpacity":0.123454321' not in out_str
353+
assert '"fillOpacity":0.543212345' in out_str
354+
250355
def test_choropleth_warning(self):
251356
"""Test that the Map.choropleth method works and raises a warning."""
252357
self.setup()

0 commit comments

Comments
 (0)