Programación

Vistazo Rápido a Python Dash – Plotly para la creación rápida de Dashboards Dinámicos

Son librerías de pytohn que en conjunto trabajan para crear dashboards dinámicos:

Para esta Guía se usan:

dash==3.2.0
Flask==3.1.1
plotly==6.2.0

El diseño básico de scripts puede seguir el siguiente orden:

  1. Importado de librerías
  2. Implementación de la aplicación
  3. Diseño del layout
  4. Desarrollo de funciones callback para inputs y outputs
  5. Ejecución de la aplicación
  6. Ejecución del script

Importado de librerías

import dash
import plotly.express as px

from dash.dependencies import Input, Output
from dash import dcc, html
from dash.exceptions import PreventUpdate
from flask import Flask

Implementación de la aplicación

La aplicación Dash es un objeto que instancia el framework Dash para el diseño del dashboard; esta instancia requiere un parámetro que es instancia de Flask, que es un objeto que implementa la aplicación WGSI (basicamente y a muy grandes razgos como un servidor web).

# Hojas de estilo externas
external_stylesheets = [
    {
        'atributo_1': 'valor_1',
        'atributo_2': 'valor_2',
        ...
        'atributo_n': 'valor_n',
    },
    ]

# Scripts externos
external_scripts = [
    {
        'atributo_1': 'valor_1',
        'atributo_2': 'valor_2',
        ...
        'atributo_n': 'valor_n',
    },
    ]

server_flask = Flask("nombre_para_el_wsgi")

app = dash.Dash("nombre_para_para_la_app", # puede ser el mismo nombre que el del server flask
                external_stylesheets=external_stylesheets,
                external_scripts=external_scripts,
                server=server_flask,
                title="Titulo a mostrar en la barra de título")

Cada hoja de estilos o script externo tiene la forma llave – valor, para cada uno de los atributos de la etiqueta link o script a generar. Por ejemplo:

a) Para generar:

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous" />

la entrada sería:

{
    'href': 'https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css',
    'rel': 'stylesheet',
    'integrity': 'sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC',
    'crossorigin': 'anonymous',
},

b) Para generar:

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>

la entrada seria:

{
    'src': 'https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js',
    'integrity': 'sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM',
    'crossorigin': 'anonymous',
},

Por default, todos los archivos (*.css y *.js) en la carpeta ./assets son integrados automáticamente.

Diseño del layout

El layout[1][2][3][4]de la aplicación se personaliza en el miembro layout de la instancia Dash (ejemplo app.layouot), que usualmente es es un div.

En el diseño del layout se emplea el modulo importado html, el cual contiene las clases correspondientes a las etiquetas HTML normales (Div, P, Label, etc.) las cuales cuando son elementos que aceptan etiquetas internas (como los div), éstos elementos hijo se insertan como primer parámetro en una lista (o tupla, en general una variable python iterable) de elementos, cuando los elementos que usualmente no aceptan etiquetas hijo (por ejemplo label, p, etc.), el primer elemento es el contenido tipo texto de la etiqueta. En términos generales, todos las clases del módulo html reciben como parámetros opcionales los argumentos className y id.

El módulo dcc incluye todos los elementos necesarios para la página web que permiten establecer comunicación con el usuario tanto para la entrada como salida de información (dependiendo del contexto particular del tablero un elemento puede emplearse como de entrada, salida o ambas.). Es muy importante que de cada elemento dcc que utilicemos establezcamos claramente su id y conozcamos la propiedad de la cual vamos a extraer el valor o en la cual realizaremos cambios para mostrar resultados.

Los principales elementos de entrada son: Checklist, DatePickerRange, DatePickerSingle, Dropdown, Input, RadioItems, RangeSlider, Slider, Textarea

Los principales elementos de salida son: Graph

También es posible utilizar la clase dash.dash_table.DataTable para trabajar de forma dinámica con tablas.

Ejemplo de dashboard para mostrar la gráfica de f(x):

f(x) = \frac{x}{10}\sin(x^2)+\frac{x}{10}
import math
from dash import dash_table

def funcion(x):
    return x / 10 * math.sin(x ** 2) + x / 10

