Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the JsCode object from the js_loader package #2081

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ jobs:
--file requirements.txt
--file requirements-dev.txt

- 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

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test_code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions .github/workflows/test_latest_branca.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/test_mypy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/test_selenium.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions docs/advanced_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
39 changes: 39 additions & 0 deletions docs/advanced_guide/js/handlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* located in js/handlers.js */
on_each_feature = function(f, l) {
l.bindPopup(function() {
return '<h5>' + dayjs.unix(f.properties.timestamp).format() + '</h5>';
});
}

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
}
76 changes: 76 additions & 0 deletions docs/advanced_guide/js_loader.md
Original file line number Diff line number Diff line change
@@ -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 '<h5>' + dayjs.unix(f.properties.timestamp).format() + '</h5>';
});
}

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/[email protected]/dayjs.min.js")
rt.add_to(m)
m
```
4 changes: 2 additions & 2 deletions folium/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
)

from folium.template import Template
from folium.utilities import JsCode
from folium.utilities import TypeJsCode


class JSCSSMixin(MacroElement):
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions folium/plugins/timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions folium/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
34 changes: 21 additions & 13 deletions folium/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -64,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

Expand Down Expand Up @@ -436,19 +457,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)):
Expand Down
7 changes: 7 additions & 0 deletions tests/test_utilities.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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",
[
Expand Down
Loading