From e7397ee6cf0e385de7a276588b6f59e825974d69 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Thu, 16 Jan 2025 16:06:53 +0100 Subject: [PATCH 1/8] Use the JsCode object from the js_loader package --- folium/utilities.py | 14 +------------- requirements.txt | 1 + 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/folium/utilities.py b/folium/utilities.py index 9210bf59e..520982ea9 100644 --- a/folium/utilities.py +++ b/folium/utilities.py @@ -35,6 +35,7 @@ none_min, write_png, ) +from js_loader import JsCode # noqa: F401 try: import pandas as pd @@ -436,19 +437,6 @@ def get_and_assert_figure_root(obj: Element) -> Figure: return figure -class JsCode: - """Wrapper around Javascript code.""" - - def __init__(self, js_code: Union[str, "JsCode"]): - if isinstance(js_code, JsCode): - self.js_code: str = js_code.js_code - else: - self.js_code = js_code - - def __str__(self): - return self.js_code - - def parse_font_size(value: Union[str, int, float]) -> str: """Parse a font size value, if number set as px""" if isinstance(value, (int, float)): diff --git a/requirements.txt b/requirements.txt index dff8e41f4..88c2afce7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ branca>=0.6.0 jinja2>=2.9 +js_loader numpy requests xyzservices From 9587f41f6193a1a71d2674442c5798813ff5d72c Mon Sep 17 00:00:00 2001 From: Hans Then Date: Thu, 16 Jan 2025 16:39:38 +0100 Subject: [PATCH 2/8] Install js_loader separately --- .github/workflows/test_code.yml | 3 +++ .github/workflows/test_selenium.yml | 4 ++++ requirements.txt | 1 - setup.py | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_code.yml b/.github/workflows/test_code.yml index ee75fe41e..b68e1a0ec 100644 --- a/.github/workflows/test_code.yml +++ b/.github/workflows/test_code.yml @@ -33,5 +33,8 @@ jobs: - name: Install folium from source run: python -m pip install -e . --no-deps --force-reinstall + - name: Install js_loader + run: python -m pip install js_loader + - name: Code tests run: python -m pytest -vv --ignore=tests/selenium diff --git a/.github/workflows/test_selenium.yml b/.github/workflows/test_selenium.yml index 34cd05430..494b0181f 100644 --- a/.github/workflows/test_selenium.yml +++ b/.github/workflows/test_selenium.yml @@ -30,6 +30,10 @@ jobs: shell: bash -l {0} run: python -m pip install -e . --no-deps --force-reinstall + - name: Install js_loader + shell: bash -l {0} + run: python -m pip install js_loader + - name: Selenium tests shell: bash -l {0} run: python -m pytest tests/selenium -vv diff --git a/requirements.txt b/requirements.txt index 88c2afce7..dff8e41f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ branca>=0.6.0 jinja2>=2.9 -js_loader numpy requests xyzservices diff --git a/setup.py b/setup.py index a2cf34823..0ba839786 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ def walk_subpkg(name): with open("requirements.txt") as f: tests_require = f.readlines() install_requires = [t.strip() for t in tests_require] +install_requires.append("js_loader") setup( name="folium", From 58eee6174a8ac7b59a405c8ee10f5e1561ec5027 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Thu, 16 Jan 2025 16:43:30 +0100 Subject: [PATCH 3/8] Missed these --- .github/workflows/deploy-docs.yml | 3 +++ .github/workflows/test_latest_branca.yml | 4 ++++ .github/workflows/test_mypy.yml | 5 +++++ 3 files changed, 12 insertions(+) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 99e0d4905..9461ca806 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -36,6 +36,9 @@ jobs: --file requirements.txt --file requirements-dev.txt + - name: Install folium from source + run: python -m pip install js_loader + - name: Install folium from source run: python -m pip install -e . --no-deps --force-reinstall diff --git a/.github/workflows/test_latest_branca.yml b/.github/workflows/test_latest_branca.yml index 73b025402..a29107447 100644 --- a/.github/workflows/test_latest_branca.yml +++ b/.github/workflows/test_latest_branca.yml @@ -26,6 +26,10 @@ jobs: shell: bash -l {0} run: python -m pip install -e . --no-deps --force-reinstall + - name: Install js_loader + shell: bash -l {0} + run: python -m pip install js_loader + - name: Tests with latest branca shell: bash -l {0} run: | diff --git a/.github/workflows/test_mypy.yml b/.github/workflows/test_mypy.yml index e99fb75ca..0a2e7332c 100644 --- a/.github/workflows/test_mypy.yml +++ b/.github/workflows/test_mypy.yml @@ -27,6 +27,11 @@ jobs: run: | python -m pip install -e . --no-deps --force-reinstall + - name: Install js_loader + shell: bash -l {0} + run: | + python -m pip install js_loader + - name: Mypy test shell: bash -l {0} run: | From 3d42d7c442f23fe5e71e718cd9e3469e594db3b9 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 18 Jan 2025 15:46:51 +0100 Subject: [PATCH 4/8] Add documentation give an example how we can load javascript from javascript files. --- docs/advanced_guide/js/handlers.js | 39 +++++++++++++++ docs/advanced_guide/js_loader.md | 76 ++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 docs/advanced_guide/js/handlers.js create mode 100644 docs/advanced_guide/js_loader.md diff --git a/docs/advanced_guide/js/handlers.js b/docs/advanced_guide/js/handlers.js new file mode 100644 index 000000000..90331e297 --- /dev/null +++ b/docs/advanced_guide/js/handlers.js @@ -0,0 +1,39 @@ +/* located in js/handlers.js */ +on_each_feature = function(f, l) { + l.bindPopup(function() { + return '
' + dayjs.unix(f.properties.timestamp).format() + '
'; + }); +} + +source = 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); +} + +module.exports = { + source, + on_each_feature +} diff --git a/docs/advanced_guide/js_loader.md b/docs/advanced_guide/js_loader.md new file mode 100644 index 000000000..a355fe2fb --- /dev/null +++ b/docs/advanced_guide/js_loader.md @@ -0,0 +1,76 @@ +# Loading event handlers from a CommonJS module + +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +## Loading Event handlers from javascript +Folium supports event handlers via the `JsCode` class. However, for more than a few lines of code, it becomes unwieldy to write javascript inside python using +only strings. For more complex code, it is much nicer to write javascript inside js files. This allows editor support, such as syntax highlighting, code completion +and linting. + +Suppose we have the following javascript file: + +``` +/* located in js/handlers.js */ +on_each_feature = function(f, l) { + l.bindPopup(function() { + return '
' + dayjs.unix(f.properties.timestamp).format() + '
'; + }); +} + +source = 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); +} + +module.exports = { + source, + on_each_feature +} +``` + +Now we can load it as follows inside our python code: + +```{code-cell} ipython3 +from js_loader import install_js_loader +from folium.plugins import Realtime +install_js_loader() + +from js import handlers + +m = folium.Map() + +rt = Realtime(handlers.source, + on_each_feature=handlers.on_each_feature, + interval=1000) +rt.add_js_link("dayjs", "https://cdn.jsdelivr.net/npm/dayjs@1.11.10/dayjs.min.js") +rt.add_to(m) +m +``` From 13687a0c8bbc445cceaec101421cadff377c98dc Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 18 Jan 2025 15:53:14 +0100 Subject: [PATCH 5/8] Updated to install pythonmonkey so the js_loader example will work --- .github/workflows/deploy-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 9461ca806..39992c9c5 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -36,8 +36,8 @@ jobs: --file requirements.txt --file requirements-dev.txt - - name: Install folium from source - run: python -m pip install js_loader + - name: Install js_loader + run: python -m pip install js_loader pythonmonkey - name: Install folium from source run: python -m pip install -e . --no-deps --force-reinstall From 97fdf955a68b41024b6896be5aca6f0a2cbff529 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 18 Jan 2025 15:54:53 +0100 Subject: [PATCH 6/8] Add to toc --- docs/advanced_guide.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/advanced_guide.rst b/docs/advanced_guide.rst index 579eada3d..f4aa1d748 100644 --- a/docs/advanced_guide.rst +++ b/docs/advanced_guide.rst @@ -15,3 +15,4 @@ Advanced guide advanced_guide/piechart_icons advanced_guide/polygons_from_list_of_points advanced_guide/customize_javascript_and_css + advanced_guide/js_loader From e5e783e35883dab228c8ab51171f1e0edf27ce87 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 18 Jan 2025 20:39:24 +0100 Subject: [PATCH 7/8] Changes type checks to use a Protocol for JsCode and JsCode like objects that come from external sources. --- folium/elements.py | 4 ++-- folium/plugins/timeline.py | 4 ++-- folium/template.py | 6 +++--- folium/utilities.py | 22 +++++++++++++++++++++- tests/test_utilities.py | 7 +++++++ 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/folium/elements.py b/folium/elements.py index 56b3ba904..e3f85e727 100644 --- a/folium/elements.py +++ b/folium/elements.py @@ -9,7 +9,7 @@ ) from folium.template import Template -from folium.utilities import JsCode +from folium.utilities import TypeJsCode class JSCSSMixin(MacroElement): @@ -121,7 +121,7 @@ class EventHandler(MacroElement): """ ) - def __init__(self, event: str, handler: JsCode, once: bool = False): + def __init__(self, event: str, handler: TypeJsCode, once: bool = False): super().__init__() self._name = "EventHandler" self.event = event diff --git a/folium/plugins/timeline.py b/folium/plugins/timeline.py index fce034024..4a26018f4 100644 --- a/folium/plugins/timeline.py +++ b/folium/plugins/timeline.py @@ -6,7 +6,7 @@ from folium.features import GeoJson from folium.folium import Map from folium.template import Template -from folium.utilities import JsCode, get_bounds, remove_empty +from folium.utilities import JsCode, TypeJsCode, get_bounds, remove_empty class Timeline(GeoJson): @@ -108,7 +108,7 @@ class Timeline(GeoJson): def __init__( self, data: Union[dict, str, TextIO], - get_interval: Optional[JsCode] = None, + get_interval: Optional[TypeJsCode] = None, **kwargs ): super().__init__(data) diff --git a/folium/template.py b/folium/template.py index 4dcd01f0b..52d831ecf 100644 --- a/folium/template.py +++ b/folium/template.py @@ -4,11 +4,11 @@ import jinja2 from branca.element import Element -from folium.utilities import JsCode, TypeJsonValue, camelize +from folium.utilities import TypeJsCode, TypeJsonValue, camelize -def tojavascript(obj: Union[str, JsCode, dict, list, Element]) -> str: - if isinstance(obj, JsCode): +def tojavascript(obj: Union[str, TypeJsCode, dict, list, Element]) -> str: + if isinstance(obj, TypeJsCode): return obj.js_code elif isinstance(obj, Element): return obj.get_name() diff --git a/folium/utilities.py b/folium/utilities.py index 520982ea9..45b387628 100644 --- a/folium/utilities.py +++ b/folium/utilities.py @@ -17,10 +17,12 @@ Iterator, List, Optional, + Protocol, Sequence, Tuple, Type, Union, + runtime_checkable, ) from urllib.parse import urlparse, uses_netloc, uses_params, uses_relative @@ -35,7 +37,6 @@ none_min, write_png, ) -from js_loader import JsCode # noqa: F401 try: import pandas as pd @@ -65,6 +66,25 @@ _VALID_URLS.add("data") +@runtime_checkable +class TypeJsCode(Protocol): + # we only care about this attribute. + js_code: str + + +class JsCode(TypeJsCode): + """Wrapper around Javascript code.""" + + def __init__(self, js_code: Union[str, "JsCode"]): + if isinstance(js_code, JsCode): + self.js_code: str = js_code.js_code + else: + self.js_code = js_code + + def __str__(self): + return self.js_code + + def validate_location(location: Sequence[float]) -> List[float]: """Validate a single lat/lon coordinate pair and convert to a list diff --git a/tests/test_utilities.py b/tests/test_utilities.py index a1f10be36..12e21f4fe 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -1,10 +1,12 @@ import numpy as np import pandas as pd import pytest +from js_loader import JsCode as external_JsCode from folium import FeatureGroup, Map, Marker, Popup from folium.utilities import ( JsCode, + TypeJsCode, _is_url, camelize, deep_copy, @@ -248,6 +250,11 @@ def test_js_code_init_js_code(): assert isinstance(js_code_2.js_code, str) +def test_external_js_code(): + js_code = external_JsCode("hi") + assert isinstance(js_code, TypeJsCode) + + @pytest.mark.parametrize( "value,expected", [ From 91e814a062d6c8954ef46130a7bd22801a16604c Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 18 Jan 2025 21:30:45 +0100 Subject: [PATCH 8/8] Remove js_loader as a dependency. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 0ba839786..a2cf34823 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,6 @@ def walk_subpkg(name): with open("requirements.txt") as f: tests_require = f.readlines() install_requires = [t.strip() for t in tests_require] -install_requires.append("js_loader") setup( name="folium",