Skip to content

Commit 9c49738

Browse files
authored
Fix heatmap intensity (#1282)
* Update link * update link, discard max_val * Update test_heat_map.py * Update url after rebase * Rename temp html file utility function * temp_html_filepath accept str and bytes * Refactor selenium tests * Small additional refactor of selenium fixture * Add HeatMap with weights selenium test * Set window size at driver load * Print screenshot if test fails * export canvas, not full screenshot * Use JS resource from folium master
1 parent f8d205b commit 9c49738

8 files changed

Lines changed: 118 additions & 62 deletions

File tree

folium/folium.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from folium.raster_layers import TileLayer
1515
from folium.utilities import (
1616
_parse_size,
17-
_tmp_html,
17+
temp_html_filepath,
1818
validate_location,
1919
parse_options,
2020
)
@@ -314,7 +314,7 @@ def _to_png(self, delay=3):
314314
driver = webdriver.Firefox(options=options)
315315

316316
html = self.get_root().render()
317-
with _tmp_html(html) as fname:
317+
with temp_html_filepath(html) as fname:
318318
# We need the tempfile to avoid JS security issues.
319319
driver.get('file:///{path}'.format(path=fname))
320320
driver.maximize_window()

folium/plugins/heat_map.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# -*- coding: utf-8 -*-
22

3+
import warnings
4+
35
from branca.element import Figure, JavascriptLink
46

57
from folium.map import Layer
@@ -17,7 +19,7 @@
1719

1820
_default_js = [
1921
('leaflet-heat.js',
20-
'https://leaflet.github.io/Leaflet.heat/dist/leaflet-heat.js'),
22+
'https://cdn.jsdelivr.net/gh/python-visualization/folium@master/folium/templates/leaflet_heat.min.js'), # noqa
2123
]
2224

2325

