Geospatial Visualizations with Dash and Folium
In this article, a brief explanation is provided about creating an interactive application that combines geographical data with powerful visualization tools. This application is based on the location of bars in Mexico City (data can be obtained from the following link: https://www.inegi.org.mx/app/descarga/ or you can access the pre-processed data in the following repository: https://github.com/OsvaldoYa22/Geospatial-Visualizations).
The application allows filtering points or polygons according to user needs. Additionally, the Geocoding API
and Street View Static API
are used to enhance the user’s visual experience. As a result, it’s possible to download the map with the selected filters into an HTML file, utilizing Python.
Importing Libraries and Preparation
In this section, we start by importing the necessary libraries for our project. Dash and Folium are the cornerstones of this application, while other libraries such as Pandas, KeplerGL, and Geopandas will assist us in working with data and creating visualizations.
from dash import Dash, dcc, html, Input, Output, callback
import base64
from dash.dependencies import Input, Output, State
import pandas as pd
import folium
from keplergl import *
import dash
import dash_bootstrap_components as dbc
import geopandas as gpd
import folium
from folium import plugins
from fun_style import *
Setting Up the Dash Application
In the following code snippet, we are using the dash.Dash
class to create our application.
app = dash.Dash(__name__, external_stylesheets = [dbc.themes.BOOTSTRAP], suppress_callback_exceptions = True)
external_stylesheets
: Here we are passing a Bootstrap stylesheet using dbc.themes.BOOTSTRAP
. This allows us to apply an appealing and responsive visual design to our application.
suppress_callback_exceptions
: Setting this to True allows us to handle callback exceptions without disrupting the entire application in case of errors in the callbacks.
Creating the Sidebar of Options
Here, we are defining the sidebar of options that will enable users to interact with the geospatial visualization in an intuitive manner. This part is crucial to provide a smooth user experience and facilitate the exploration of geographical data.
sidebar = html.Div(
[
html.Div(
children = [
html.Img(src = "/assets/Logo.PNG", height = 150, style = {"width": "100%"}),
]
),
html.Hr(),
.....
.....
.....
# Logo Section
html.Div([
html.Img(src = "/assets/Logo.PNG", height = 150, style = {"width": "100%"}),
]),
The image is loaded from the path /assets/Logo.PNG
and a style is applied to adjust its height to 150 pixels and a width of 100%.
# Clear Filters Button
html.Div([
dbc.Button("Limpiar filtros", outline = True, id = "f5-button", color = "primary", className = "me-1", n_clicks = 0),
], className = "d-grid gap-2"),
Here, a button is created using the dash_bootstrap_components
library from Bootstrap. The button is used to reset the filters applied in the interface. It is assigned a unique ID f5-button
, which can be used to perform actions in response to user clicks.
# Classification Section with Checkboxes
html.H3('Clasificación', style = CONTENT_BOX_TITLES),
html.Div(
dcc.Checklist(
id = "classification",
options = [
{'label': 'Bares', 'value': 'Bares'},
{'label': 'Centros nocturnos', 'value': 'Centros nocturnos'},
],
inputStyle = {'margin-right': '4px'},
),
style = {
'background-color': '#F1F2F2',
'border-radius': '15px',
'margin': '5px',
'padding': '7px',
'position': 'relative',
'overflow-y': 'auto',
},
),
This section displays a checklist checkboxes
that allows the user to select different sorting options, such as Bars
and Nightclubs
. The checklist is created using the dcc.Checklist
component of Dash. The section has a style that defines the background, rounded borders, and scrolling style if there are too many items to fit in the available space.
# HTML Map Download Section
html.Div([
html.Div([
dbc.Button(
"Descargue el mapa en HTML",
id = "open-lg",
outline = True,
color = "primary",
n_clicks = 0,
),
], className = "d-grid"),
dbc.Modal(
[
# Modal Content
],
id = "modal-lg",
size = "sm",
is_open = False,
keyboard = False,
backdrop = "static",
),
]),
In this section, a button is displayed that, upon clicking, will open a modal providing instructions for generating and downloading a map in HTML format. The modal is defined using dbc.Modal
and contains informative content on how to generate the HTML file of the map.
Implementing Callbacks to Switch Tabs
In this part of the code, we are utilizing callback functionality with @callback
to change the content of a section based on the tab selected by the user.
@callback(Output('tabs-content-inline', 'children'),
Input('tabs-styled-with-inline', 'value'))
def render_content(tab):
if tab == 'tab-1':
return html.Div([
html.Div(
children = [
html.Div(
style = {
'background-color': '#FFFFFF',
'margin': '15px',
'padding': '0px',
'position': 'relative',
'box-shadow': '4px 4px 4px 4px lightgrey',
'width': '100vw',
'height': '55vh'
},
children = [
html.Div(id = 'right-column_map'),
]
)
],
style = CONTENT_STYLE
),
])
Output('tabs-content-inline', 'children')
: This sets that the content of the section with the identifier tabs-content-inline
will change according to the selected tab.
Input('tabs-styled-with-inline', 'value')
: This takes the value of the tab selected by the user from the component with the identifier tabs-styled-with-inline
.
Structuring the Overall Layout of the Application
Here, the sidebar of options, the tabs, and the container where content will change based on the selected tab are included.
app.layout = html.Div([
sidebar,
html.Div([
dcc.Tabs(id = "tabs-styled-with-inline", value = 'tab-1', children = [
dcc.Tab(label = 'CONSUMO DE ALCOHOL', value = 'tab-1', style = style_tabs, selected_style = tab_selected_style),
dcc.Tab(label = 'VENTA DE ALCOHOL', value = 'tab-2', style = style_tabs, selected_style = tab_selected_style),
], style = CONTENT_STYLE),
html.Div(id = 'tabs-content-inline'),
])
])
Function to Generate the Interactive Map
@app.callback(
Output('right-column_map', 'children'),
[Input('alcaldias', 'value'),
Input('classification', 'value'),
Input('capas', 'value'),
Input('poligonos-carga','value'),
Input('colonia-dropdown','value'),
]
)
def generate_map(selected_location, selected_classification,
selected_capas, poli, selected_colonia):
Within the generate_map
function, data processing and filtering are performed based on user selections. For example:
if selected_location:
data = data[data['municipio'].isin(selected_location)]
if selected_classification:
data = data[data['nombre_act'].isin(selected_classification)]
These lines filter the dataset based on the alcaldías
(boroughs) and clasificaciones
(classifications) selected by the user.
Then, a Folium
map is created based on the user’s layer selection.
if selected_capas == "1":
m = folium.Map(tiles = "OpenStreetMap")
elif selected_capas == "2":
m = folium.Map(tiles = "Cartodb Positron")
# ...
Markers are generated on the map for each location in the data:
for index, row in data.iterrows():
longitude = row['longitud']
latitude = row['latitud']
# ...
To incorporate images and Google Maps links into our map, we use Google APIs, specifically the Geocoding API
and the Street View Static API
, using a short code snippet in the R
language.
library(dplyr)
library(httr)
library(stringr)
library(tidyr)
base <- read.csv("C:/Users/.csv", fileEncoding = "Latin1",header = T,stringsAsFactors = F)
api_Key <- "Your API key"
generate_streetview_link <- function(lat, lon, key) {
base_url <- "https://maps.googleapis.com/maps/api/streetview"
query_params <- list(
location = paste(lat, lon, sep = ","),
size = "640x480",
key = key
)
url <- modify_url(base_url, query = query_params)
return(url)
}
base$IMAGEN <- mapply(generate_streetview_link, base$latitud, base$longitud, api_Key)
generate_streetview_url <- function(lat, lon) {
base_url <- "https://www.google.com/maps/@"
coords <- paste(lat, lon, sep = ",")
url <- paste(base_url, coords, ",3a,75y,114.26h,90t/data=!3m4!1e1!3m2!1s", coords, "!2e0", sep = "")
return(url)
}
base$MAPS <- mapply(generate_streetview_url, base$latitud, base$longitud)
Now we can integrate the images and links into our map.
for index, row in data.iterrows():
longitude = row['longitud']
latitude = row['latitud']
especialidad = row['nombre_act']
id_bct_o = row['nom_estab']
web_df = row['www_2']
imagen_url = row['IMAGEN']
link_url = row['MAPS']
color = tipo_poste_colors.get(especialidad, 'gray')
popup_content = f'<a href="{link_url}" target="_blank">' \
f'<img src="{imagen_url}" alt="{especialidad}" width="200" height="150"></a><br>' \
f'Clasificación: {especialidad}<br>' \
f'Nom. establecimiento: {id_bct_o}<br>'\
f'Sitio Web: <a href="{web_df}" target="_blank">{web_df}</a><br>'
tooltip_content = f'Clasificación: {especialidad}<br>' \
f'Nom. establecimiento: {id_bct_o}<br>'\
f'Sitio Web: <a href="{web_df}" target="_blank">{web_df}</a><br>'
folium.CircleMarker(
[latitude, longitude],
radius = 4,
color = color,
fill = True,
fill_opacity = 1,
weight = 1,
popup = folium.Popup(popup_content, max_width = 300),
tooltip = folium.Tooltip(tooltip_content),
).add_to(m)
Adding the Legend to the Map
for type, count in camras_count.items():
color = tipo_colors.get(type, 'gray')
legend_html += f'<tr><td><span style="background-color:{color}; padding: 6px; border-radius: 50%; display: inline-block;"></span> {type}</td><td>{count}</td></tr>'
legend_html += '</table></div>'
m.get_root().html.add_child(folium.Element(legend_html))
This for
loop iterates through each key-value pair in the camras_count
dictionary.
type
represents the type of element, and count
represents the quantity associated with that type of element. The code searches the tipo_colors
dictionary for the color associated with the type of element. If a color is not found for that type of element in the dictionary, the default color ‘gray’ is assigned. The dictionary is defined in the fun_style.py
file as follows:
tipo_colors = {
'Bares' : '#FF0000',
'Centros nocturnos' : '#FF6E00',
}
A span
element with a background color representing the color associated with the element type is used. The name of the element type is displayed alongside this colored element, and the quantity is also displayed. This is added to the legend_html
string, and the string is appended as an HTML element to the interactive map m
using the add_child()
method.
Defining the boundaries of the visible map area;
sw = data[['latitud', 'longitud']].min().values.tolist()
ne = data[['latitud', 'longitud']].max().values.tolist()
m.fit_bounds([sw, ne])
These points are based on the minimum and maximum values of the latitude
and longitude
columns in the data
DataFrame. This means that the most extreme coordinates in terms of latitude
and longitude
of all points in the data
dataset are being determined.
Finally, the map is rendered as an HTML iframe
element and returned as the result of the callback.
map_html = m.get_root().render()
return html.Iframe(srcDoc = map_html, style = {'width': '100%', 'height': '600px'})
The srcDoc
attribute is used to specify the HTML content that will be displayed inside the <iframe>
. In this case, the HTML content is the previously generated map stored in the map_html
variable.
Function to Download the Map in HTML Format
@app.callback(
Output('download-map', 'href'),
Input('btn_map', 'n_clicks'),
State('alcaldias', 'value'),
State('classification', 'value'),
State('capas', 'value'),
State('poligonos-carga','value'),
State('colonia-dropdown','value'),
prevent_initial_call = True,
)
def download_map(n_clicks,selected_location,selected_classification,
selected_capas,poli,selected_colonia):
map_iframe = generate_map(selected_location,selected_classification,
selected_capas,poli,selected_colonia)
map_html = map_iframe.srcDoc
encoded_map = base64.b64encode(map_html.encode()).decode()
href = f"data:text/html;charset=utf-8;base64,{encoded_map}"
if n_clicks:
return href
return dash.no_update
map_iframe
: Here, the generate_map
function we discussed earlier is called to generate the interactive map.
map_html
: The HTML content of the generated map is obtained.
encoded_map
: The HTML content is encoded in base64 to generate a text string that can be embedded in the download link.
href
: The complete download link is formed here, with the content encoded in base64
and the MIME
type specified.
Function to Toggle Modal Visibility
A modal is a popup window that typically displays additional information or options to the user.
def toggle_modal(n1, is_open):
if n1:
return not is_open
return is_open
app.callback(
Output("modal-lg", "is_open"),
Input("open-lg", "n_clicks"),
State("modal-lg", "is_open"),
)(toggle_modal)
Function to Reset Filters to Their Initial Values
This function implements a callback that resets the values of various components in your Dash application to their initial states. It allows the user to reset filters and selections to their initial settings.
@app.callback(
[Output('alcaldias', 'value'),
Output('classification', 'value'),
Output('capas', 'value'),
Output('poligonos-carga','value'),
Output('colonia-dropdown','value'),
],
Input("f5-button", "n_clicks"),
prevent_initial_call = True,
)
def redirect_to_self(n):
selected_location = []
selected_classification = []
selected_colonia = []
selected_capas = []
poli = []
return (selected_location,selected_classification,
selected_capas,poli,selected_colonia)
Function to Update Dropdown Menu Options for ‘colonias’
@app.callback(
Output("colonia-dropdown", "options"),
Input("alcaldias", "value")
)
def update_sector_dropdown_02(selected_location):
data = pd.read_csv("assets/BASES/BASE.csv", encoding='latin-1', low_memory=False)
if selected_location is None or selected_location == []:
unique_colonias = data['nomb_asent'].unique()
else:
data = data[data['municipio'].isin(selected_location)]
unique_colonias = data['nomb_asent'].unique()
options = [{'label': colonia, 'value': colonia} for colonia in unique_colonias]
A list of options is created in the format required by the dropdown menu {'label': ..., 'value': ...}
using the unique neighborhoods obtained.
return options
The updated list of options is returned so that the neighborhood dropdown menu reflects the neighborhoods corresponding to the selected boroughs.
Main Execution Block of the Application
if __name__ == "__main__":
app.run_server(debug=True, port=9050)
debug=True
enables the debug mode, which means detailed messages will be displayed in case of errors.
port=9050
sets the port number on which the server will run. In this case, port 9050 is being used.