Skip to content

Commit 5ce3bd5

Browse files
committed
fix: use viewport units for percentage map dimensions (fixes #2186)
1 parent 720ee82 commit 5ce3bd5

2 files changed

Lines changed: 168 additions & 25 deletions

File tree

folium/folium.py

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55

66
import time
77
import webbrowser
8-
from collections.abc import Sequence
9-
from typing import Any, Optional, Union
8+
from typing import Any, List, Optional, Sequence, Union
109

1110
from branca.element import Element, Figure
1211

@@ -64,12 +63,14 @@
6463

6564

6665
class GlobalSwitches(Element):
67-
_template = Template("""
66+
_template = Template(
67+
"""
6868
<script>
6969
L_NO_TOUCH = {{ this.no_touch |tojson}};
7070
L_DISABLE_3D = {{ this.disable_3d|tojson }};
7171
</script>
72-
""")
72+
"""
73+
)
7374

7475
def __init__(self, no_touch=False, disable_3d=False):
7576
super().__init__()
@@ -176,15 +177,26 @@ class Map(JSCSSMixin, Evented):
176177
177178
""" # noqa
178179

179-
_template = Template("""
180+
_template = Template(
181+
"""
180182
{% macro header(this, kwargs) %}
181183
<meta name="viewport" content="width=device-width,
182184
initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
183185
<style>
184186
#{{ this.get_name() }} {
185187
position: {{this.position}};
188+
{%- if this._width_is_percent %}
189+
width: {{this.width[0]}}vw;
190+
{%- else %}
186191
width: {{this.width[0]}}{{this.width[1]}};
192+
min-width: {{this.width[0]}}{{this.width[1]}};
193+
{%- endif %}
194+
{%- if this._height_is_percent %}
195+
height: {{this.height[0]}}vh;
196+
{%- else %}
187197
height: {{this.height[0]}}{{this.height[1]}};
198+
min-height: {{this.height[0]}}{{this.height[1]}};
199+
{%- endif %}
188200
left: {{this.left[0]}}{{this.left[1]}};
189201
top: {{this.top[0]}}{{this.top[1]}};
190202
}
@@ -199,15 +211,6 @@ class Map(JSCSSMixin, Evented):
199211
}
200212
</style>
201213
202-
<style>#map {
203-
position:absolute;
204-
top:0;
205-
bottom:0;
206-
right:0;
207-
left:0;
208-
}
209-
</style>
210-
211214
<script>
212215
L_NO_TOUCH = {{ this.global_switches.no_touch |tojson}};
213216
L_DISABLE_3D = {{ this.global_switches.disable_3d|tojson }};
@@ -249,7 +252,8 @@ class Map(JSCSSMixin, Evented):
249252
{%- endif %}
250253
251254
{% endmacro %}
252-
""")
255+
"""
256+
)
253257

254258
# use the module variables for backwards compatibility
255259
default_js = _default_js
@@ -301,6 +305,10 @@ def __init__(
301305
# Map Size Parameters.
302306
self.width = _parse_size(width)
303307
self.height = _parse_size(height)
308+
309+
self._height_is_percent = self.height[1] == "%"
310+
self._width_is_percent = self.width[1] == "%"
311+
304312
self.left = _parse_size(left)
305313
self.top = _parse_size(top)
306314
self.position = position
@@ -334,7 +342,7 @@ def __init__(
334342
**kwargs,
335343
)
336344

337-
self.objects_to_stay_in_front: list[Layer] = []
345+
self.objects_to_stay_in_front: List[Layer] = []
338346

339347
if isinstance(tiles, TileLayer):
340348
self.add_child(tiles)
@@ -368,16 +376,15 @@ def _to_png(
368376
Examples
369377
--------
370378
>>> m._to_png()
371-
>>> m._to_png(delay=10) # Wait 10 seconds between render and snapshot.
379+
>>> m._to_png(time=10) # Wait 10 seconds between render and snapshot.
372380
373381
"""
374382

375383
if self._png_image is None:
376384
if driver is None:
377385
from selenium import webdriver
378-
from selenium.webdriver.firefox.options import Options
379386

380-
options = Options()
387+
options = webdriver.firefox.options.Options()
381388
options.add_argument("--headless")
382389
driver = webdriver.Firefox(options=options)
383390

@@ -392,16 +399,11 @@ def _to_png(
392399
*size,
393400
)
394401
driver.set_window_size(*window_size)
395-
from selenium.webdriver.support.ui import WebDriverWait
396-
397402
html = self.get_root().render()
398403
with temp_html_filepath(html) as fname:
399404
# We need the tempfile to avoid JS security issues.
400405
driver.get(f"file:///{fname}")
401-
WebDriverWait(driver, delay).until(
402-
lambda _driver: _driver.execute_script("return document.readyState")
403-
== "complete"
404-
)
406+
time.sleep(delay)
405407
div = driver.find_element("class name", "folium-map")
406408
png = div.screenshot_as_png
407409
driver.quit()

tests/test_issue_2186.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""
2+
Tests for issue #2186: Add better control for height when browser window
3+
is not maximized.
4+
5+
Validates that:
6+
1. Percentage heights/widths emit vh/vw viewport units instead of %
7+
2. Pixel heights/widths emit min-height/min-width to prevent collapse
8+
3. The dead `#map { position:absolute; ... }` CSS rule is removed
9+
4. All existing size formats (int, px string, % string) still parse correctly
10+
"""
11+
12+
import re
13+
import sys
14+
15+
import pytest
16+
17+
sys.path.insert(0, ".")
18+
import folium
19+
20+
21+
def _get_map_css(m: folium.Map) -> str:
22+
"""Return the CSS block for the map's own ID selector."""
23+
html = m.get_root().render()
24+
match = re.search(r"(#map_[a-f0-9]+ \{.*?\})", html, re.DOTALL)
25+
assert match, "Could not find map CSS block in rendered HTML"
26+
return match.group(1)
27+
28+
29+
# ---------------------------------------------------------------------------
30+
# Percentage → viewport units
31+
# ---------------------------------------------------------------------------
32+
33+
class TestPercentageUsesViewportUnits:
34+
def test_height_100_percent_emits_vh(self):
35+
"""The default height='100%' should use 100vh, not 100%."""
36+
m = folium.Map(location=[0, 0], height="100%")
37+
css = _get_map_css(m)
38+
assert "100.0vh" in css, f"Expected vh unit, got:\n{css}"
39+
assert "100.0%" not in css.split("position")[1], (
40+
"Should not emit bare % for height when using viewport units"
41+
)
42+
43+
def test_width_100_percent_emits_vw(self):
44+
"""width='100%' should use 100vw."""
45+
m = folium.Map(location=[0, 0], width="100%")
46+
css = _get_map_css(m)
47+
assert "100.0vw" in css, f"Expected vw unit, got:\n{css}"
48+
49+
def test_partial_percentage_height(self):
50+
"""height='80%' should emit 80vh."""
51+
m = folium.Map(location=[0, 0], height="80%", width="60%")
52+
css = _get_map_css(m)
53+
assert "80.0vh" in css, f"Expected 80vh, got:\n{css}"
54+
assert "60.0vw" in css, f"Expected 60vw, got:\n{css}"
55+
56+
def test_percentage_height_no_min_height(self):
57+
"""Viewport-unit heights don't need min-height (it's implicit via vh)."""
58+
m = folium.Map(location=[0, 0], height="100%")
59+
css = _get_map_css(m)
60+
assert "min-height" not in css
61+
62+
63+
# ---------------------------------------------------------------------------
64+
# Pixel values → min-height / min-width guards
65+
# ---------------------------------------------------------------------------
66+
67+
class TestPixelValuesGetMinConstraints:
68+
def test_pixel_string_height_gets_min_height(self):
69+
"""height='1000px' should also emit min-height: 1000px."""
70+
m = folium.Map(location=[0, 0], height="1000px")
71+
css = _get_map_css(m)
72+
assert "height: 1000.0px" in css
73+
assert "min-height: 1000.0px" in css, f"min-height missing:\n{css}"
74+
75+
def test_integer_height_gets_min_height(self):
76+
"""height=500 (integer) should emit min-height: 500px."""
77+
m = folium.Map(location=[0, 0], height=500, width=750)
78+
css = _get_map_css(m)
79+
assert "min-height: 500.0px" in css, f"min-height missing:\n{css}"
80+
assert "min-width: 750.0px" in css, f"min-width missing:\n{css}"
81+
82+
def test_pixel_string_width_gets_min_width(self):
83+
"""width='400px' should emit min-width: 400px."""
84+
m = folium.Map(location=[0, 0], width="400px")
85+
css = _get_map_css(m)
86+
assert "min-width: 400.0px" in css, f"min-width missing:\n{css}"
87+
88+
89+
# ---------------------------------------------------------------------------
90+
# Dead CSS rule removal
91+
# ---------------------------------------------------------------------------
92+
93+
class TestDeadMapRuleRemoved:
94+
def test_dead_map_id_rule_absent(self):
95+
"""The stale `#map { position:absolute; ... }` block must not appear."""
96+
m = folium.Map(location=[0, 0])
97+
html = m.get_root().render()
98+
# The old rule targeted the literal id="map" which never matched the
99+
# hashed IDs folium actually generates.
100+
assert "<style>#map {" not in html, (
101+
"Dead `#map { position:absolute; }` CSS rule should have been removed"
102+
)
103+
104+
105+
# ---------------------------------------------------------------------------
106+
# Backwards-compatibility: all existing call signatures still work
107+
# ---------------------------------------------------------------------------
108+
109+
class TestBackwardsCompatibility:
110+
def test_default_call(self):
111+
"""folium.Map() with no size args still renders."""
112+
m = folium.Map(location=[0, 0])
113+
assert m.get_root().render()
114+
115+
def test_integer_sizes(self):
116+
m = folium.Map(location=[0, 0], width=750, height=500)
117+
css = _get_map_css(m)
118+
assert "750.0px" in css
119+
assert "500.0px" in css
120+
121+
def test_px_string_sizes(self):
122+
m = folium.Map(location=[0, 0], width="800px", height="600px")
123+
css = _get_map_css(m)
124+
assert "800.0px" in css
125+
assert "600.0px" in css
126+
127+
def test_percent_string_sizes_parse(self):
128+
"""Percentage strings are accepted without raising."""
129+
m = folium.Map(location=[0, 0], width="90%", height="75%")
130+
css = _get_map_css(m)
131+
assert "90.0vw" in css
132+
assert "75.0vh" in css
133+
134+
def test_flags_set_correctly(self):
135+
m_pct = folium.Map(location=[0, 0], height="100%", width="100%")
136+
assert m_pct._height_is_percent is True
137+
assert m_pct._width_is_percent is True
138+
139+
m_px = folium.Map(location=[0, 0], height=500, width=750)
140+
assert m_px._height_is_percent is False
141+
assert m_px._width_is_percent is False

0 commit comments

Comments
 (0)