diff --git a/.github/workflows/test_code.yml b/.github/workflows/test_code.yml
index ee75fe41e3..1a17de5940 100644
--- a/.github/workflows/test_code.yml
+++ b/.github/workflows/test_code.yml
@@ -34,4 +34,4 @@ jobs:
run: python -m pip install -e . --no-deps --force-reinstall
- name: Code tests
- run: python -m pytest -vv --ignore=tests/selenium
+ run: python -m pytest -vv --ignore=tests/selenium --ignore=tests/streamlit
diff --git a/.github/workflows/test_latest_branca.yml b/.github/workflows/test_latest_branca.yml
index 73b0254027..77287c6396 100644
--- a/.github/workflows/test_latest_branca.yml
+++ b/.github/workflows/test_latest_branca.yml
@@ -31,4 +31,4 @@ jobs:
run: |
micromamba remove branca --yes --force
python -m pip install git+https://github.com/python-visualization/branca.git
- python -m pytest -vv --ignore=tests/selenium
+ python -m pytest -vv --ignore=tests/selenium --ignore=tests/streamlit
diff --git a/.github/workflows/test_playwright.yml b/.github/workflows/test_playwright.yml
new file mode 100644
index 0000000000..08656c1ed0
--- /dev/null
+++ b/.github/workflows/test_playwright.yml
@@ -0,0 +1,56 @@
+# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Playwright tests
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.9", "3.13"]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - uses: actions/setup-node@v3
+ with:
+ node-version: 20
+ - name: Install python dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install flake8 pytest pytest-playwright
+ if [ -f tests/streamlit/requirements.txt ]; then pip install -r tests/streamlit/requirements.txt; fi
+ pip install -e . --force-reinstall
+ - uses: pre-commit/action@v2.0.3
+ - name: Install playwright dependencies
+ run: |
+ playwright install --with-deps
+ - name: Install annotate-failures-plugin
+ run: pip install pytest-github-actions-annotate-failures
+
+ - name: Test with pytest and retry flaky tests up to 3 times
+ run: |
+ pytest --browser chromium -s --reruns 3 --junit-xml=test-results.xml tests/streamlit
+
+ - name: Surface failing tests
+ if: always()
+ uses: pmeier/pytest-results-action@main
+ with:
+ path: test-results.xml
+ fail-on-empty: false
+
+ - uses: actions/upload-artifact@v3
+ if: failure()
+ with:
+ name: screenshots
+ path: screenshot*.png
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e06a532635..ae3241944f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,6 +9,7 @@ repos:
- id: debug-statements
- id: end-of-file-fixer
- id: check-docstring-first
+ exclude: examples/.*
- id: check-added-large-files
- id: requirements-txt-fixer
- id: file-contents-sorter
diff --git a/examples/pages/draw_support.py b/examples/pages/draw_support.py
new file mode 100644
index 0000000000..74252c3999
--- /dev/null
+++ b/examples/pages/draw_support.py
@@ -0,0 +1,39 @@
+import streamlit as st
+
+st.set_page_config(
+ page_title="streamlit-folium documentation: Draw Support",
+ page_icon=":pencil:",
+ layout="wide",
+)
+
+"""
+# streamlit-folium: Draw Support
+
+Folium supports some of the [most popular leaflet
+plugins](https://python-visualization.github.io/folium/plugins.html). In this example,
+we can add the
+[`Draw`](https://python-visualization.github.io/folium/plugins.html#folium.plugins.Draw)
+plugin to our map, which allows for drawing geometric shapes on the map.
+
+When a shape is drawn on the map, the coordinates that represent that shape are passed
+back as a geojson feature via the `all_drawings` and `last_active_drawing` data fields.
+
+Draw something below to see the return value back to Streamlit!
+"""
+
+with st.echo(code_location="below"):
+ import streamlit as st
+ from streamlit_folium import st_folium
+
+ import folium
+ from folium.plugins import Draw
+
+ m = folium.Map(location=[39.949610, -75.150282], zoom_start=5)
+ Draw(export=True).add_to(m)
+
+ c1, c2 = st.columns(2)
+ with c1:
+ output = st_folium(m, width=700, height=500)
+
+ with c2:
+ st.write(output)
diff --git a/examples/pages/dynamic_layer_control.py b/examples/pages/dynamic_layer_control.py
new file mode 100644
index 0000000000..f452ae37a4
--- /dev/null
+++ b/examples/pages/dynamic_layer_control.py
@@ -0,0 +1,86 @@
+from __future__ import annotations
+
+import geopandas as gpd
+import shapely
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+import folium.features
+
+st.set_page_config(layout="wide")
+
+st.write("## Dynamic layer control updates")
+
+START_LOCATION = [37.7944347109497, -122.398077892527]
+START_ZOOM = 17
+
+if "feature_group" not in st.session_state:
+ st.session_state["feature_group"] = None
+
+wkt1 = (
+ "POLYGON ((-122.399077892527 37.7934347109497, -122.398922660838 "
+ "37.7934544916178, -122.398980265018 37.7937266504805, -122.399133972495 "
+ "37.7937070646238, -122.399077892527 37.7934347109497))"
+)
+wkt2 = (
+ "POLYGON ((-122.397416 37.795017, -122.397137 37.794712, -122.396332 37.794983,"
+ " -122.396171 37.795483, -122.396858 37.795695, -122.397652 37.795466, "
+ "-122.397759 37.79511, -122.397416 37.795017))"
+)
+
+polygon_1 = shapely.wkt.loads(wkt1)
+polygon_2 = shapely.wkt.loads(wkt2)
+
+gdf1 = gpd.GeoDataFrame(geometry=[polygon_1]).set_crs(epsg=4326)
+gdf2 = gpd.GeoDataFrame(geometry=[polygon_2]).set_crs(epsg=4326)
+
+style_parcels = {
+ "fillColor": "#1100f8",
+ "color": "#1100f8",
+ "fillOpacity": 0.13,
+ "weight": 2,
+}
+style_buildings = {
+ "color": "#ff3939",
+ "fillOpacity": 0,
+ "weight": 3,
+ "opacity": 1,
+ "dashArray": "5, 5",
+}
+
+polygon_folium1 = folium.GeoJson(data=gdf1, style_function=lambda x: style_parcels)
+polygon_folium2 = folium.GeoJson(data=gdf2, style_function=lambda x: style_buildings)
+
+map = folium.Map(
+ location=START_LOCATION,
+ zoom_start=START_ZOOM,
+ tiles="OpenStreetMap",
+ max_zoom=21,
+)
+
+fg1 = folium.FeatureGroup(name="Parcels")
+fg1.add_child(polygon_folium1)
+
+fg2 = folium.FeatureGroup(name="Buildings")
+fg2.add_child(polygon_folium2)
+
+fg_dict = {"Parcels": fg1, "Buildings": fg2, "None": None, "Both": [fg1, fg2]}
+
+control = folium.LayerControl(collapsed=False)
+
+fg = st.radio("Feature Group", ["Parcels", "Buildings", "None", "Both"])
+
+layer = st.radio("Layer Control", ["yes", "no"])
+
+layer_dict = {"yes": control, "no": None}
+
+st_folium(
+ map,
+ width=800,
+ height=450,
+ returned_objects=[],
+ feature_group_to_add=fg_dict[fg],
+ debug=True,
+ layer_control=layer_dict[layer],
+)
diff --git a/examples/pages/dynamic_map_vs_rerender.py b/examples/pages/dynamic_map_vs_rerender.py
new file mode 100644
index 0000000000..11fa022356
--- /dev/null
+++ b/examples/pages/dynamic_map_vs_rerender.py
@@ -0,0 +1,82 @@
+import random
+
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+
+st.set_page_config(layout="wide")
+
+
+CENTER_START = [39.949610, -75.150282]
+ZOOM_START = 8
+
+if "center" not in st.session_state:
+ st.session_state["center"] = [39.949610, -75.150282]
+if "zoom" not in st.session_state:
+ st.session_state["zoom"] = 8
+if "markers" not in st.session_state:
+ st.session_state["markers"] = []
+
+col1, col2, col3 = st.columns(3)
+
+if col1.button("Shift center"):
+ random_shift_y = (random.random() - 0.5) * 0.3
+ random_shift_x = (random.random() - 0.5) * 0.3
+ st.session_state["center"] = [
+ st.session_state["center"][0] + random_shift_y,
+ st.session_state["center"][1] + random_shift_x,
+ ]
+
+if col2.button("Shift zoom"):
+ st.session_state["zoom"] = st.session_state["zoom"] + 1
+ if st.session_state["zoom"] >= 10:
+ st.session_state["zoom"] = 5
+
+if col3.button("Add random marker"):
+ random_lat = random.random() * 0.5 + 39.8
+ random_lon = random.random() * 0.5 - 75.2
+ random_marker = folium.Marker(
+ location=[random_lat, random_lon],
+ popup=f"Random marker at {random_lat:.2f}, {random_lon:.2f}",
+ )
+ st.session_state["markers"].append(random_marker)
+
+col1, col2 = st.columns(2)
+
+with col1:
+ "# New method"
+ "### Pass `center`, `zoom`, and `feature_group_to_add` to `st_folium`"
+ with st.echo(code_location="below"):
+ m = folium.Map(location=CENTER_START, zoom_start=8)
+ fg = folium.FeatureGroup(name="Markers")
+ for marker in st.session_state["markers"]:
+ fg.add_child(marker)
+
+ st_folium(
+ m,
+ center=st.session_state["center"],
+ zoom=st.session_state["zoom"],
+ key="new",
+ feature_group_to_add=fg,
+ height=400,
+ width=700,
+ )
+
+with col2:
+ "# Old method"
+ "### Update the map before passing it to `st_folium`"
+ with st.echo(code_location="below"):
+ m = folium.Map(
+ location=st.session_state["center"], zoom_start=st.session_state["zoom"]
+ )
+ fg = folium.FeatureGroup(name="Markers")
+ for marker in st.session_state["markers"]:
+ fg.add_child(marker)
+ m.add_child(fg)
+ st_folium(
+ m,
+ key="old",
+ height=400,
+ width=700,
+ )
diff --git a/examples/pages/dynamic_updates.py b/examples/pages/dynamic_updates.py
new file mode 100644
index 0000000000..817bbf99bd
--- /dev/null
+++ b/examples/pages/dynamic_updates.py
@@ -0,0 +1,184 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+import geopandas as gpd
+import pandas as pd
+import requests
+import shapely
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+import folium.features
+
+p = Path(__file__).parent / "states.csv"
+STATE_DATA = pd.read_csv(p)
+
+
+st.set_page_config(layout="wide")
+
+"# Dynamic Updates -- Click on a marker"
+
+st.subheader(
+ "Use new arguments `center`, `zoom`, and `feature_group_to_add` to update the map "
+ "without re-rendering it."
+)
+
+
+@st.cache_data
+def _get_all_state_bounds() -> dict:
+ url = "https://raw.githubusercontent.com/PublicaMundi/MappingAPI/master/data/geojson/us-states.json"
+ data = requests.get(url).json()
+ return data
+
+
+@st.cache_data
+def get_state_bounds(state: str) -> dict:
+ data = _get_all_state_bounds()
+ state_entry = [f for f in data["features"] if f["properties"]["name"] == state][0]
+ return {"type": "FeatureCollection", "features": [state_entry]}
+
+
+def get_state_from_lat_lon(lat: float, lon: float) -> str:
+ state_row = STATE_DATA[
+ STATE_DATA.latitude.between(lat - 0.0001, lat + 0.0001)
+ & STATE_DATA.longitude.between(lon - 0.0001, lon + 0.0001)
+ ].iloc[0]
+ return state_row["state"]
+
+
+def get_population(state: str) -> int:
+ return STATE_DATA.set_index("state").loc[state]["population"]
+
+
+def main():
+ if "last_object_clicked" not in st.session_state:
+ st.session_state["last_object_clicked"] = None
+ if "selected_state" not in st.session_state:
+ st.session_state["selected_state"] = "Indiana"
+
+ bounds = get_state_bounds(st.session_state["selected_state"])
+
+ st.write(f"## {st.session_state['selected_state']}")
+ population = get_population(st.session_state["selected_state"])
+ st.write(f"### Population: {population:,}")
+
+ center = None
+ if st.session_state["last_object_clicked"]:
+ center = st.session_state["last_object_clicked"]
+
+ with st.echo(code_location="below"):
+ m = folium.Map(location=[39.8283, -98.5795], zoom_start=5)
+
+ # If you want to dynamically add or remove items from the map,
+ # add them to a FeatureGroup and pass it to st_folium
+ fg = folium.FeatureGroup(name="State bounds")
+ fg.add_child(folium.features.GeoJson(bounds))
+
+ capitals = STATE_DATA
+
+ for capital in capitals.itertuples():
+ fg.add_child(
+ folium.Marker(
+ location=[capital.latitude, capital.longitude],
+ popup=f"{capital.capital}, {capital.state}",
+ tooltip=f"{capital.capital}, {capital.state}",
+ icon=(
+ folium.Icon(color="green")
+ if capital.state == st.session_state["selected_state"]
+ else None
+ ),
+ )
+ )
+
+ out = st_folium(
+ m,
+ feature_group_to_add=fg,
+ center=center,
+ width=1200,
+ height=500,
+ )
+
+ if (
+ out["last_object_clicked"]
+ and out["last_object_clicked"] != st.session_state["last_object_clicked"]
+ ):
+ st.session_state["last_object_clicked"] = out["last_object_clicked"]
+ state = get_state_from_lat_lon(*out["last_object_clicked"].values())
+ st.session_state["selected_state"] = state
+ st.rerun()
+
+ st.write("## Dynamic feature group updates")
+
+ START_LOCATION = [37.7944347109497, -122.398077892527]
+ START_ZOOM = 17
+
+ if "feature_group" not in st.session_state:
+ st.session_state["feature_group"] = None
+
+ wkt1 = (
+ "POLYGON ((-122.399077892527 37.7934347109497, -122.398922660838 "
+ "37.7934544916178, -122.398980265018 37.7937266504805, -122.399133972495 "
+ "37.7937070646238, -122.399077892527 37.7934347109497))"
+ )
+ wkt2 = (
+ "POLYGON ((-122.397416 37.795017, -122.397137 37.794712, -122.396332 37.794983,"
+ " -122.396171 37.795483, -122.396858 37.795695, -122.397652 37.795466, "
+ "-122.397759 37.79511, -122.397416 37.795017))"
+ )
+
+ polygon_1 = shapely.wkt.loads(wkt1)
+ polygon_2 = shapely.wkt.loads(wkt2)
+
+ gdf1 = gpd.GeoDataFrame(geometry=[polygon_1]).set_crs(epsg=4326)
+ gdf2 = gpd.GeoDataFrame(geometry=[polygon_2]).set_crs(epsg=4326)
+
+ style_parcels = {
+ "fillColor": "#1100f8",
+ "color": "#1100f8",
+ "fillOpacity": 0.13,
+ "weight": 2,
+ }
+ style_buildings = {
+ "color": "#ff3939",
+ "fillOpacity": 0,
+ "weight": 3,
+ "opacity": 1,
+ "dashArray": "5, 5",
+ }
+
+ polygon_folium1 = folium.GeoJson(data=gdf1, style_function=lambda x: style_parcels)
+ polygon_folium2 = folium.GeoJson(
+ data=gdf2, style_function=lambda x: style_buildings
+ )
+
+ map = folium.Map(
+ location=START_LOCATION,
+ zoom_start=START_ZOOM,
+ tiles="OpenStreetMap",
+ max_zoom=21,
+ )
+
+ fg1 = folium.FeatureGroup(name="Parcels")
+ fg1.add_child(polygon_folium1)
+
+ fg2 = folium.FeatureGroup(name="Buildings")
+ fg2.add_child(polygon_folium2)
+
+ fg_dict = {"Parcels": fg1, "Buildings": fg2, "None": None, "Both": [fg1, fg2]}
+
+ fg = st.radio("Feature Group", ["Parcels", "Buildings", "None", "Both"])
+
+ st_folium(
+ map,
+ width=800,
+ height=450,
+ returned_objects=[],
+ feature_group_to_add=fg_dict[fg],
+ debug=True,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/pages/geojson_popup.py b/examples/pages/geojson_popup.py
new file mode 100644
index 0000000000..91537a388f
--- /dev/null
+++ b/examples/pages/geojson_popup.py
@@ -0,0 +1,119 @@
+import branca
+import geopandas
+import pandas as pd
+import requests
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+from folium.features import GeoJsonPopup, GeoJsonTooltip
+
+st.write("# GeoJson Popup")
+st.write(
+ "See [original](https://github.com/python-visualization/folium/blob/main/examples/GeoJsonPopupAndTooltip.ipynb)"
+)
+
+
+@st.cache_resource
+def get_df() -> pd.DataFrame:
+ response = requests.get(
+ "https://raw.githubusercontent.com/python-visualization/folium/main/examples/data/us-states.json"
+ )
+ data = response.json()
+ states = geopandas.GeoDataFrame.from_features(data, crs="EPSG:4326")
+
+ income = pd.read_csv(
+ "https://raw.githubusercontent.com/pri-data/50-states/master/data/income-counties-states-national.csv",
+ dtype={"fips": str},
+ )
+ income["income-2015"] = pd.to_numeric(income["income-2015"], errors="coerce")
+
+ response = requests.get(
+ "https://gist.githubusercontent.com/tvpmb/4734703/raw/"
+ "b54d03154c339ed3047c66fefcece4727dfc931a/US%2520State%2520List"
+ )
+ abbrs = pd.read_json(response.text)
+
+ statesmerge = states.merge(abbrs, how="left", left_on="name", right_on="name")
+ statesmerge["geometry"] = statesmerge.geometry.simplify(0.05)
+
+ income.groupby("state")["income-2015"].median().head()
+
+ statesmerge["medianincome"] = statesmerge.merge(
+ income.groupby("state")["income-2015"].median(),
+ how="left",
+ left_on="alpha-2",
+ right_on="state",
+ )["income-2015"]
+ statesmerge["change"] = statesmerge.merge(
+ income.groupby("state")["change"].median(),
+ how="left",
+ left_on="alpha-2",
+ right_on="state",
+ )["change"]
+
+ return statesmerge
+
+
+df = get_df()
+
+colormap = branca.colormap.LinearColormap(
+ vmin=df["change"].quantile(0.0),
+ vmax=df["change"].quantile(1),
+ colors=["red", "orange", "lightblue", "green", "darkgreen"],
+ caption="State Level Median County Household Income (%)",
+)
+
+m = folium.Map(location=[35.3, -97.6], zoom_start=4)
+
+popup = GeoJsonPopup(
+ fields=["name", "change"],
+ aliases=["State", "% Change"],
+ localize=True,
+ labels=True,
+ style="background-color: yellow;",
+)
+
+tooltip = GeoJsonTooltip(
+ fields=["name", "medianincome", "change"],
+ aliases=["State:", "2015 Median Income(USD):", "Median % Change:"],
+ localize=True,
+ sticky=False,
+ labels=True,
+ style="""
+ background-color: #F0EFEF;
+ border: 2px solid black;
+ border-radius: 3px;
+ box-shadow: 3px;
+ """,
+ max_width=800,
+)
+
+folium.GeoJson(
+ df,
+ style_function=lambda x: {
+ "fillColor": (
+ colormap(x["properties"]["change"])
+ if x["properties"]["change"] is not None
+ else "transparent"
+ ),
+ "color": "black",
+ "fillOpacity": 0.4,
+ },
+ tooltip=tooltip,
+ popup=popup,
+).add_to(m)
+
+colormap.add_to(m)
+
+return_on_hover = st.checkbox("Return on hover?", True)
+
+output = st_folium(m, width=700, height=500, return_on_hover=return_on_hover)
+
+left, right = st.columns(2)
+with left:
+ st.write("## Tooltip")
+ st.write(output["last_object_clicked_tooltip"])
+with right:
+ st.write("## Popup")
+ st.write(output["last_object_clicked_popup"])
diff --git a/examples/pages/geojson_styles.py b/examples/pages/geojson_styles.py
new file mode 100644
index 0000000000..0f20f66b08
--- /dev/null
+++ b/examples/pages/geojson_styles.py
@@ -0,0 +1,37 @@
+import geopandas as gpd
+import shapely
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+
+st.title("GeoJSON Styling")
+
+START_LOCATION = [37.7934347109497, -122.399077892527]
+START_ZOOM = 18
+
+wkt = (
+ "POLYGON ((-122.399077892527 37.7934347109497, -122.398922660838 "
+ "37.7934544916178, -122.398980265018 37.7937266504805, -122.399133972495 "
+ "37.7937070646238, -122.399077892527 37.7934347109497))"
+)
+polygon_ = shapely.wkt.loads(wkt)
+gdf = gpd.GeoDataFrame(geometry=[polygon_]).set_crs(epsg=4326)
+
+style_parcels = {"fillColor": "red", "fillOpacity": 0.2}
+
+polygon_folium = folium.GeoJson(data=gdf, style_function=lambda x: style_parcels)
+
+map = folium.Map(
+ location=START_LOCATION, zoom_start=START_ZOOM, tiles="OpenStreetMap", max_zoom=21
+)
+fg = folium.FeatureGroup(name="Parcels")
+fg = fg.add_child(polygon_folium)
+
+st_folium(
+ map,
+ width=800,
+ height=450,
+ feature_group_to_add=fg,
+ debug=True,
+)
diff --git a/examples/pages/grouped_layer_control.py b/examples/pages/grouped_layer_control.py
new file mode 100644
index 0000000000..f59b756a30
--- /dev/null
+++ b/examples/pages/grouped_layer_control.py
@@ -0,0 +1,34 @@
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+from folium.plugins import GroupedLayerControl
+
+st.set_page_config(
+ page_title="streamlit-folium documentation: Grouped Layer Control",
+ page_icon=":pencil:",
+ layout="wide",
+)
+
+st.title("streamlit-folium: Grouped Layer Control")
+
+m = folium.Map([40.0, 70.0], zoom_start=6)
+
+fg1 = folium.FeatureGroup(name="g1")
+fg2 = folium.FeatureGroup(name="g2")
+fg3 = folium.FeatureGroup(name="g3")
+folium.Marker([40, 74]).add_to(fg1)
+folium.Marker([38, 72]).add_to(fg2)
+folium.Marker([40, 72]).add_to(fg3)
+m.add_child(fg1)
+m.add_child(fg2)
+m.add_child(fg3)
+
+folium.LayerControl(collapsed=False).add_to(m)
+
+GroupedLayerControl(
+ groups={"groups1": [fg1, fg2]},
+ collapsed=False,
+).add_to(m)
+
+st_folium(m)
diff --git a/examples/pages/image_overlay.py b/examples/pages/image_overlay.py
new file mode 100644
index 0000000000..5d4cd07bff
--- /dev/null
+++ b/examples/pages/image_overlay.py
@@ -0,0 +1,46 @@
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+
+st.set_page_config(
+ layout="wide",
+ page_title="streamlit-folium documentation: Misc Examples",
+ page_icon="random",
+)
+"""
+# streamlit-folium: Image Overlay
+
+By default, st_folium renders images using browser image rendering mechanism.
+Use st_folium(map, pixelated=True) in order to see image pixels without resample.
+"""
+
+url_image = "https://i.postimg.cc/kG2FSxSR/image.png"
+image_bounds = [[-20.664910, -46.538223], [-20.660001, -46.532977]]
+
+m = folium.Map()
+m1 = folium.Map()
+
+folium.raster_layers.ImageOverlay(
+ image=url_image,
+ name="image overlay",
+ opacity=1,
+ bounds=image_bounds,
+).add_to(m)
+folium.raster_layers.ImageOverlay(
+ image=url_image,
+ name="image overlay",
+ opacity=1,
+ bounds=image_bounds,
+).add_to(m1)
+
+m.fit_bounds(image_bounds, padding=(0, 0))
+m1.fit_bounds(image_bounds, padding=(0, 0))
+
+col1, col2 = st.columns(2)
+with col1:
+ st.markdown("## Pixelated off")
+ st_folium(m, use_container_width=True, pixelated=False, key="pixelated_off")
+with col2:
+ st.markdown("## Pixelated on")
+ st_folium(m1, use_container_width=True, pixelated=True, key="pixelated_on")
diff --git a/examples/pages/limit_data_return.py b/examples/pages/limit_data_return.py
new file mode 100644
index 0000000000..797f28bde7
--- /dev/null
+++ b/examples/pages/limit_data_return.py
@@ -0,0 +1,55 @@
+import streamlit as st
+
+st.set_page_config(
+ page_title="streamlit-folium documentation: Limit Data Return",
+ page_icon="🤏",
+ layout="wide",
+)
+
+"""
+# streamlit-folium: Limit Data Return
+
+By default, st_folium returns quite a few data fields (zoom, bounds, last active
+drawing,
+ all drawings, etc). If you only need a subset of these fields, you can pass a list of
+ the fields you want returned to the `returned_objects` parameter.
+"""
+
+"""
+### Example: Only return the last object clicked on the map
+
+Try clicking on the tooltips below. Note that clicking elsewhere on the map, or zooming
+or scrolling will not cause the app to rerun.
+
+"""
+
+with st.echo(code_location="below"):
+ import streamlit as st
+ from streamlit_folium import st_folium
+
+ import folium
+
+ m = folium.Map(location=[39.949610, -75.150282], zoom_start=13)
+
+ folium.Marker(
+ [39.949610, -75.150282], popup="Liberty Bell", tooltip="Liberty Bell"
+ ).add_to(m)
+ folium.Marker(
+ [39.95887, -75.150026],
+ popup="Independence Hall",
+ tooltip="Independence Hall",
+ ).add_to(m)
+ folium.Marker(
+ [39.965570, -75.180966],
+ popup="Philadelphia Museum of Art",
+ tooltip="Philadelphia Museum of Art",
+ ).add_to(m)
+
+ c1, c2 = st.columns(2)
+ with c1:
+ output = st_folium(
+ m, width=700, height=500, returned_objects=["last_object_clicked"]
+ )
+
+ with c2:
+ st.write(output)
diff --git a/examples/pages/misc_examples.py b/examples/pages/misc_examples.py
new file mode 100644
index 0000000000..dba3164dda
--- /dev/null
+++ b/examples/pages/misc_examples.py
@@ -0,0 +1,73 @@
+import branca
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+import folium.plugins
+
+st.set_page_config(
+ layout="wide",
+ page_title="streamlit-folium documentation: Misc Examples",
+ page_icon="random",
+)
+
+page = st.radio("Select map type", ["Single map", "Dual map", "Branca figure"], index=0)
+
+# center on Liberty Bell, add marker
+if page == "Single map":
+ m = folium.Map(location=[39.949610, -75.150282], zoom_start=16)
+ tooltip = "Liberty Bell"
+ folium.Marker(
+ [39.949610, -75.150282], popup="Liberty Bell", tooltip=tooltip
+ ).add_to(m)
+ st.code(
+ """
+m = folium.Map(location=[39.949610, -75.150282], zoom_start=16)
+tooltip = "Liberty Bell"
+folium.Marker(
+ [39.949610, -75.150282], popup="Liberty Bell", tooltip=tooltip
+).add_to(m)
+""",
+ language="python",
+ )
+
+elif page == "Dual map":
+ m = folium.plugins.DualMap(location=[39.949610, -75.13], zoom_start=16)
+ tooltip = "Liberty Bell"
+ folium.Marker(
+ [39.949610, -75.150282], popup="Liberty Bell", tooltip=tooltip
+ ).add_to(m)
+ st.code(
+ """
+m = folium.plugins.DualMap(location=[39.949610, -75.13], zoom_start=16)
+tooltip = "Liberty Bell"
+folium.Marker(
+ [39.949610, -75.150282], popup="Liberty Bell", tooltip=tooltip
+).add_to(m)
+""",
+ language="python",
+ )
+else:
+ m = branca.element.Figure()
+ fm = folium.Map(location=[39.949610, -75.150282], zoom_start=16)
+ tooltip = "Liberty Bell"
+ folium.Marker(
+ [39.949610, -75.150282], popup="Liberty Bell", tooltip=tooltip
+ ).add_to(fm)
+ m.add_child(fm)
+ st.code(
+ """
+m = branca.element.Figure()
+fm = folium.Map(location=[39.949610, -75.150282], zoom_start=16)
+tooltip = "Liberty Bell"
+folium.Marker(
+ [39.949610, -75.150282], popup="Liberty Bell", tooltip=tooltip
+).add_to(fm)
+m.add_child(fm)
+""",
+ language="python",
+ )
+
+with st.echo():
+ # call to render Folium map in Streamlit
+ st_folium(m, width=2000, height=500, returned_objects=[], debug=True)
diff --git a/examples/pages/realtime.py b/examples/pages/realtime.py
new file mode 100644
index 0000000000..90a7545ec6
--- /dev/null
+++ b/examples/pages/realtime.py
@@ -0,0 +1,96 @@
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+from folium.plugins import Realtime
+
+"""
+# streamlit-folium: Realtime Support
+
+streamlit-folium supports the Realtime plugin, which can pull geo data
+periodically from a datasource. The example below shows a map that
+displays the current location of the International Space Station.
+
+Since Realtime fetches data from an external source the actual
+contents of the data is unknown when creating the map. If you want
+to react to map events, such as clicking on a Feature, you can pass
+a `JsCode` object to the plugin.
+
+Inside the `JsCode` object you have access to the Streamlit object,
+which allows you to set a return value for your Streamlit app.
+"""
+
+with st.echo():
+ m = folium.Map()
+ source = folium.JsCode(
+ """
+ function(responseHandler, errorHandler) {
+ var url = 'https://api.wheretheiss.at/v1/satellites/25544';
+
+ fetch(url)
+ .then((response) => {
+ return response.json().then((data) => {
+ var { id, timestamp, longitude, latitude } = data;
+
+ return {
+ 'type': 'FeatureCollection',
+ 'features': [{
+ 'type': 'Feature',
+ 'geometry': {
+ 'type': 'Point',
+ 'coordinates': [longitude, latitude]
+ },
+ 'properties': {
+ 'id': id,
+ 'timestamp': timestamp
+ }
+ }]
+ };
+ })
+ })
+ .then(responseHandler)
+ .catch(errorHandler);
+ }
+ """
+ )
+
+ on_each_feature = folium.JsCode(
+ """
+ (feature, layer) => {
+ layer.bindTooltip(`${feature.properties.timestamp}`);
+ layer.on("click", (event) => {
+ Streamlit.setComponentValue({
+ id: feature.properties.id,
+ // Be careful, on_each_feature binds only once.
+ // You need to extract the current location from
+ // the event.
+ location: event.sourceTarget.feature.geometry
+ });
+ });
+
+ }
+ """
+ )
+
+ update_feature = folium.JsCode(
+ """
+ (feature, layer) => {
+ L.Realtime.prototype.options.updateFeature(feature, layer);
+ if(layer) {
+ layer.unbindTooltip();
+ layer.bindTooltip(`${feature.properties.timestamp}`);
+ }
+ }
+ """
+ )
+
+ Realtime(
+ source,
+ on_each_feature=on_each_feature,
+ update_feature=update_feature,
+ interval=10000,
+ ).add_to(m)
+
+ data = st_folium(m, returned_objects=[], debug=False)
+
+ st.write(data)
diff --git a/examples/pages/responsive.py b/examples/pages/responsive.py
new file mode 100644
index 0000000000..384246ea08
--- /dev/null
+++ b/examples/pages/responsive.py
@@ -0,0 +1,26 @@
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+
+st.set_page_config(layout="wide")
+
+m = folium.Map(location=(45, -90), zoom_start=5)
+
+left, right = st.columns([2, 1])
+with left:
+ st_folium(m, use_container_width=True, key="1", debug=True)
+with right:
+ st_folium(m, use_container_width=True, key="2", debug=True)
+
+st_folium(m, use_container_width=True, key="3", debug=True)
+
+
+col1, col2, col3 = st.columns([1, 2, 3])
+
+with col1:
+ st_folium(m, use_container_width=True, key="4", debug=True)
+with col2:
+ st_folium(m, use_container_width=True, key="5", debug=True)
+with col3:
+ st_folium(m, use_container_width=True, key="6", debug=True)
diff --git a/examples/pages/simple_popup.py b/examples/pages/simple_popup.py
new file mode 100644
index 0000000000..e6da41ca69
--- /dev/null
+++ b/examples/pages/simple_popup.py
@@ -0,0 +1,29 @@
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+from folium.features import Marker, Popup
+
+st.write("# Simple Popup & Tooltip")
+
+return_on_hover = st.checkbox("Return on hover?")
+
+with st.echo("below"):
+ m = folium.Map(location=[45, -122], zoom_start=4)
+
+ Marker(
+ location=[45.5, -122],
+ popup=Popup("Popup!", parse_html=False),
+ tooltip="Tooltip!",
+ ).add_to(m)
+
+ Marker(
+ location=[45.5, -112],
+ popup=Popup("Popup 2!", parse_html=False),
+ tooltip="Tooltip 2!",
+ ).add_to(m)
+
+ out = st_folium(m, height=200, return_on_hover=return_on_hover)
+
+ st.write("Popup:", out["last_object_clicked_popup"])
+ st.write("Tooltip:", out["last_object_clicked_tooltip"])
diff --git a/examples/pages/states.csv b/examples/pages/states.csv
new file mode 100644
index 0000000000..10f1ebc9a4
--- /dev/null
+++ b/examples/pages/states.csv
@@ -0,0 +1,51 @@
+state,capital,latitude,longitude,population
+Alabama,Montgomery,32.377716,-86.300568,4893186
+Alaska,Juneau,58.301598,-134.420212,736990
+Arizona,Phoenix,33.448143,-112.096962,7174064
+Arkansas,Little Rock,34.746613,-92.288986,3011873
+California,Sacramento,38.576668,-121.493629,39346023
+Colorado,Denver,39.739227,-104.984856,5684926
+Connecticut,Hartford
,41.764046,-72.682198,3570549
+Delaware,Dover,39.157307,-75.519722,967679
+Hawaii,Honolulu,21.307442,-157.857376,1420074
+Florida,Tallahassee,30.438118,-84.281296,21216924
+Georgia,Atlanta
,33.749027,-84.388229,10516579
+Idaho,Boise,43.617775,-116.199722,1754367
+Illinois,Springfield,39.798363,-89.654961,12716164
+Indiana,Indianapolis,39.768623,-86.162643,6696893
+Iowa,Des Moines,41.591087,-93.603729,3150011
+Kansas,Topeka,39.048191,-95.677956,2912619
+Kentucky,Frankfort,38.186722,-84.875374,4461952
+Louisiana,Baton Rouge,30.457069,-91.187393,4664616
+Maine,Augusta,44.307167,-69.781693,1340825
+Maryland,Annapolis,38.978764,-76.490936,6037624
+Massachusetts,Boston,42.358162,-71.063698,6873003
+Michigan,Lansing,42.733635,-84.555328,9973907
+Minnesota,St. Paul,44.955097,-93.102211,5600166
+Mississippi,Jackson,32.303848,-90.182106,2981835
+Missouri,Jefferson City,38.579201,-92.172935,6124160
+Montana,Helena,46.585709,-112.018417,1061705
+Nebraska,Lincoln,40.808075,-96.699654,1923826
+Nevada,Carson City,39.163914,-119.766121,3030281
+New Hampshire,Concord,43.206898,-71.537994,1355244
+New Jersey,Trenton,40.220596,-74.769913,8885418
+New Mexico,Santa Fe,35.68224,-105.939728,2097021
+North Carolina,Raleigh,35.78043,-78.639099,10386227
+North Dakota,Bismarck,46.82085,-100.783318,760394
+New York,Albany,42.652843,-73.757874,19514849
+Ohio,Columbus,39.961346,-82.999069,11675275
+Oklahoma,Oklahoma City,35.492207,-97.503342,3949342
+Oregon,Salem,44.938461,-123.030403,4176346
+Pennsylvania,Harrisburg,40.264378,-76.883598,12794885
+Rhode Island,Providence,41.830914,-71.414963,1057798
+South Carolina,Columbia,34.000343,-81.033211,5091517
+South Dakota,Pierre,44.367031,-100.346405,879336
+Tennessee,Nashville,36.16581,-86.784241,6772268
+Texas,Austin,30.27467,-97.740349,28635442
+Utah,Salt Lake City,40.777477,-111.888237,3151239
+Vermont,Montpelier,44.262436,-72.580536,624340
+Virginia,Richmond,37.538857,-77.43364,8509358
+Washington,Olympia,47.035805,-122.905014,7512465
+West Virginia,Charleston,38.336246,-81.612328,1807426
+Wisconsin,Madison,43.074684,-89.384445,5806975
+Wyoming,Cheyenne,41.140259,-104.820236,581348
diff --git a/examples/pages/static_map.py b/examples/pages/static_map.py
new file mode 100644
index 0000000000..bd44c4ac40
--- /dev/null
+++ b/examples/pages/static_map.py
@@ -0,0 +1,35 @@
+import streamlit as st
+
+st.set_page_config(
+ page_title="streamlit-folium documentation: Static Map",
+ page_icon=":ice:",
+ layout="wide",
+)
+
+"""
+# streamlit-folium: Non-interactive Map
+
+If you don't need any data returned from the map, you can just
+pass returned_objects=[] to st_folium. The streamlit app will not rerun
+when the user interacts with the map, and you will not get any data back from the map.
+
+---
+
+"""
+"### Basic `returned_objects=[]` Example"
+
+with st.echo():
+ import streamlit as st
+ from streamlit_folium import st_folium
+
+ import folium
+
+ # center on Liberty Bell, add marker
+ m = folium.Map(location=[39.949610, -75.150282], zoom_start=16)
+ folium.Marker(
+ [39.949610, -75.150282], popup="Liberty Bell", tooltip="Liberty Bell"
+ ).add_to(m)
+
+ # call to render Folium map in Streamlit, but don't get any data back
+ # from the map (so that it won't rerun the app when the user interacts)
+ st_folium(m, width=725, returned_objects=[])
diff --git a/examples/pages/vector_grid.py b/examples/pages/vector_grid.py
new file mode 100644
index 0000000000..7913909032
--- /dev/null
+++ b/examples/pages/vector_grid.py
@@ -0,0 +1,17 @@
+import streamlit as st
+from streamlit_folium import st_folium
+
+import folium
+from folium.plugins import VectorGridProtobuf
+
+st.set_page_config(
+ page_title="streamlit-folium documentation: Vector Grid",
+)
+
+with st.echo(code_location="below"):
+ m = folium.Map(location=(30, 20), zoom_start=4)
+ folium.Marker(location=(30, 20), popup="test").add_to(m)
+ url = "https://area.uqcom.jp/api2/rakuten/{z}/{x}/{y}.mvt"
+ vc = VectorGridProtobuf(url, "test").add_to(m)
+
+ st_folium(m, returned_objects=[])
diff --git a/examples/streamlit_app.py b/examples/streamlit_app.py
new file mode 100644
index 0000000000..f9f67dd0df
--- /dev/null
+++ b/examples/streamlit_app.py
@@ -0,0 +1,80 @@
+import streamlit as st
+
+st.set_page_config(
+ page_title="streamlit-folium documentation",
+ page_icon=":world_map:️",
+ layout="wide",
+)
+
+"# streamlit-folium"
+
+"""streamlit-folium integrates two great open-source projects in the Python ecosystem:
+[Streamlit](https://streamlit.io) and
+[Folium](https://python-visualization.github.io/folium/)!"""
+
+"""
+Currently, there are two functions defined:
+
+- `st_folium()`: a bi-directional Component, taking a Folium/Branca object and plotting
+ to the Streamlit app. Upon mount/interaction with the Streamlit app, st_folium()
+ returns a Dict with selected information including the bounding box and items clicked
+ on
+
+- `folium_static()`: takes a folium.Map, folium.Figure, or branca.element.Figure object
+ and displays it in a Streamlit app.
+"""
+
+"""
+On its own, Folium is limited to _display-only_ visualizations; the Folium API generates
+the proper [leaflet.js](https://leafletjs.com/) specification, as HTML and displays it.
+Some interactivity is provided (depending on how the Folium API is utilized), but the
+biggest drawback is that the interactivity from the visualization isn't passed back to
+Python, and as such, you can't make full use of the functionality provided by the
+leaflet.js library.
+
+`streamlit-folium` builds upon the convenient [Folium
+API](https://python-visualization.github.io/folium/modules.html) for building geospatial
+visualizations by adding a _bi-directional_ data transfer functionality. This not only
+allows for increased interactivity between the web browser and Python, but also the use
+of larger datasets through intelligent querying.
+
+### Bi-directional data model
+"""
+left, right = st.columns(2)
+
+
+with left:
+ """
+ If we take a look at the example from the Home page, it might seem trivial. We
+ define a single point with a marker and pop-up and display it:
+ """
+ with st.echo():
+ import streamlit as st
+ from streamlit_folium import st_folium
+
+ import folium
+
+ # center on Liberty Bell, add marker
+ m = folium.Map(location=[39.949610, -75.150282], zoom_start=16)
+ folium.Marker(
+ [39.949610, -75.150282], popup="Liberty Bell", tooltip="Liberty Bell"
+ ).add_to(m)
+
+ # call to render Folium map in Streamlit
+ st_data = st_folium(m, width=725)
+
+with right:
+ """
+ But behind the scenes, a lot more is happening _by default_. The return value of
+ `st_folium` is set to `st_data`, and within this Python variable is information
+ about what is being displayed on the screen:
+ """
+
+ st_data
+
+ """
+ As the user interacts with the data visualization, the values for `bounds` are
+ constantly updating, along with `zoom`. With these values available in Python, we
+ can now limit queries based on bounding box, change the marker size based on the
+ `zoom` value and much more!
+ """
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 3834e1e8ba..2523d651a0 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -28,11 +28,12 @@ pillow
pre-commit
pycodestyle
pydata-sphinx-theme
-pytest
scipy
selenium<4.27.0
setuptools_scm
sphinx
+streamlit
+streamlit-folium
types-requests
vega_datasets
vincent
diff --git a/tests/streamlit/requirements.txt b/tests/streamlit/requirements.txt
new file mode 100644
index 0000000000..888db52500
--- /dev/null
+++ b/tests/streamlit/requirements.txt
@@ -0,0 +1,6 @@
+geopandas
+pytest>=7.1.2
+pytest-playwright
+pytest-rerunfailures
+streamlit>=1.13.0,!=1.34.0
+streamlit-folium
diff --git a/tests/streamlit/test_streamlit.py b/tests/streamlit/test_streamlit.py
new file mode 100644
index 0000000000..8e57b39540
--- /dev/null
+++ b/tests/streamlit/test_streamlit.py
@@ -0,0 +1,395 @@
+from __future__ import annotations
+
+from contextlib import contextmanager
+from time import sleep
+
+import pytest
+from playwright.sync_api import Page, Response, expect
+
+LOCAL_TEST = False
+
+PORT = "8503" if LOCAL_TEST else "8699"
+
+
+@pytest.fixture(scope="module", autouse=True)
+def before_module():
+ # Run the streamlit app before each module
+ with run_streamlit():
+ yield
+
+
+@pytest.fixture(scope="function", autouse=True)
+def before_test(page: Page):
+ page.goto(f"localhost:{PORT}")
+ page.set_viewport_size({"width": 2000, "height": 2000})
+ expect.set_options(timeout=5_000)
+
+
+# Take screenshot of each page if there are failures for this session
+@pytest.fixture(scope="function", autouse=True)
+def after_test(page: Page, request):
+ yield
+ if request.node.rep_call.failed:
+ page.screenshot(path=f"screenshot-{request.node.name}.png", full_page=True)
+
+
+@contextmanager
+def run_streamlit():
+ """Run the streamlit app at examples/streamlit_app.py on port 8599"""
+ import subprocess
+
+ if LOCAL_TEST:
+ try:
+ yield 1
+ finally:
+ pass
+ else:
+ p = subprocess.Popen(
+ [
+ "streamlit",
+ "run",
+ "examples/streamlit_app.py",
+ "--server.port",
+ PORT,
+ "--server.headless",
+ "true",
+ ]
+ )
+
+ sleep(5)
+
+ try:
+ yield 1
+ finally:
+ p.kill()
+
+
+def click_button_or_marker(page: Page, nth: int = 0, locator: str | None = None):
+ """For some reason, there's a discrepancy between how the map markers are
+ selectable locally and on github actions, perhaps related some error in loading
+ the actual marker images. This tries both ways to select a marker"""
+
+ frame = page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]')
+ if locator is not None:
+ frame = frame.locator(locator)
+ try:
+ frame.get_by_role("button", name="Marker").nth(nth).click(timeout=5_000)
+ except Exception:
+ frame.get_by_role("img").nth(nth).click(timeout=5_000)
+
+
+def test_marker_click(page: Page):
+ def check_for_404(response: Response):
+ if not response.ok:
+ print(response)
+ print(response.text())
+ print(response.url)
+ print(response.status)
+ raise Exception("404")
+
+ page.on("response", check_for_404)
+
+ # Check page title
+ expect(page).to_have_title("streamlit-folium documentation")
+
+ expect(page.get_by_text('"last_object_clicked":NULL')).to_be_visible()
+
+ # Click marker
+ try:
+ click_button_or_marker(page)
+ except Exception as e:
+ page.screenshot(path="screenshot-test-marker-click.png", full_page=True)
+ raise e
+
+ expect(page.get_by_text('"last_object_clicked":NULL')).to_be_hidden()
+
+
+def test_draw(page: Page):
+ # Test draw support
+ page.get_by_role("link", name="draw support").click()
+ # Click again to see if it resolves timeout issues
+ page.get_by_role("link", name="draw support").click()
+
+ expect(page).to_have_title("streamlit-folium documentation: Draw Support")
+
+ page.frame_locator(
+ 'internal:attr=[title="streamlit_folium.st_folium"i]'
+ ).get_by_role("link", name="Draw a polygon").click()
+
+ # Should be no drawings
+ expect(page.get_by_text('"all_drawings":NULL')).to_be_visible()
+
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]').get_by_role(
+ "link", name="Draw a marker"
+ ).click()
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]').locator(
+ ".leaflet-marker-icon"
+ ).first.click()
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]').locator(
+ "#map_div"
+ ).click()
+
+ # Should be one item in drawings after having placed a marker
+ expect(page.get_by_text('"all_drawings":NULL')).to_be_hidden()
+
+
+def test_limit_data(page: Page):
+ # Test limit data support
+ page.get_by_role("link", name="limit data return").click()
+ # Click again to see if it resolves timeout issues
+ page.get_by_role("link", name="limit data return").click()
+
+ expect(page).to_have_title("streamlit-folium documentation: Limit Data Return")
+
+ expect(page.get_by_text('{"last_object_clicked":NULL}')).to_be_visible()
+
+ # Click marker
+ click_button_or_marker(page, 2)
+
+ # Have to click a second time for some reason, maybe because it doesn't load right
+ # away
+ click_button_or_marker(page, 2)
+
+ expect(page.get_by_text('{"last_object_clicked":{"lat":39.96')).to_be_visible()
+
+
+def test_dual_map(page: Page):
+ page.get_by_role("link", name="misc examples").click()
+ # Click again to see if it resolves timeout issues
+ page.get_by_role("link", name="misc examples").click()
+
+ expect(page).to_have_title("streamlit-folium documentation: Misc Examples")
+
+ page.locator("label").filter(has_text="Dual map").click()
+ page.locator("label").filter(has_text="Dual map").click()
+
+ # Click marker on left map
+ try:
+ click_button_or_marker(page, 0, "#map_div")
+ click_button_or_marker(page, 0, "#map_div2")
+ except Exception as e:
+ page.screenshot(path="screenshot-dual-map.png", full_page=True)
+ raise e
+
+
+def test_vector_grid(page: Page):
+ page.get_by_role("link", name="vector grid").click()
+ page.get_by_role("link", name="vector grid").click()
+
+ expect(page).to_have_title("streamlit-folium documentation: Vector Grid")
+
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]').locator(
+ ".leaflet-marker-icon"
+ ).click()
+
+
+def test_tooltip_click(page: Page):
+ expect(page.get_by_text('"last_object_clicked_tooltip":NULL')).to_be_visible()
+
+ # Click marker on map
+ click_button_or_marker(page)
+
+ expect(
+ page.get_by_text('"last_object_clicked_tooltip":"Liberty Bell"')
+ ).to_be_visible()
+
+
+def test_popup_text(page: Page):
+ page.get_by_role("link", name="simple popup").click()
+ page.get_by_role("link", name="simple popup").click()
+
+ expect(page.get_by_text("Popup: None")).to_be_visible()
+ expect(page.get_by_text("Tooltip: None")).to_be_visible()
+
+ click_button_or_marker(page)
+
+ try:
+ expect(page.get_by_text("Popup: Popup!")).to_be_visible()
+ expect(page.get_by_text("Tooltip: Tooltip!")).to_be_visible()
+ except Exception as e:
+ page.screenshot(path="screenshot-popup.png")
+ raise e
+
+
+def test_return_on_hover(page: Page):
+ page.get_by_role("link", name="simple popup").click()
+ page.get_by_role("link", name="simple popup").click()
+
+ expect(page.get_by_text("Popup: None")).to_be_visible()
+ expect(page.get_by_text("Tooltip: None")).to_be_visible()
+
+ page.get_by_text("Return on hover?").click()
+
+ click_button_or_marker(page, 1)
+
+ try:
+ expect(page.get_by_text("Popup: Popup 2!")).to_be_visible()
+ expect(page.get_by_text("Tooltip: Tooltip 2!")).to_be_visible()
+ except Exception as e:
+ page.screenshot(path="screenshot-popup2.png")
+ raise e
+
+
+def test_responsiveness(page: Page):
+ page.get_by_role("link", name="responsive").click()
+ page.get_by_role("link", name="responsive").click()
+
+ page.set_viewport_size({"width": 500, "height": 3000})
+
+ try:
+ initial_bbox = (
+ page.frame_locator("iframe").nth(2).locator("#map_div").bounding_box()
+ )
+ except Exception as e:
+ page.screenshot(path="screenshot-responsive.png", full_page=True)
+ raise e
+
+ page.set_viewport_size({"width": 1000, "height": 3000})
+
+ sleep(1)
+
+ new_bbox = page.query_selector_all("iframe")[2].bounding_box()
+
+ print(initial_bbox)
+ print(new_bbox)
+
+ assert initial_bbox is not None
+
+ assert new_bbox is not None
+
+ assert new_bbox["width"] > initial_bbox["width"] + 300
+
+ # Check that the iframe is reasonably tall, which makes sure it hasn't failed to
+ # render at all
+ assert new_bbox["height"] > 100
+
+ page.set_viewport_size({"width": 2000, "height": 2000})
+
+
+def test_geojson_styles(page: Page):
+ page.get_by_role("link", name="geojson styles").click()
+ page.get_by_role("link", name="geojson styles").click()
+
+ page.get_by_text("Show generated code").click()
+ expect(page.get_by_text('"fillOpacity"')).to_be_visible()
+
+
+def test_grouped_layer_control(page: Page):
+ page.get_by_role("link", name="grouped layer control").click()
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]').locator(
+ "label"
+ ).filter(has_text="g2").click()
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]').get_by_label(
+ "g2"
+ ).check()
+
+
+def test_geojson_popup(page: Page):
+ page.get_by_role("link", name="geojson popup").click()
+
+ expect(page.get_by_text("AttributeError")).to_be_hidden()
+
+
+@pytest.mark.skip(reason="too flaky")
+def test_dynamic_feature_group_update(page: Page):
+ page.get_by_role("link", name="dynamic updates").click()
+ page.get_by_text("Show generated code").click()
+
+ # Test showing only Parcel layer
+ page.get_by_test_id("stRadio").get_by_text("Parcels").click()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"] >> nth=1')
+ .locator("path")
+ .first
+ ).to_be_visible()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"] >> nth=1')
+ .get_by_role("img")
+ .locator("svg")
+ ).to_be_hidden()
+ expect(
+ page.get_by_text('"fillColor"')
+ ).to_be_visible() # fillColor only present in parcel style
+ expect(
+ page.get_by_text('"dashArray"')
+ ).to_be_hidden() # dashArray only present in building style
+
+ # Test showing only Building layer
+ page.get_by_test_id("stRadio").get_by_text("Buildings").click()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"] >> nth=1')
+ .locator("path")
+ .first
+ ).to_be_visible()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"] >> nth=1')
+ .get_by_role("img")
+ .locator("svg")
+ ).to_be_hidden()
+ expect(page.get_by_text("fillColor")).to_be_hidden()
+ expect(page.get_by_text("dashArray")).to_be_visible()
+
+ # Test showing no layers
+ page.get_by_test_id("stRadio").get_by_text("None").click()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"] >> nth=1')
+ .get_by_role("img")
+ .locator("svg")
+ ).to_be_hidden()
+ expect(page.get_by_text("fillColor")).to_be_hidden()
+ expect(page.get_by_text("dashArray")).to_be_hidden()
+
+ # Test showing both layers
+ page.get_by_test_id("stRadio").get_by_text("Both").click()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"] >> nth=1')
+ .locator("path")
+ .first
+ ).to_be_visible()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"] >> nth=1')
+ .locator("path")
+ .nth(1)
+ ).to_be_visible()
+ expect(page.get_by_text("fillColor")).to_be_visible()
+ expect(page.get_by_text("dashArray")).to_be_visible()
+
+
+def test_layer_control_dynamic_update(page: Page):
+ page.get_by_role("link", name="dynamic layer control").click()
+ # page.get_by_text("Show generated code").click()
+
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]').get_by_text(
+ "Parcels"
+ ).click()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]').get_by_text(
+ "Parcels"
+ )
+ ).not_to_be_checked()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]')
+ .get_by_role("img")
+ .locator("svg")
+ ).to_be_hidden()
+ expect(page.get_by_text("dashArray")).to_be_hidden()
+
+ page.get_by_test_id("stRadio").get_by_text("Both").click()
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]').get_by_text(
+ "Parcels"
+ ).click()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]').get_by_text(
+ "Parcels"
+ )
+ ).not_to_be_checked()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]').get_by_text(
+ "Buildings"
+ )
+ ).to_be_checked()
+ expect(
+ page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]')
+ .get_by_role("img")
+ .locator("path")
+ ).to_be_visible()