@@ -37,8 +39,6 @@ class HeatMap(Layer):
3739
max_zoom : default 18
3840
Zoom level where the points reach maximum intensity (as intensity
3941
scales with zoom), equals maxZoom of the map by default
40-
max_val : float, default 1.
41-
Maximum point intensity
4242
radius : int, default 25
4343
Radius of each "point" of the heatmap
4444
blur : int, default 15
@@ -62,7 +62,7 @@ class HeatMap(Layer):
6262
""")
6363

6464
def __init__(self, data, name=None, min_opacity=0.5, max_zoom=18,
65-
max_val=1.0, radius=25, blur=15, gradient=None,
65+
radius=25, blur=15, gradient=None,
6666
overlay=True, control=True, show=True, **kwargs):
6767
super(HeatMap, self).__init__(name=name, overlay=overlay,
6868
control=control, show=show)
@@ -72,10 +72,13 @@ def __init__(self, data, name=None, min_opacity=0.5, max_zoom=18,
7272
for line in data]
7373
if np.any(np.isnan(self.data)):
7474
raise ValueError('data may not contain NaNs.')
75+
if kwargs.pop('max_val', None):
76+
warnings.warn('The `max_val` parameter is no longer necessary. '
77+
'The largest intensity is calculated automatically.',
78+
stacklevel=2)
7579
self.options = parse_options(
7680
min_opacity=min_opacity,
7781
max_zoom=max_zoom,
78-
max=max_val,
7982
radius=radius,
8083
blur=blur,
8184
gradient=gradient,

folium/utilities.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,12 +429,12 @@ def normalize(rendered):
429429

430430

431431
@contextmanager
432-
def _tmp_html(data):
432+
def temp_html_filepath(data):
433433
"""Yields the path of a temporary HTML file containing data."""
434434
filepath = ''
435435
try:
436436
fid, filepath = tempfile.mkstemp(suffix='.html', prefix='folium_')
437-
os.write(fid, data.encode('utf8'))
437+
os.write(fid, data.encode('utf8') if isinstance(data, str) else data)
438438
os.close(fid)
439439
yield filepath
440440
finally:

tests/plugins/test_heat_map.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def test_heat_map():
2828
out = normalize(m._parent.render())
2929

3030
# We verify that the script import is present.
31-
script = '<script src="https://leaflet.github.io/Leaflet.heat/dist/leaflet-heat.js"></script>' # noqa
31+
script = '<script src="https://cdn.jsdelivr.net/gh/python-visualization/folium@master/folium/templates/leaflet_heat.min.js"></script>' # noqa
3232
assert script in out
3333

3434
# We verify that the script part is correct.
@@ -38,7 +38,6 @@ def test_heat_map():
3838
{
3939
minOpacity: {{this.min_opacity}},
4040
maxZoom: {{this.max_zoom}},
41-
max: {{this.max_val}},
4241
radius: {{this.radius}},
4342
blur: {{this.blur}},
4443
gradient: {{this.gradient}}

tests/selenium/conftest.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
import pytest
3+
from selenium.webdriver import Chrome, ChromeOptions
4+
from selenium.webdriver.common.by import By
5+
from selenium.webdriver.support.ui import WebDriverWait
6+
from selenium.webdriver.support.expected_conditions import visibility_of_element_located
7+
8+
9+
@pytest.fixture(scope='session')
10+
def driver():
11+
"""Pytest fixture that yields a Selenium WebDriver instance"""
12+
driver = DriverFolium()
13+
try:
14+
yield driver
15+
finally:
16+
driver.quit()
17+
18+
19+
class DriverFolium(Chrome):
20+
"""Selenium WebDriver wrapper that adds folium test specific features."""
21+
22+
def __init__(self):
23+
options = ChromeOptions()
24+
options.add_argument('--no-sandbox')
25+
options.add_argument('--disable-dev-shm-usage')
26+
options.add_argument('--disable-gpu')
27+
options.add_argument('--headless')
28+
options.add_argument("--window-size=1024,768")
29+
super().__init__(options=options)
30+
31+
def get_file(self, filepath):
32+
self.clean_window()
33+
super().get('file://' + filepath)
34+
35+
def clean_window(self):
36+
"""Make sure we have a fresh window (without restarting the browser)."""
37+
# open new tab
38+
self.execute_script('window.open();')
39+
# close old tab
40+
self.close()
41+
# switch to new tab
42+
self.switch_to.window(self.window_handles[0])
43+
44+
def verify_js_logs(self):
45+
"""Raise an error if there are errors in the browser JS console."""
46+
logs = self.get_log('browser')
47+
for log in logs:
48+
if log['level'] == 'SEVERE':
49+
msg = ' '.join(log['message'].split()[2:])
50+
raise RuntimeError('Javascript error: "{}".'.format(msg))
51+
52+
def wait_until(self, css_selector, timeout=10):
53+
"""Wait for and return the element(s) selected by css_selector."""
54+
wait = WebDriverWait(self, timeout=timeout)
55+
is_visible = visibility_of_element_located((By.CSS_SELECTOR, css_selector))
56+
return wait.until(is_visible)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import base64
2+
import os
3+
4+
import folium
5+
from folium.plugins.heat_map import HeatMap
6+
from folium.utilities import temp_html_filepath
7+
8+
9+
def test_heat_map_with_weights(driver):
10+
"""Verify that HeatMap uses weights in data correctly.
11+
12+
This test will fail in non-headless mode because window size will be different.
13+
14+
"""
15+
m = folium.Map((0.5, 0.5), zoom_start=8, tiles=None)
16+
HeatMap(
17+
# make four dots with different weights: 1, 1, 1.5 and 2.
18+
data=[
19+
(0, 0, 1.5),
20+
(0, 1, 1),
21+
(1, 0, 1),
22+
(1, 1, 2),
23+
],
24+
radius=70,
25+
blur=50,
26+
).add_to(m)
27+
html = m.get_root().render()
28+
with temp_html_filepath(html) as filepath:
29+
driver.get_file(filepath)
30+
assert driver.wait_until('.folium-map')
31+
driver.verify_js_logs()
32+
canvas = driver.wait_until('canvas.leaflet-heatmap-layer')
33+
assert canvas
34+
# get the canvas as a PNG base64 string
35+
canvas_base64 = driver.execute_script(
36+
"return arguments[0].toDataURL('image/png').substring(21);", canvas)
37+
screenshot = base64.b64decode(canvas_base64)
38+
path = os.path.dirname(__file__)
39+
with open(os.path.join(path, 'test_heat_map_selenium_screenshot.png'), 'rb') as f:
40+
screenshot_expected = f.read()
41+
if hash(screenshot) != hash(screenshot_expected):
42+
print(screenshot)
43+
assert False, 'screenshot is not as expected'
96 KB
Loading

tests/selenium/test_selenium.py

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,9 @@
77

88
import nbconvert
99
import pytest
10-
from selenium.webdriver import Chrome, ChromeOptions
1110
from selenium.common.exceptions import UnexpectedAlertPresentException
12-
from selenium.webdriver.common.by import By
13-
from selenium.webdriver.support.ui import WebDriverWait
14-
from selenium.webdriver.support.expected_conditions import visibility_of_element_located
1511

16-
17-
def create_driver():
18-
"""Create a Selenium WebDriver instance."""
19-
options = ChromeOptions()
20-
options.add_argument('--no-sandbox')
21-
options.add_argument('--disable-dev-shm-usage')
22-
options.add_argument('--disable-gpu')
23-
options.add_argument('--headless')
24-
driver = Chrome(options=options)
25-
return driver
26-
27-
28-
@pytest.fixture(scope='module')
29-
def driver():
30-
"""Pytest fixture that yields a Selenium WebDriver instance"""
31-
driver = create_driver()
32-
try:
33-
yield driver
34-
finally:
35-
driver.quit()
36-
37-
38-
def clean_window(driver):
39-
# open new tab
40-
driver.execute_script('window.open();')
41-
# close old tab
42-
driver.close()
43-
# switch to new tab
44-
driver.switch_to.window(driver.window_handles[0])
12+
from folium.utilities import temp_html_filepath
4513

4614

4715
def find_notebooks():
@@ -59,22 +27,15 @@ def find_notebooks():
5927
@pytest.mark.parametrize('filepath', find_notebooks())
6028
def test_notebook(filepath, driver):
6129
for filepath_html in get_notebook_html(filepath):
62-
clean_window(driver)
63-
driver.get('file://' + filepath_html)
64-
wait = WebDriverWait(driver, timeout=10)
65-
map_is_visible = visibility_of_element_located((By.CSS_SELECTOR, '.folium-map'))
30+
driver.get_file(filepath_html)
6631
try:
67-
assert wait.until(map_is_visible)
32+
assert driver.wait_until('.folium-map')
6833
except UnexpectedAlertPresentException:
6934
# in Plugins.ipynb we get an alert about geolocation permission
7035
# for some reason it cannot be closed or avoided, so just ignore it
7136
print('skipping', filepath_html, 'because of alert')
7237
continue
73-
logs = driver.get_log('browser')
74-
for log in logs:
75-
if log['level'] == 'SEVERE':
76-
msg = ' '.join(log['message'].split()[2:])
77-
raise RuntimeError('Javascript error: "{}".'.format(msg))
38+
driver.verify_js_logs()
7839

7940

8041
def get_notebook_html(filepath_notebook, execute=True):
@@ -93,15 +54,9 @@ def get_notebook_html(filepath_notebook, execute=True):
9354
parser.feed(body)
9455
iframes = parser.iframes
9556

96-
for i, iframe in enumerate(iframes):
97-
filepath_html = filepath_notebook.replace('.ipynb', '.{}.html'.format(i))
98-
filepath_html = os.path.abspath(filepath_html)
99-
with open(filepath_html, 'wb') as f:
100-
f.write(iframe)
101-
try:
57+
for iframe in iframes:
58+
with temp_html_filepath(iframe) as filepath_html:
10259
yield filepath_html
103-
finally:
104-
os.remove(filepath_html)
10560

10661

10762
class IframeParser(HTMLParser):

0 commit comments

Comments
 (0)