I have a Shiny for Python app that I would like to make interactive to allow user to see various statistics depending on where they click on the map.
I am using folium
to display the map.
I can't find a way to return the coordinates of the clicked spot back to shiny
for further processing.
I found this process relatively easy to implement using ipyleaflet
but its dependency on ipywidgets
conflicts with plotly
which my app heavily depends on to display interactive charts relevant to the place clicked on.
Here is a toy example that I would like to use for practice:
from shiny import App, render, ui, reactive
import folium
def server(input, output, session):
# Initialize reactive value for coordinates
coords = reactive.Value({'lat': None, 'lng': None})
@output
@render.ui
def map():
m = folium.Map(location=[-1.9403, 29.8739], zoom_start=8)
# Define a click event handler here that returns the coordinate to shiny
return ui.HTML(m._repr_html_())
@reactive.Effect
@reactive.event(input.map_click)
def _():
# capture the coordinates returned by the click event handler on the map
coords.set()
return coords
app_ui = ui.page_fluid(
ui.h1("Folium Map that returns Coordinates of clicked spot"),
ui.div(
ui.output_ui("map"),
style="height: 600px; width: 100%;"
)
)
app = App(app_ui, server)
You can add a folium.MacroElement()
to the map which captures the coordinates of the click location and sends them to Shiny. It can use a jinja2.Template()
having this content:
{% macro script(this, kwargs) %}
function getLatLng(e){
var lat = e.latlng.lat.toFixed(6),
lng = e.latlng.lng.toFixed(6);
parent.Shiny.setInputValue('coords', [lat, lng], {priority: 'event'});
};
{{ this._parent.get_name() }}.on('click', getLatLng);
{% endmacro %}
What happens here is that we have a function getLatLng()
which reads lat
and lng
and then sends these values to Shiny. Therefore, we use Shiny.setInputValue()
(this link refers to an R article on Shiny because I did not find it documented for Python, but it's really similar). Important is the parent
prefix because the map is rendered within an <iframe>
and we need to refer to the global environment. The values are then received within an reactive.Value
, here coords
, and can be used for further processing.
from shiny import App, render, ui, reactive
import folium
from jinja2 import Template
app_ui = ui.page_fluid(
ui.h1("Folium Map that returns Coordinates of clicked spot"),
ui.output_code("result"),
ui.div(
ui.output_ui("map"),
style="height: 600px; width: 100%;"
)
)
def server(input, output, session):
# Initialize reactive value for coordinates
coords = reactive.Value({'lat': None, 'lng': None})
@output
@render.ui
def map():
m = folium.Map(location=[-1.9403, 29.8739], zoom_start=8)
el = folium.MacroElement().add_to(m)
el._template = Template(
"""
{% macro script(this, kwargs) %}
function getLatLng(e){
var lat = e.latlng.lat.toFixed(6),
lng = e.latlng.lng.toFixed(6);
parent.Shiny.setInputValue('coords', [lat, lng], {priority: 'event'});
};
{{ this._parent.get_name() }}.on('click', getLatLng);
{% endmacro %}
"""
)
return ui.HTML(m._repr_html_())
# processing the coordinates, e.g. rendering them
@render.code
@reactive.event(input.coords)
def result():
return (
f"""
Coordinates of click location: \n
Latitude: {input.coords()[0]} \n
Longitude: {input.coords()[1]}"""
)
app = App(app_ui, server)