From f683989fa9e699f5be571fe24f3d1bc0b1f9becc Mon Sep 17 00:00:00 2001 From: Navxihziq Date: Sun, 16 Jun 2024 02:36:15 -0400 Subject: [PATCH 1/4] Fix dynamic JS import and add missing scripts for colormaps --- streamlit_folium/__init__.py | 10 +- streamlit_folium/frontend/src/index.tsx | 155 +++++++++--------------- 2 files changed, 64 insertions(+), 101 deletions(-) diff --git a/streamlit_folium/__init__.py b/streamlit_folium/__init__.py index b62abaf..63d08f8 100644 --- a/streamlit_folium/__init__.py +++ b/streamlit_folium/__init__.py @@ -9,6 +9,7 @@ import branca import folium +import folium.elements import folium.plugins import streamlit as st import streamlit.components.v1 as components @@ -22,6 +23,7 @@ _component_func = components.declare_component( "st_folium", url="http://localhost:3001" ) + else: parent_dir = os.path.dirname(os.path.abspath(__file__)) build_dir = os.path.join(parent_dir, "frontend/build") @@ -367,6 +369,8 @@ def bounds_to_dict(bounds_list: list[list[float]]) -> dict[str, dict[str, float] st.code(layer_control_string) def walk(fig): + if isinstance(fig, branca.colormap.ColorMap): + yield fig if isinstance(fig, folium.plugins.DualMap): yield from walk(fig.m1) yield from walk(fig.m2) @@ -380,9 +384,13 @@ def walk(fig): js_links = [] for elem in walk(folium_map): + if isinstance(elem, branca.colormap.ColorMap): + # manually add d3.js + js_links.insert(0, "https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js") + js_links.insert(0, "https://d3js.org/d3.v4.min.js") css_links.extend([href for _, href in elem.default_css]) js_links.extend([src for _, src in elem.default_js]) - + component_value = _component_func( script=leaflet, html=html, diff --git a/streamlit_folium/frontend/src/index.tsx b/streamlit_folium/frontend/src/index.tsx index 9d05c43..3d2dd59 100644 --- a/streamlit_folium/frontend/src/index.tsx +++ b/streamlit_folium/frontend/src/index.tsx @@ -144,7 +144,7 @@ function onLayerClick(e: any) { debouncedUpdateComponentValue(window.map) } -function getPixelatedStyles(pixelated: boolean) { +function getPixelatedStyles(pixelated: boolean) { if (pixelated) { const styles = ` .leaflet-image-layer { @@ -164,7 +164,6 @@ function getPixelatedStyles(pixelated: boolean) { } ` return styles - } window.initComponent = (map: any, return_on_hover: boolean) => { @@ -190,7 +189,7 @@ window.initComponent = (map: any, return_on_hover: boolean) => { * the component is initially loaded, and then again every time the * component gets new data from Python. */ -function onRender(event: Event): void { +async function onRender(event: Event) { // Get the RenderData from the event const data = (event as CustomEvent).detail @@ -209,69 +208,59 @@ function onRender(event: Event): void { const layer_control: string = data.args["layer_control"] const pixelated: boolean = data.args["pixelated"] - var finalizeOnRender = () => { + // load scripts + const loadScripts = async () => { + for (const link of js_links) { + // use promise to load scripts synchronously + await new Promise((resolve, reject) => { + const script = document.createElement("script") + script.src = link + script.async = false + script.onload = resolve + script.onerror = reject + window.document.body.appendChild(script) + }) + } + + css_links.forEach((link) => { + const linkTag = document.createElement("link") + linkTag.rel = "stylesheet" + linkTag.href = link + window.document.head.appendChild(linkTag) + }) + + const style = document.createElement("style") + style.innerHTML = getPixelatedStyles(pixelated) + window.document.head.appendChild(style) + } + + // finalize rendering + const finalizeOnRender = () => { if ( feature_group !== window.__GLOBAL_DATA__.last_feature_group || layer_control !== window.__GLOBAL_DATA__.last_layer_control ) { + // remove previous feature group and layer control if (window.feature_group && window.feature_group.length > 0) { window.feature_group.forEach((layer: Layer) => { - window.map.removeLayer(layer); - }); + window.map.removeLayer(layer) + }) } if (window.layer_control) { window.map.removeControl(window.layer_control) } + // update feature group and layer control cache window.__GLOBAL_DATA__.last_feature_group = feature_group window.__GLOBAL_DATA__.last_layer_control = layer_control - if (feature_group){ - // Though using `eval` is generally a bad idea, we're using it here - // because we're evaluating code that we've generated ourselves on the - // Python side. This is safe because we're not evaluating user input, so this - // couldn't be used to execute arbitrary code. - - // eslint-disable-next-line - eval(feature_group + layer_control) - for (let key in window.map._layers) { - let layer = window.map._layers[key] - layer.off("click", onLayerClick) - layer.on("click", onLayerClick) - if (return_on_hover) { - layer.off("mouseover", onLayerClick) - layer.on("mouseover", onLayerClick) - } - } - } else { - // eslint-disable-next-line - eval(layer_control) + if (feature_group) { + // render feature group + console.log("Rendering feature group...") + eval(feature_group) } } - - var view_changed = false - var new_zoom = window.map.getZoom() - if (zoom && zoom !== window.__GLOBAL_DATA__.last_zoom) { - new_zoom = zoom - window.__GLOBAL_DATA__.last_zoom = zoom - view_changed = true - } - - var new_center = window.map.getCenter() - if ( - center && - JSON.stringify(center) !== - JSON.stringify(window.__GLOBAL_DATA__.last_center) - ) { - new_center = center - window.__GLOBAL_DATA__.last_center = center - view_changed = true - } - - if (view_changed) { - window.map.setView(new_center, new_zoom) - } } if (!window.map) { @@ -296,7 +285,6 @@ function onRender(event: Event): void { document.body.appendChild(a) } - const render_script = document.createElement("script") // HACK -- update the folium-generated JS to add, most importantly, // the map to this global variable so that it can be used elsewhere // in the script. @@ -322,60 +310,27 @@ function onRender(event: Event): void { parent_div?.classList.remove("single") parent_div?.classList.add("double") } + } + await loadScripts().then(() => { + const render_script = document.createElement("script") - // This is only loaded once, from the onload callback - var postLoad = () => { - if (!window.map) { - render_script.innerHTML = + if (!window.map) { + render_script.innerHTML = script + - `window.map = map_div; window.initComponent(map_div, ${return_on_hover});` - document.body.appendChild(render_script) - const html_div = document.createElement("div") - html_div.innerHTML = html - document.body.appendChild(html_div) - const styles = getPixelatedStyles(pixelated) - var styleSheet = document.createElement("style") - styleSheet.innerText = styles - document.head.appendChild(styleSheet) - } - finalizeOnRender(); + `window.map = map_div; window.initComponent(map_div, ${return_on_hover});` + document.body.appendChild(render_script) + const html_div = document.createElement("div") + html_div.innerHTML = html + document.body.appendChild(html_div) + const styles = getPixelatedStyles(pixelated) + var styleSheet = document.createElement("style") + styleSheet.innerText = styles + document.head.appendChild(styleSheet) } - - if (js_links.length === 0) { - postLoad(); - } else { - // make sure dependent js files are loaded - // before we initialize the component - var count = 0; - js_links.forEach((elem) => { - var scr = document.createElement('script'); - scr.src = elem; - scr.async = false; - scr.onload = () => { - count -= 1; - if(count === 0) { - setTimeout(postLoad, 0); - } - }; - document.head.appendChild(scr); - count += 1; - }); - } - - // css is okay regardless loading order - css_links.forEach((elem) => { - var link = document.createElement('link'); - link.rel = "stylesheet"; - link.type = "text/css"; - link.href = elem; - document.head.appendChild(link); - }); - Streamlit.setFrameHeight() - } - } else { - finalizeOnRender(); + finalizeOnRender() + }) } - + finalizeOnRender() } // Attach our `onRender` handler to Streamlit's render event. From 3d8d5373d8b01916027c7c31593e4f1acdb36109 Mon Sep 17 00:00:00 2001 From: Navxihziq Date: Sun, 16 Jun 2024 03:24:13 -0400 Subject: [PATCH 2/4] included the fogotted view change logics --- streamlit_folium/frontend/src/index.tsx | 40 +++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/streamlit_folium/frontend/src/index.tsx b/streamlit_folium/frontend/src/index.tsx index 3d2dd59..bcf54b4 100644 --- a/streamlit_folium/frontend/src/index.tsx +++ b/streamlit_folium/frontend/src/index.tsx @@ -256,11 +256,45 @@ async function onRender(event: Event) { window.__GLOBAL_DATA__.last_layer_control = layer_control if (feature_group) { - // render feature group - console.log("Rendering feature group...") - eval(feature_group) + // eslint-disable-next-line + eval(feature_group + layer_control) + for (let key in window.map._layers) { + let layer = window.map._layers[key] + layer.off("click", onLayerClick) + layer.on("click", onLayerClick) + if (return_on_hover) { + layer.off("mouseover", onLayerClick) + layer.on("mouseover", onLayerClick) + } + } + } else { + // eslint-disable-next-line + eval(layer_control) } } + + var view_changed = false + var new_zoom = window.map.getZoom() + if (zoom && zoom !== window.__GLOBAL_DATA__.last_zoom) { + new_zoom = zoom + window.__GLOBAL_DATA__.last_zoom = zoom + view_changed = true + } + + var new_center = window.map.getCenter() + if ( + center && + JSON.stringify(center) !== + JSON.stringify(window.__GLOBAL_DATA__.last_center) + ) { + new_center = center + window.__GLOBAL_DATA__.last_center = center + view_changed = true + } + + if (view_changed) { + window.map.setView(new_center, new_zoom) + } } if (!window.map) { From 5ab74d5cdc740210a072e7de5c95ac426f99015c Mon Sep 17 00:00:00 2001 From: Navxihziq Date: Tue, 18 Jun 2024 10:58:04 -0400 Subject: [PATCH 3/4] Add type hints to comply with static type checking --- streamlit_folium/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/streamlit_folium/__init__.py b/streamlit_folium/__init__.py index 63d08f8..2710a3d 100644 --- a/streamlit_folium/__init__.py +++ b/streamlit_folium/__init__.py @@ -5,7 +5,7 @@ import re import warnings from textwrap import dedent -from typing import Iterable +from typing import Iterable, List import branca import folium @@ -23,7 +23,7 @@ _component_func = components.declare_component( "st_folium", url="http://localhost:3001" ) - + else: parent_dir = os.path.dirname(os.path.abspath(__file__)) build_dir = os.path.join(parent_dir, "frontend/build") @@ -380,17 +380,19 @@ def walk(fig): for child in fig._children.values(): yield from walk(child) - css_links = [] - js_links = [] + css_links: List[str] = [] + js_links: List[str] = [] for elem in walk(folium_map): if isinstance(elem, branca.colormap.ColorMap): # manually add d3.js - js_links.insert(0, "https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js") + js_links.insert( + 0, "https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js" + ) js_links.insert(0, "https://d3js.org/d3.v4.min.js") css_links.extend([href for _, href in elem.default_css]) js_links.extend([src for _, src in elem.default_js]) - + component_value = _component_func( script=leaflet, html=html, From 7e61dbdd9f9a0d7a5b0fa071638ceb68514c0fc7 Mon Sep 17 00:00:00 2001 From: Navxihziq Date: Tue, 18 Jun 2024 12:06:34 -0400 Subject: [PATCH 4/4] Fixed the typing to use built-in generics in Python 3.9+ --- streamlit_folium/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/streamlit_folium/__init__.py b/streamlit_folium/__init__.py index 2710a3d..a9bc74c 100644 --- a/streamlit_folium/__init__.py +++ b/streamlit_folium/__init__.py @@ -5,7 +5,7 @@ import re import warnings from textwrap import dedent -from typing import Iterable, List +from typing import Iterable import branca import folium @@ -380,8 +380,8 @@ def walk(fig): for child in fig._children.values(): yield from walk(child) - css_links: List[str] = [] - js_links: List[str] = [] + css_links: list[str] = [] + js_links: list[str] = [] for elem in walk(folium_map): if isinstance(elem, branca.colormap.ColorMap):