def evalua_funcion(inicio, fin, incremento):
    resultados = list()
    x = inicio
    while x < fin + incremento:
        resultados.append({'x': round(x, 4), 'y': round(funcion(x), 4)})
        x += incremento
    return resultados

X_INICIO = -5
X_FIN = 5
INCREMENTO = 0.1

app.layout = html.Div([
    html.Nav([
        html.Div([
            html.Span("Mi Graficador", className="navbar-brand mb-0 h1"),
            ], className="container"),
    ], className="navbar navbar-dark bg-dark"),
    html.Div([
        dcc.Markdown("""
            $$
            f(x) = \\frac{x}{10} \\sin(x^2) + \\frac{x}{10}
            $$
        """, mathjax=True, className="h1"),
        html.Div([
            html.Div([
                html.Div([
                    html.Label("X Inicial: ", htmlFor="x_inicio", className="form-label"),
                    dcc.Input(id="x_inicio", type="number", value=X_INICIO, className="form-control"),
                ]),
                html.Div([
                    html.Label("X Final: ", htmlFor="x_fin", className="form-label"),
                    dcc.Input(id="x_fin", type="number", value=X_FIN, className="form-control"),
                ]),
                html.Div([
                    html.Label("Incremento: ", htmlFor="incremento", className="form-label"),
                    dcc.Slider(0.1, 10, id="incremento", value=INCREMENTO)
                ]),
                html.Div("", style={"height": "250px"}, className="d-none d-sm-none d-md-block"),
            ], className="col"),
            html.Div([
                dcc.Graph(id="grafica_f", figure={})
            ], className="col"),
        ], className="row mb-3"),
        html.Div("Tabla de Datos", className="h3", id="tabla_titulo"),
        html.Div([
            dash_table.DataTable(evalua_funcion(X_INICIO, X_FIN, INCREMENTO), id="tabla_datos")
        ], className="mb-3"),
    ], className="container"),
])

Y generaría un dashboard similar al siguiente:

Desarrollo de funciones callback para inputs y outputs

Dash emplea callback functions, las cuales son funciones que se ejecutan automáticamente cuando algún elemento de entrada cambia su valor con el objetivo de actualizar un elemento de salida. La forma general es la siguiente:

@app.callback(
    Output(component_id="elemento_de_salida_1", component_property="propiedad_a_actualizar"),
    Output(component_id="elemento_de_salida_2", component_property="propiedad_a_actualizar"),
    ....
    Output(component_id="elemento_de_salida_n", component_property="propiedad_a_actualizar"),
    Input(component_id="elemento_de_entrada_1", component_property="propiedad_de_valor"),
    Input(component_id="elemento_de_entrada_2", component_property="propiedad_de_valor"),
    ...
    Input(component_id="elemento_de_entrada_n", component_property="propiedad_de_valor"),
)
def funcion_de_actualizacion(valor_input_1, valor_input_2, ..., valor_input_n):
     # Código de la funcion de actualicacion
    return 
        valor_retorno_salida_1,
        valor_retorno_salida_2,
        ...
        valor_retorno_salida_n,

Un objeto de entrada puede aparecer en una o mas callback functions, y un objeto de salida puede ser actualizado por una o más callback functions.

Una forma de calback para el ejemplo es:

@app.callback(
    Output(component_id="grafica_f", component_property="figure"),
    Output(component_id="tabla_datos", component_property="data"),
    Output(component_id="tabla_titulo", component_property="children"),
    Input(component_id="x_inicio", component_property="value"),
    Input(component_id="x_fin", component_property="value"),
    Input(component_id="incremento", component_property="value"),
)
def actualiza_datos(valor_inicio, valor_fin, valor_incremento):
    if valor_incremento is None or valor_fin is None or valor_incremento is None:
        raise PreventUpdate
    res = evalua_funcion(valor_inicio, valor_fin, valor_incremento)
    return (
        px.line(res, x='x', y='y'),
        res,
        f"Tabla de Datos desde {valor_inicio} hasta {valor_fin} con incrementos de {valor_incremento}"
        )

Que genera el siguiente mapa de callback:

Ejecución de la aplicación

app.run(debug=True, port=8055)

Ejecución del script

python app2.py