Desarrollo de Tableros Web de Visualización de Datos con Python, Dash y Dash Sylvereye
Dr. Alberto García Robledo
CentroGeo Querétaro
Escuela de Verano Científico CentroGeo 2022
Contenido
- ¿Qué es Dash?
- Layouts y callbacks de Dash
- Actividad: Visualizando correlaciones
- Dash Sylvereye: Visualización de redes de caminos
- Actividad: Visualizando las calles más largas
¿Qué es Dash?
¿Qué es Dash?
-
Dash es código abierto, publicado bajo la licencia MIT.
-
Está disponible para múltiples lenguajes:
-
Python
-
R
-
Julia
-
.NET
-
Instalación
-
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
Layouts de Dash
-
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:
Ejemplo 1: Hello Dash
Ejemplo 1: Hello Dash
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()
Ejemplo 1: Para recordar
-
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.
Ejemplo 2: Markdown
Ejemplo 2: Markdown
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()
Ejemplo 2: Para recordar
-
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.
Ejemplo 3: Hello Graph
Ejemplo 3: Hello Graph
Ejemplo 3: Hello Graph
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()
Ejemplo 3: Para recordar
-
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:
- Sólo hay que pasar la figura Plotly al componente dcc.Graph a través del argumento "figure"
Callbacks de Dash
-
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.
-
Ejemplo 4: Callback simple
Ejemplo 4: Callback simple
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()
Ejemplo 4: Para recordar
-
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.
Ejemplo 5: Múltiples Inputs y Outputs
Ejemplo 5: Múltiples Inputs y Outputs
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)
Ejemplo 5: Para recordar
-
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!
-
Componentes Dash de terceros
-
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
Dash Bootstrap Components
-
Provee componentes de UI basados en el framework Bootstrap 4:
-
Modal
-
Cards
-
Form
-
Jumbotron
-
Navbar
-
Actividad 1: Correlaciones
Actividad 1: Correlaciones
Paso 0. Haz una copia del siguiente notebook:
Actividad 1: Correlaciones
Paso 1. Asigna a la variable df el dataset Iris provisto por Plotly Express:
Actividad 1: Correlaciones
df = px.data.iris() # iris is a pandas DataFrame
Actividad 1: Correlaciones
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',
)
Actividad 1: Correlaciones
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',
)
Actividad 1: Correlaciones
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),
Actividad 1: Correlaciones
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
Actividad: Correlaciones
Ahora inténtalo con cualquier otro dataset de Plotly Express:
Actividad: Para recordar
-
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.
Dash Sylvereye
-
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.
Ejemplo 6: Visualización de una red de caminos
Ejemplo 6: Visualización de una red de caminos
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()
Para recordar
-
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
Ejemplo 7: Interacción con la red de caminos
Ejemplo 7: Interacción con 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()
Para recordar
-
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
Ejemplo 8: Personalización de la visualización de una red
Ejemplo 8: Personalización de la visualización de una red
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()
Para recordar
-
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
Ejemplo 9: Visualización de marcadores sobre la red
Ejemplo 9: Visualización de marcadores sobre la red
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()
Para recordar
-
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
Actividad 2: Calles más largas
Paso 1. Haz una copia del siguiente notebook:
Actividad 2: Calles más largas
Actividad 2: Calles más largas
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"
Actividad 2: Calles más largas
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,
)
Actividad 2: Calles más largas
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
)
Actividad 2: Calles más largas
Paso 6. Define un componente Markdown en donde se mostrará una tabla con los datos del enlace clickeado:
markdown = \
Markdown(id='markdown-street-data')
Actividad 2: Calles más largas
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
)
Actividad 2: Calles más largas
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"]} |
'''
Actividad 2: Calles más largas
Actividad 2: Calles más largas
Ahora inténtalo con con tu propia ciudad obteniendo las coordenadas centrales del sitio de OpenStreetMap:
Actividad 2: Calles más largas
Pasos para obtener las coordenadas centrales:
Actividad 2: Calles más largas
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.
Para recordar
-
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
¡Gracias!
agarcia@centrogeo.edu.mx
Verano CentroGeo 2022
By Alberto Garcia-Robledo
Verano CentroGeo 2022
- 469