Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
python-visualization
GitHub Repository: python-visualization/folium
Path: blob/main/tests/test_folium.py
1593 views
1
"""
2
Folium Tests
3
-------
4
5
"""
6
7
import json
8
import os
9
10
import geopandas as gpd
11
import numpy as np
12
import pandas as pd
13
import pytest
14
import xyzservices.providers as xyz
15
from jinja2.utils import htmlsafe_json_dumps
16
17
import folium
18
from folium import TileLayer
19
from folium.features import Choropleth, GeoJson
20
from folium.template import Template
21
22
rootpath = os.path.abspath(os.path.dirname(__file__))
23
24
# For testing remote requests
25
remote_url = "https://raw.githubusercontent.com/python-visualization/folium/main/examples/data/us-states.json" # noqa
26
27
28
def setup_data():
29
"""Import economic data for testing."""
30
with open(os.path.join(rootpath, "us-counties.json")) as f:
31
get_id = json.load(f)
32
33
county_codes = [x["id"] for x in get_id["features"]]
34
county_df = pd.DataFrame({"FIPS_Code": county_codes}, dtype=str)
35
36
# Read into Dataframe, cast to string for consistency.
37
df = pd.read_csv(os.path.join(rootpath, "us_county_data.csv"), na_values=[" "])
38
df["FIPS_Code"] = df["FIPS_Code"].astype(str)
39
40
# Perform an inner join, pad NA's with data from nearest county.
41
merged = pd.merge(df, county_df, on="FIPS_Code", how="inner")
42
return merged.fillna(method="pad")
43
44
45
def test_location_args():
46
"""Test some data types for a location arg."""
47
location = np.array([45.5236, -122.6750])
48
m = folium.Map(location)
49
assert m.location == [45.5236, -122.6750]
50
51
df = pd.DataFrame({"location": [45.5236, -122.6750]})
52
m = folium.Map(df["location"])
53
assert m.location == [45.5236, -122.6750]
54
55
56
class TestFolium:
57
"""Test class for the Folium library."""
58
59
def setup_method(self):
60
"""Setup Folium Map."""
61
attr = "http://openstreetmap.org"
62
self.m = folium.Map(
63
location=[45.5236, -122.6750],
64
width=900,
65
height=400,
66
max_zoom=20,
67
zoom_start=4,
68
max_bounds=True,
69
font_size="1.5rem",
70
attr=attr,
71
)
72
self.fit_bounds_template = Template(
73
"""
74
{% if autobounds %}
75
var autobounds = L.featureGroup({{ features }}).getBounds()
76
{% if not bounds %}
77
{% set bounds = "autobounds" %}
78
{% endif %}
79
{% endif %}
80
{% if bounds %}
81
{{this._parent.get_name()}}.fitBounds({{ bounds }},
82
{{ fit_bounds_options }}
83
);
84
{% endif %}
85
"""
86
)
87
88
def test_init(self):
89
"""Test map initialization."""
90
91
assert self.m.get_name().startswith("map_")
92
assert self.m.get_root() == self.m._parent
93
assert self.m.location == [45.5236, -122.6750]
94
assert self.m.options["zoom"] == 4
95
assert self.m.options["max_bounds"] == [[-90, -180], [90, 180]]
96
assert self.m.position == "relative"
97
assert self.m.height == (400, "px")
98
assert self.m.width == (900, "px")
99
assert self.m.left == (0, "%")
100
assert self.m.top == (0, "%")
101
assert self.m.global_switches.no_touch is False
102
assert self.m.global_switches.disable_3d is False
103
assert self.m.font_size == "1.5rem"
104
assert self.m.to_dict() == {
105
"name": "Map",
106
"id": self.m._id,
107
"children": {
108
"openstreetmap": {
109
"name": "TileLayer",
110
"id": self.m._children["openstreetmap"]._id,
111
"children": {},
112
}
113
},
114
}
115
116
@pytest.mark.parametrize(
117
"tiles,provider",
118
[
119
("OpenStreetMap", xyz.OpenStreetMap.Mapnik),
120
("CartoDB positron", xyz.CartoDB.Positron),
121
("CartoDB dark_matter", xyz.CartoDB.DarkMatter),
122
],
123
)
124
def test_builtin_tile(self, tiles, provider):
125
"""Test custom maptiles."""
126
127
m = folium.Map(location=[45.5236, -122.6750], tiles=tiles)
128
tiles = "".join(tiles.lower().strip().split())
129
url = provider.build_url(fill_subdomain=False, scale_factor="{r}")
130
attr = provider.html_attribution
131
132
assert m._children[tiles.replace("_", "")].tiles == url
133
assert htmlsafe_json_dumps(attr) in m._parent.render()
134
135
bounds = m.get_bounds()
136
assert bounds == [[None, None], [None, None]], bounds
137
138
def test_custom_tile(self):
139
"""Test custom tile URLs."""
140
141
url = "http://{s}.custom_tiles.org/{z}/{x}/{y}.png"
142
attr = "Attribution for custom tiles"
143
144
with pytest.raises(ValueError):
145
folium.Map(location=[45.5236, -122.6750], tiles=url)
146
147
m = folium.Map(location=[45.52, -122.67], tiles=url, attr=attr)
148
assert m._children[url].tiles == url
149
assert attr in m._parent.render()
150
151
bounds = m.get_bounds()
152
assert bounds == [[None, None], [None, None]], bounds
153
154
def test_tilelayer_object(self):
155
url = "http://{s}.custom_tiles.org/{z}/{x}/{y}.png"
156
attr = "Attribution for custom tiles"
157
m = folium.Map(location=[45.52, -122.67], tiles=TileLayer(url, attr=attr))
158
assert next(iter(m._children.values())).tiles == url
159
assert attr in m._parent.render()
160
161
def test_feature_group(self):
162
"""Test FeatureGroup."""
163
164
m = folium.Map()
165
feature_group = folium.FeatureGroup()
166
feature_group.add_child(folium.Marker([45, -30], popup=folium.Popup("-30")))
167
feature_group.add_child(folium.Marker([45, 30], popup=folium.Popup("30")))
168
m.add_child(feature_group)
169
m.add_child(folium.LayerControl())
170
171
m._repr_html_()
172
173
bounds = m.get_bounds()
174
assert bounds == [[45, -30], [45, 30]], bounds
175
176
def test_topo_json_smooth_factor(self):
177
"""Test topojson smooth factor method."""
178
self.m = folium.Map([43, -100], zoom_start=4)
179
180
# Adding TopoJSON as additional layer.
181
with open(os.path.join(rootpath, "or_counties_topo.json")) as f:
182
choropleth = Choropleth(
183
f, topojson="objects.or_counties_geo", smooth_factor=0.5
184
).add_to(self.m)
185
186
out = self.m._parent.render()
187
188
# Verify TopoJson
189
topo_json = choropleth.geojson
190
topojson_str = topo_json._template.module.script(topo_json)
191
assert "".join(topojson_str.split())[:-1] in "".join(out.split())
192
193
def test_choropleth_features(self):
194
"""Test to make sure that Choropleth function doesn't allow
195
values outside of the domain defined by bins.
196
197
It also tests that all parameters work as expected regarding
198
nan and missing values.
199
"""
200
with open(os.path.join(rootpath, "us-counties.json")) as f:
201
geo_data = json.load(f)
202
data = {"1001": -1}
203
fill_color = "BuPu"
204
key_on = "id"
205
206
with pytest.raises(ValueError):
207
Choropleth(
208
geo_data=geo_data,
209
data=data,
210
key_on=key_on,
211
fill_color=fill_color,
212
bins=[0, 1, 2, 3],
213
).add_to(self.m)
214
self.m._parent.render()
215
216
Choropleth(
217
geo_data=geo_data,
218
data={"1001": 1, "1003": float("nan")},
219
key_on=key_on,
220
fill_color=fill_color,
221
fill_opacity=0.543212345,
222
nan_fill_color="a_random_color",
223
nan_fill_opacity=0.123454321,
224
).add_to(self.m)
225
226
out = self.m._parent.render()
227
out_str = "".join(out.split())
228
assert '"fillColor":"a_random_color","fillOpacity":0.123454321' in out_str
229
assert '"fillOpacity":0.543212345' in out_str
230
231
def test_choropleth_key_on(self):
232
"""Test to make sure that Choropleth function doesn't raises
233
a ValueError when the 'key_on' field is set to a column that might
234
have 0 as a value.
235
"""
236
with open(os.path.join(rootpath, "geo_grid.json")) as f:
237
geo_data = json.load(f)
238
data = pd.DataFrame(
239
{
240
"idx": {"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5},
241
"value": {
242
"0": 78.0,
243
"1": 39.0,
244
"2": 0.0,
245
"3": 81.0,
246
"4": 42.0,
247
"5": 68.0,
248
},
249
}
250
)
251
fill_color = "BuPu"
252
columns = ["idx", "value"]
253
key_on = "feature.properties.idx"
254
255
Choropleth(
256
geo_data=geo_data,
257
data=data,
258
key_on=key_on,
259
fill_color=fill_color,
260
columns=columns,
261
)
262
263
def test_choropleth_geopandas_numeric(self):
264
"""Test to make sure that Choropleth function does complete the lookup
265
between a GeoJSON generated from a GeoDataFrame and data from the GeoDataFrame itself.
266
267
key_on field is dtype = str, while column 0 is dtype = int
268
All geometries have matching values (no nan_fill_color allowed)
269
"""
270
with open(os.path.join(rootpath, "geo_grid.json")) as f:
271
geo_data = json.load(f)
272
273
geo_data_frame = gpd.GeoDataFrame.from_features(geo_data["features"])
274
geo_data_frame = geo_data_frame.set_crs("epsg: 4326")
275
fill_color = "BuPu"
276
key_on = "feature.id"
277
278
Choropleth(
279
geo_data=geo_data_frame.geometry,
280
data=geo_data_frame.value,
281
key_on=key_on,
282
fill_color=fill_color,
283
fill_opacity=0.543212345,
284
nan_fill_color="a_random_color",
285
nan_fill_opacity=0.123454321,
286
).add_to(self.m)
287
288
out = self.m._parent.render()
289
out_str = "".join(out.split())
290
291
assert '"fillColor":"a_random_color","fillOpacity":0.123454321' not in out_str
292
assert '"fillOpacity":0.543212345' in out_str
293
294
def test_choropleth_geopandas_mixed(self):
295
"""Test to make sure that Choropleth function does complete the lookup
296
between a GeoJSON generated from a GeoDataFrame and data from a DataFrame.
297
298
key_on field is dtype = str, while column 0 is dtype = object (mixed int and str)
299
All geometries have matching values (no nan_fill_color allowed)
300
"""
301
with open(os.path.join(rootpath, "geo_grid.json")) as f:
302
geo_data = json.load(f)
303
304
geo_data_frame = gpd.GeoDataFrame.from_features(geo_data["features"])
305
geo_data_frame = geo_data_frame.set_crs("epsg: 4326")
306
data = pd.DataFrame(
307
{
308
"idx": {"0": 0, "1": "1", "2": 2, "3": 3, "4": 4, "5": 5},
309
"value": {
310
"0": 78.0,
311
"1": 39.0,
312
"2": 0.0,
313
"3": 81.0,
314
"4": 42.0,
315
"5": 68.0,
316
},
317
}
318
)
319
fill_color = "BuPu"
320
columns = ["idx", "value"]
321
key_on = "feature.id"
322
323
Choropleth(
324
geo_data=geo_data_frame.geometry,
325
data=data,
326
key_on=key_on,
327
columns=columns,
328
fill_color=fill_color,
329
fill_opacity=0.543212345,
330
nan_fill_color="a_random_color",
331
nan_fill_opacity=0.123454321,
332
).add_to(self.m)
333
334
out = self.m._parent.render()
335
out_str = "".join(out.split())
336
337
assert '"fillColor":"a_random_color","fillOpacity":0.123454321' not in out_str
338
assert '"fillOpacity":0.543212345' in out_str
339
340
def test_choropleth_geopandas_str(self):
341
"""Test to make sure that Choropleth function does complete the lookup
342
between a GeoJSON generated from a GeoDataFrame and data from a DataFrame.
343
344
key_on field and column 0 from data are both strings.
345
All geometries have matching values (no nan_fill_color allowed)
346
"""
347
with open(os.path.join(rootpath, "geo_grid.json")) as f:
348
geo_data = json.load(f)
349
350
geo_data_frame = gpd.GeoDataFrame.from_features(geo_data["features"])
351
geo_data_frame = geo_data_frame.set_crs("epsg: 4326")
352
data = pd.DataFrame(
353
{
354
"idx": {"0": "0", "1": "1", "2": "2", "3": "3", "4": "4", "5": "5"},
355
"value": {
356
"0": 78.0,
357
"1": 39.0,
358
"2": 0.0,
359
"3": 81.0,
360
"4": 42.0,
361
"5": 68.0,
362
},
363
}
364
)
365
fill_color = "BuPu"
366
columns = ["idx", "value"]
367
key_on = "feature.id"
368
369
Choropleth(
370
geo_data=geo_data_frame.geometry,
371
data=data,
372
key_on=key_on,
373
columns=columns,
374
fill_color=fill_color,
375
fill_opacity=0.543212345,
376
nan_fill_color="a_random_color",
377
nan_fill_opacity=0.123454321,
378
).add_to(self.m)
379
380
out = self.m._parent.render()
381
out_str = "".join(out.split())
382
383
assert '"fillColor":"a_random_color","fillOpacity":0.123454321' not in out_str
384
assert '"fillOpacity":0.543212345' in out_str
385
386
def test_tile_attr_unicode(self):
387
"""Test tile attribution unicode"""
388
m = folium.Map(location=[45.5236, -122.6750], tiles="test", attr="юникод")
389
m._parent.render()
390
391
def test_fit_bounds(self):
392
"""Test fit_bounds."""
393
bounds = ((52.193636, -2.221575), (52.636878, -1.139759))
394
self.m.fit_bounds(bounds)
395
fitbounds = [
396
val
397
for key, val in self.m._children.items()
398
if isinstance(val, folium.FitBounds)
399
][0]
400
out = self.m._parent.render()
401
402
fit_bounds_rendered = self.fit_bounds_template.render(
403
{
404
"bounds": json.dumps(bounds),
405
"this": fitbounds,
406
"fit_bounds_options": {},
407
}
408
)
409
410
assert "".join(fit_bounds_rendered.split()) in "".join(out.split())
411
412
def test_fit_bounds_2(self):
413
bounds = ((52.193636, -2.221575), (52.636878, -1.139759))
414
self.m.fit_bounds(bounds, max_zoom=15, padding=(3, 3))
415
fitbounds = [
416
val
417
for key, val in self.m._children.items()
418
if isinstance(val, folium.FitBounds)
419
][0]
420
out = self.m._parent.render()
421
422
fit_bounds_rendered = self.fit_bounds_template.render(
423
{
424
"bounds": json.dumps(bounds),
425
"fit_bounds_options": json.dumps(
426
{
427
"maxZoom": 15,
428
"padding": (3, 3),
429
},
430
sort_keys=True,
431
),
432
"this": fitbounds,
433
}
434
)
435
436
assert "".join(fit_bounds_rendered.split()) in "".join(out.split())
437
438
bounds = self.m.get_bounds()
439
assert bounds == [[None, None], [None, None]], bounds
440
441
def test_custom_icon(self):
442
"""Test CustomIcon."""
443
icon_image = "http://leafletjs.com/docs/images/leaf-green.png"
444
shadow_image = "http://leafletjs.com/docs/images/leaf-shadow.png"
445
446
self.m = folium.Map([45, -100], zoom_start=4)
447
i = folium.features.CustomIcon(
448
icon_image,
449
icon_size=(38, 95),
450
icon_anchor=(22, 94),
451
shadow_image=shadow_image,
452
shadow_size=(50, 64),
453
shadow_anchor=(4, 62),
454
popup_anchor=(-3, -76),
455
)
456
mk = folium.Marker([45, -100], icon=i, popup=folium.Popup("Hello"))
457
self.m.add_child(mk)
458
self.m._parent.render()
459
460
bounds = self.m.get_bounds()
461
assert bounds == [[45, -100], [45, -100]], bounds
462
463
def test_global_switches(self):
464
m = folium.Map(prefer_canvas=True)
465
out = m._parent.render()
466
out_str = "".join(out.split())
467
assert '"preferCanvas":true' in out_str
468
assert not m.global_switches.no_touch
469
assert not m.global_switches.disable_3d
470
471
m = folium.Map(no_touch=True)
472
out = m._parent.render()
473
out_str = "".join(out.split())
474
assert '"preferCanvas":false' in out_str
475
assert m.global_switches.no_touch
476
assert not m.global_switches.disable_3d
477
478
m = folium.Map(disable_3d=True)
479
out = m._parent.render()
480
out_str = "".join(out.split())
481
assert '"preferCanvas":false' in out_str
482
assert not m.global_switches.no_touch
483
assert m.global_switches.disable_3d
484
485
m = folium.Map(prefer_canvas=True, no_touch=True, disable_3d=True)
486
out = m._parent.render()
487
out_str = "".join(out.split())
488
assert '"preferCanvas":true' in out_str
489
assert m.global_switches.no_touch
490
assert m.global_switches.disable_3d
491
492
def test_json_request(self):
493
"""Test requests for remote GeoJSON files."""
494
self.m = folium.Map(zoom_start=4)
495
496
# Adding remote GeoJSON as additional layer.
497
GeoJson(remote_url, smooth_factor=0.5).add_to(self.m)
498
499
self.m._parent.render()
500
bounds = self.m.get_bounds()
501
np.testing.assert_allclose(
502
bounds, [[18.948267, -178.123152], [71.351633, 173.304726]]
503
)
504
505
def test_control_typecheck(self):
506
m = folium.Map(
507
location=[39.949610, -75.150282], zoom_start=5, zoom_control=False
508
)
509
tiles = TileLayer(
510
tiles="OpenStreetMap",
511
show=False,
512
control=False,
513
)
514
tiles.add_to(m)
515
516
with pytest.raises(TypeError) as excinfo:
517
minimap = folium.Control("MiniMap", tiles, position="downunder")
518
minimap.add_js_link(
519
"minimap_js",
520
"https://cdnjs.cloudflare.com/ajax/libs/leaflet-minimap/3.6.1/Control.MiniMap.min.js",
521
)
522
minimap.add_css_link(
523
"minimap_css",
524
"https://cdnjs.cloudflare.com/ajax/libs/leaflet-minimap/3.6.1/Control.MiniMap.css",
525
)
526
minimap.add_to(m)
527
assert "position must be one of ('bottomright', 'bottomleft'" in str(
528
excinfo.value
529
)
530
531