Dr. Alberto García Robledo
CentroGeo Querétaro
Escuela de Verano Científico CentroGeo 2022
Dash es código abierto, publicado bajo la licencia MIT.
Está disponible para múltiples lenguajes:
Python
R
Julia
.NET
En una terminal, instala Dash a través de pip:
pip install dash
Si prefieres desarrollar en Jupyter Notebook o Jupyter Lab, instala jupyter-dash:
pip install jupyter-dash
En este taller utilizaremos Google Colab
Una aplicación Dash está integrada de dos partes:
La descripción del UI (layout)
La descripción de la interactividad (callbacks)
Un layout de Dash está integrado de uno o más componentes Dash anidados. Los hay de dos tipos:
import dash_html_components as html
from jupyter_dash import JupyterDash
app = JupyterDash(__name__)
app.layout = html.Div([
html.H1('H1 header!'),
html.H2('H2 header!'),
html.H3('H3 header!'),
html.H4('H4 header!'),
html.H5('H5 header!'),
html.H6('H6 header!'),
])
app.run_server()
dash_html_components provee un componente Dash por cada etiqueta HTML.
Un layout está compuesto por Dash components anidados:
Un layout de Dash es parecido a un árbol DOM.
El componente raíz de un layout es asignado a app.layout.
from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
app = JupyterDash(__name__)
markdown = '''
# Lorem Ipsum
**Lorem ipsum dolor sit amet**, consectetur adipiscing elit.
1. Duis ullamcorper nisl eget diam laoreet, ac rutrum enim aliquet.
2. Curabitur aliquam libero et leo consectetur, at pretium urna mollis.
3. Cras et vestibulum nisi, sit amet sollicitudin turpis.
***Nam imperdiet porttitor venenatis***. Aenean vitae maximus enim, ac consectetur augue.
## Vestibulum nec felis ac erat auctor pharetra.
> Vestibulum vitae velit at nisl pretium ultricies.
>> Suspendisse mi urna, lobortis sed ante sit amet, porta venenatis ligula.
Text generated by the [Lorem Ipsum generator](https://www.lipsum.com/).
'''
app.layout = html.Div([
dcc.Markdown(children=markdown)
])
app.run_server()
Puede ser tedioso construir un layout utilizando únicamente los dash_html_components.
Para desplegar bloques de texto con formato de manera rápida, puedes usar el componente Markdown del dash_core_components.
from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
import plotly.express as px
df = px.data.iris() # iris is a pandas DataFrame
app = JupyterDash(__name__)
fig = px.scatter(df, x='petal_length', y='sepal_length')
app.layout = html.Div([
dcc.Graph(
figure=fig,
style={'height': 300},
)
])
app.run_server()
Puedes utilizar DataFrames de Pandas como la fuente de datos de tus tableros Dash.
Dash provee Plotly Express, el cual contiene funciones para crear gráficas ricas y complejas de manera fácil.
El componente dcc.Graph puede visualizar figuras generadas por Plotly y Plotly Express:
Un callback es una función de Python que permite definir la interacción de un tablero Dash.
Cada componente Dash puede tener un id único
Un callback contiene uno o más Input y Output:
Cada entrada/salida está asociada al id de un componente y a un atributo del componente.
import dash_html_components as html
import dash_core_components as dcc
from jupyter_dash import JupyterDash
from dash.dependencies import Input, Output
app = JupyterDash(__name__)
app.layout = html.Div([
html.Div(['Text:',
dcc.Input(id='text-input',
value='',
type='text')]),
html.Div(id='echo-div')
])
@app.callback(
Output('echo-div', 'children'),
Input('text-input', 'value')
)
def update_echo_div_callback(text_value):
if text_value:
return f'Echo: {text_value}'
app.run_server()
Es necesario asignar un id único a un componente si deseamos hacer referencia a éste en un callback.
Un Output dado sólo puede aparecer en un callback.
Pero un Input dado puede aparecer en más de un callback.
Los callbacks son disparados de manera automática al iniciar la aplicación.
from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
app = JupyterDash(__name__)
app.layout = html.Div([
dcc.Input(
id='x',
type='number',
value=1
),
dcc.Input(
id='c',
type='number',
value=0
),
html.Table([
html.Tr(html.Td(id='y-1-td')),
html.Tr(html.Td(id='y-2-td')),
html.Tr(html.Td(id='y-3-td')),
html.Tr(html.Td(id='y-4-td')),
html.Tr(html.Td(id='y-5-td'))
])
])
@app.callback(
Output('y-1-td', 'children'),
Output('y-2-td', 'children'),
Output('y-3-td', 'children'),
Output('y-4-td', 'children'),
Output('y-5-td', 'children'),
Input('x', 'value'),
Input('c', 'value'))
def compute_callback(x, c):
if (not x is None) and (not c is None):
return x*1+c, x*2+c, x*3+c, x*4+c, x*5+c
app.run_server(debug=True)
Un callback puede tener múltiples Inputs y Outputs
Tienes que asegurarte que todos los Inputs tengan valores válidos:
Puedes pasar prevent_initial_call = True al decorador de un callback para evitar que el callback se dispare automáticamente
Diseñar callbacks puede ser complicado en un principio ¡No te desanimes!
Dash provee herramientas para que puedas desarrollar tus propios componentes
Básicamente, un componente Dash no es más que un envoltorio de un componente React.js
Dash goza de una nutrida comunidad de desarrolladores que han creado sus propios componentes Dash
Provee componentes de UI basados en el framework Bootstrap 4:
Modal
Cards
Form
Jumbotron
Navbar
Paso 0. Haz una copia del siguiente notebook:
Paso 1. Asigna a la variable df el dataset Iris provisto por Plotly Express:
df = px.data.iris() # iris is a pandas DataFrame
Paso 2. Define un Dropdown que tenga como options diferentes columnas del dataset. Asígnale el id xaxis-dropdown:
dcc.Dropdown(
id='xaxis-dropdown',
options=[
{'label': 'sepal_width', 'value': 'sepal_width'},
{'label': 'sepal_length', 'value': 'sepal_length'},
{'label': 'petal_length', 'value': 'petal_length'}
],
value='sepal_width',
)
Paso 3. Define otro Dropdown que tenga como options diferentes columnas del dataset. Asígnale el id yaxis-dropdown:
dcc.Dropdown(
id='yaxis-dropdown',
options=[
{'label': 'sepal_width', 'value': 'sepal_width'},
{'label': 'sepal_length', 'value': 'sepal_length'},
{'label': 'petal_length', 'value': 'petal_length'}
],
value='sepal_width',
)
Paso 4. un objeto dcc.Graph en el layout en donde se ubicará el scatterplot. Asígnale el id top-graph:
dbc.Col(dcc.Graph(id='top-graph'), md=8),
Paso 5. Define un callback que tenga como inputs el valor de xaxis-dropdown y yaxis-dropdown y como output la figura de top-graph:
@app.callback(
Output('top-graph', 'figure'),
Input('xaxis-dropdown', 'value'),
Input('yaxis-dropdown', 'value'),
)
def update_callback(xaxis_col, yaxis_col):
fig = px.scatter(df, x=xaxis_col, y=yaxis_col)
return fig
Ahora inténtalo con cualquier otro dataset de Plotly Express:
Puedes utilizar archivos CSV como fuente de datos de tus tableros Dash utilizando Pandas.
Puedes utilizar Pandas para manipular el dataset dentro de un callback (e.g. filtrarlo u ordenarlo).
La biblioteca Dash Bootstrap Components permite la utilización de componentes de Bootstrap 4 en nuestras aplicaciones Dash.
Biblioteca Dash desarrollada en CentroGeo.
Permite la visualización de redes de caminos grandes utilizando WebGL.
Las visualizaciones son altamente personalizables.
Eventos de click para nodos y enlaces de la red.
import osmnx as ox
from dash import Dash
from dash_html_components import Div
from dash_sylvereye import SylvereyeRoadNetwork
from dash_sylvereye.utils import load_from_osmnx_graph
# configuración de la visualización
OSMNX_QUERY = 'Kamppi, Helsinki, Finland'
TILE_LAYER_URL = '//stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png'
TILE_LAYER_SUBDOMAINS = 'abcd'
TILE_LAYER_ATTRIBUTION = 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
MAP_CENTER = [60.1663, 24.9313]
MAP_ZOOM = 15
MAP_STYLE = {'width': '100%', 'height': '98vh'}
# obtener la topología de la red desde OSM
road_network = ox.graph_from_place(OSMNX_QUERY, network_type='drive')
nodes_data, edges_data = load_from_osmnx_graph(road_network)
# construir visualización
app = Dash()
app.layout = Div([
SylvereyeRoadNetwork(
id='sylvereye-roadnet',
tile_layer_url=TILE_LAYER_URL,
tile_layer_subdomains=TILE_LAYER_SUBDOMAINS,
tile_layer_attribution=TILE_LAYER_ATTRIBUTION,
map_center=MAP_CENTER,
map_zoom=MAP_ZOOM,
map_style=MAP_STYLE,
nodes_data=nodes_data,
edges_data=edges_data
)
])
# correr visualización
if __name__ == '__main__':
app.run_server()
Dash Sylvereye permite incrustar la visualización de una red de caminos en tu tablero Dash
La topología de una red de caminos puede ser obtenida desde OSM a través de la biblioteca OSMnx
Dash Sylvereye permite la configuración de la capa de mapa web sobre la que se dibuja la red de caminos
import osmnx as ox
from dash import Dash
from dash.dependencies import Input, Output
from dash_html_components import Div
from dash_html_components import H2, H3
from dash_sylvereye import SylvereyeRoadNetwork
from dash_sylvereye.utils import load_from_osmnx_graph
# configuración de la visualización
OSMNX_QUERY = 'Kamppi, Helsinki, Finland'
TILE_LAYER_URL = '//stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png'
TILE_LAYER_SUBDOMAINS = 'abcd'
TILE_LAYER_ATTRIBUTION = 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
MAP_CENTER = [60.1663, 24.9313]
MAP_ZOOM = 15
MAP_STYLE = {'width': '100%', 'height': '84vh'}
# obtener la topología de la red desde OSM
road_network = ox.graph_from_place(OSMNX_QUERY, network_type='drive')
nodes_data, edges_data = load_from_osmnx_graph(road_network)
# construcción de la visualización
app = Dash()
app.layout = Div([
SylvereyeRoadNetwork(
id='sylvereye-roadnet',
tile_layer_url=TILE_LAYER_URL,
tile_layer_subdomains=TILE_LAYER_SUBDOMAINS,
tile_layer_attribution=TILE_LAYER_ATTRIBUTION,
map_center=MAP_CENTER,
map_zoom=MAP_ZOOM,
map_style=MAP_STYLE,
nodes_data=nodes_data,
edges_data=edges_data
),
H5("Clicked elements:"),
H6(id='h3-clicked-node-coords'),
H6(id='h3-clicked-edge-coords')
])
# callbacks
@app.callback(
Output('h3-clicked-node-coords', 'children'),
[Input('sylvereye-roadnet', 'clicked_node')])
def update_node_data(clicked_node):
if clicked_node:
return f'Clicked node coords: {clicked_node["data"]["lat"]}, \
{clicked_node["data"]["lon"]}'
@app.callback(
Output('h3-clicked-edge-coords', 'children'),
[Input('sylvereye-roadnet', 'clicked_edge')])
def update_edge_data(clicked_edge):
if clicked_edge:
return f'Clicked edge coords: {clicked_edge["data"]["coords"]}'
# correr visualización
if __name__ == '__main__':
app.run_server()
Dash Sylvereye permite interactuar con los nodos (cruces de caminos) y los enlaces (caminos)
Lo anterior a través de la detección de eventos de click
Esto significa que puede asociar un callback al evento click de los nodos y/o enlaces
Dash Sylvereye pasará al callback los datos del elemento clickeado
import osmnx as ox
import numpy as np
from dash import Dash
from dash_html_components import Div
from dash_sylvereye import SylvereyeRoadNetwork
from dash_sylvereye.utils import load_from_osmnx_graph
from dash_sylvereye.enums import NodeSizeMethod, EdgeColorMethod, EdgeWidthMethod
from dash_sylvereye.defaults import get_default_node_options, get_default_edge_options
OSMNX_QUERY = 'Kamppi, Helsinki, Finland'
TILE_LAYER_URL = '//stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png'
TILE_LAYER_SUBDOMAINS = 'abcd'
TILE_LAYER_ATTRIBUTION = 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
MAP_CENTER = [60.1663, 24.9313]
MAP_ZOOM = 15
MAP_STYLE = {'width': '100%', 'height': '84vh'}
# obtener la red desde OSM
road_network = ox.graph_from_place(OSMNX_QUERY, network_type='drive')
nodes_data, edges_data = load_from_osmnx_graph(road_network)
# asignar pesos aleatorios a los nodos y enlaces de acuerdo a una dist. libre de escala
for node in nodes_data: node["data"]["weight"] = 1 - np.random.power(a=3, size=None)
for edge in edges_data: edge["data"]["weight"] = 1 - np.random.power(a=3, size=None)
# configurar las opciones visuales de los nodos
node_options = get_default_node_options()
node_options["alpha_default"] = 0.25
node_options["size_method"] = NodeSizeMethod.SCALE
node_options["size_scale_field"] = "weight"
# configurar las opciones visualies de los enlaces
edge_options = get_default_edge_options()
edge_options["width_method"] = EdgeWidthMethod.SCALE
edge_options["width_scale_field"] = "weight"
edge_options["color_method"] = EdgeColorMethod.SCALE
edge_options["color_scale_field"] = "weight"
edge_options["color_scale_left"] = 0xcbdbff
edge_options["color_scale_right"] = 0x06696
# construir la visualización
app = Dash()
app.layout = Div([
SylvereyeRoadNetwork(
id='sylvereye-roadnet',
tile_layer_url=TILE_LAYER_URL,
tile_layer_subdomains=TILE_LAYER_SUBDOMAINS,
tile_layer_attribution=TILE_LAYER_ATTRIBUTION,
map_center=MAP_CENTER,
map_zoom=MAP_ZOOM,
map_style=MAP_STYLE,
nodes_data=nodes_data,
edges_data=edges_data,
node_options=node_options,
edge_options=edge_options
)
])
# correr servidor
if __name__ == '__main__':
app.run_server()
Dash Sylvereye permite la configuración visual de los nodos y enlaces individuales de la red
Para los nodos es posible configurar su diámetro, color y transparencia
Para los enlaces es posible configurar su grosor, color y transparencia
Dash Sylvereye puede calcular automáticamente el color de nodos y enlaces en función de un atributo de peso
import osmnx as ox
from dash import Dash
from dash.dependencies import Input, Output
from dash_html_components import Div
from dash_html_components import H2, H3
from dash_sylvereye import SylvereyeRoadNetwork
from dash_sylvereye.utils import load_from_osmnx_graph, generate_markers_from_coords
from dash_sylvereye.defaults import get_default_marker_options
OSMNX_QUERY = 'Kamppi, Helsinki, Finland'
TILE_LAYER_URL = '//stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png'
TILE_LAYER_SUBDOMAINS = 'abcd'
TILE_LAYER_ATTRIBUTION = 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
MAP_CENTER = [60.1663, 24.9313]
MAP_ZOOM = 15
MAP_STYLE = {'width': '100%', 'height': '80vh'}
# obtener red de caminos desde OSM
road_network = ox.graph_from_place(OSMNX_QUERY, network_type='drive')
nodes_data, edges_data = load_from_osmnx_graph(road_network)
# crear un marcador por cada nodo de la red
markers_coords = [ [node_data["lat"], node_data["lon"]] for node_data in nodes_data ]
markers_data = generate_markers_from_coords(markers_coords)
# escalar el tamaño de los marcadores al nivel de zoom
marker_options = get_default_marker_options()
marker_options["enable_zoom_scaling"] = True
# construir visualización
app = Dash()
app.layout = Div([
SylvereyeRoadNetwork(
id='sylvereye-roadnet',
tile_layer_url=TILE_LAYER_URL,
tile_layer_subdomains=TILE_LAYER_SUBDOMAINS,
tile_layer_attribution=TILE_LAYER_ATTRIBUTION,
map_center=MAP_CENTER,
map_zoom=MAP_ZOOM,
map_style=MAP_STYLE,
nodes_data=nodes_data,
edges_data=edges_data,
markers_data=markers_data,
marker_options=marker_options
),
H2("Clicked elements:"),
H3(id='h3-clicked-marker-coords')
])
@app.callback(
Output('h3-clicked-marker-coords', 'children'),
[Input('sylvereye-roadnet', 'clicked_marker')])
def update_marker_data(clicked_marker):
if clicked_marker:
marker = clicked_marker["marker"]
return f'Clicked marker coords: {[ marker["lat"], marker["lon"] ]}'
# correr visualización
if __name__ == '__main__':
app.run_server()
Dash Sylvereye permite visualizar marcadores encima tanto del mapa web como de la red
Los marcadores son interactivos: es posible asociar un callback al evento click de cada marcador
Dash Sylvereye pasará los datos del marcador clickeado a un parámetro del callback
Paso 1. Haz una copia del siguiente notebook:
Paso 2. Asigna al campo "weight" de todos los enlaces la longitud de la calle:
for edge in edges_data:
edge["data"]["weight"] = edge["data"]["length"]
Paso 3. Indica a Dash Sylvereye que el ancho, color y transparencia (alpha) de los enlaces estarán escalados en proporción al campo "weight":
edge_options = get_default_edge_options()
edge_options["width_method"] = EdgeWidthMethod.SCALE
edge_options["width_scale_field"] = "weight"
edge_options["color_method"] = EdgeColorMethod.SCALE
edge_options["color_scale_field"] = "weight"
edge_options["color_scale_left"] = 0xcbdbff
edge_options["color_scale_right"] = 0x06696
edge_options["alpha_method"] = EdgeAlphaMethod.SCALE
edge_options["alpha_scale_field"] = "weight"
Paso 4. Define un barra de navegación (navbar):
navbar = dbc.NavbarSimple(
brand="Visualization of street lengths with Dash Sylvereye",
brand_href="#",
color="primary",
dark=True,
)
Paso 5. Define un componente Dash Sylvereye que sólo reciba los datos de los enlaces y sus opciones de visualización:
sylvereye = \
SylvereyeRoadNetwork(
id='sylvereye-roadnet',
tile_layer_url=TILE_LAYER_URL,
tile_layer_subdomains=TILE_LAYER_SUBDOMAINS,
tile_layer_attribution=TILE_LAYER_ATTRIBUTION,
map_center=MAP_CENTER,
map_zoom=MAP_ZOOM,
map_style=MAP_STYLE,
edges_data=edges_data,
edge_options=edge_options,
tile_layer_opacity=TILE_LAYER_OPACITY
)
Paso 6. Define un componente Markdown en donde se mostrará una tabla con los datos del enlace clickeado:
markdown = \
Markdown(id='markdown-street-data')
Paso 7. Pon todos los componentes juntos para conformar el layout de la visualización:
app.layout = dbc.Container(
[
navbar,
dbc.Row(
[
dbc.Col(sylvereye, md=10),
dbc.Col(markdown, md=2)
],
align='center'
)
], fluid=True
)
Paso 8. Define un callback que devuelva una tabla Markdown con los datos del enlace clickeado:
@app.callback(
Output('markdown-street-data', 'children'),
[Input('sylvereye-roadnet', 'clicked_edge')])
def update_street_data(clicked_edge):
if clicked_edge:
data = clicked_edge["data"]["data"]
return f'''
## Query
{OSMNX_QUERY}
## Street data
| Property | Value |
| -------- | ----- |
| Source OSMID | {data["source_osmid"]} |
| Target OSMID | {data["target_osmid"]} |
| Street name| {data["name"]} |
| Max. speed | {data["maxspeed"]} |
| Length | {data["length"]} |
| One-way | {data["oneway"]} |
'''
Ahora inténtalo con con tu propia ciudad obteniendo las coordenadas centrales del sitio de OpenStreetMap:
Pasos para obtener las coordenadas centrales:
Actualiza las siguientes constantes en la libreta:
OSMNX_QUERY = 'poza rica de hidalgo, veracruz, mexico'
MAP_CENTER = [20.5377, -97.4405]
Finalmente, corre nuevamente todas las celdas, exceptuando las primeras celdas de instalación de bibliotecas.
Dash Sylvereye puede ser combinado con Dash Core components y Dash Bootstrap Components para crear tableros más atractivos
Dash Sylvereye puede establecer el estilo de los enlaces (grosor, color y transparencia) en función de los datos almacenados en los enlaces
Dash Sylvereye permite la personalización de la escala de colores
agarcia@centrogeo.edu.mx