Apunte 35 — Dashboards con Quarto: layout, value boxes, plotly, gt y DT

Analítica de Personas · Semestre otoño 2026 · Semana 16 · Prof. René Gempp

1. Reporte, dashboard y presentación: tres cosas distintas

Durante quince semanas tu entregable fue un reporte: un documento .qmd que documenta un análisis de principio a fin, pensado para que alguien (tu profesor, tu yo del futuro) reconstruya cada paso. Esta semana cambias de vehículo. El directorio de InnovaCo no va a leer once reportes. Necesita un dashboard.

VehículoPropósitoDensidadCuándo
ReporteDocumentar y reproducirAltaLas tareas; la trazabilidad importa
DashboardMonitorear de un vistazoMedia, jerarquizadaIndicadores que se revisan periódicamente
PresentaciónMover a una decisiónBaja, una idea por slideUna reunión, una decisión

La definición canónica de dashboard es la de Stephen Few (2013): una representación visual de la información más importante necesaria para alcanzar un objetivo, consolidada en una sola pantalla para monitorearse de un vistazo. Cada palabra es una restricción: la más importante (no todo), una sola pantalla (sin scroll infinito), de un vistazo (la prueba del vistazo: si necesitas estudiarlo, falló).

La prueba del vistazo (glance test). Antes de entregar, muéstrale el dashboard a alguien por 10 segundos, tápalo, y pregúntale qué entendió. Si no captó el estado general, tienes demasiada tinta o muy poca jerarquía.

2. Quarto Dashboards: un format nuevo, no una herramienta nueva

La buena noticia: no aprendes una herramienta nueva. Un Quarto Dashboard es el mismo .qmd que ya dominas, con una línea distinta en el YAML:

---
title: "InnovaCo · Dashboard Ejecutivo"
format:
  dashboard:           # <- esta es la magia
    orientation: rows
execute:
  echo: false           # el directorio no ve código
  warning: false
---

Con format: dashboard, Quarto deja de producir un documento que se lee de arriba abajo y produce una cuadrícula que llena la pantalla. El contenido se reparte el alto disponible (comportamiento fill): no hay scroll infinito, hay un tablero.

¿Y flexdashboard? Si googleas dashboards en R vas a encontrar flexdashboard, su antecesor. Quarto Dashboards lo reemplaza: misma idea, mejor integración, soporte nativo de parámetros. Usa Quarto Dashboards.

3. Layout: páginas, filas, columnas y cards

El layout se controla con encabezados Markdown y atributos entre llaves:

# Página principal

## Row {height="20%"}      # fila de KPIs (20% del alto)

# ...aquí van 4 value boxes...

## Row {height="40%"}      # fila de gráficos

### Column {width="50%"}   # media fila
### Column {width="50%"}   # la otra media

## Row {height="40%"} {.tabset}  # cards como pestañas
El título de la card. Para ponerle título a un gráfico o tabla, usa la opción de chunk #| title: "Rotación 12 meses". Ese texto aparece como encabezado de la card. Es distinto del labs(title=) de ggplot — en un dashboard, deja el título a la card y usa subtitle en el gráfico para el matiz.

4. Value boxes: el KPI grande

Una value box es la card estrella del dashboard ejecutivo: un número grande, un ícono, y un color que comunica el estado. La forma más limpia de crearla es un chunk con #| content: valuebox que devuelve una lista:

```{r}
#| content: valuebox
#| title: "Rotación acumulada"
list(
  value = percent(rotacion_global, 0.1),
  icon  = "box-arrow-right",   # íconos de Bootstrap
  color = col_rot                # "success" | "danger" | "warning"
)
```

El color acepta nombres semánticos de Bootstrap: "success" (verde), "danger" (rojo), "warning" (ámbar), "primary" (azul). La gracia es calcular el color según la lógica de negocio, de modo que un KPI que está mal se vea mal sin que nadie lo lea:

col_rot  <- if (rotacion_global > 0.20) "danger" else "success"
col_enps <- if (enps_actual > 0)     "success" else "danger"

Para íconos con más control, bslib::value_box() + bsicons::bs_icon() ofrecen la versión completa, útil si quieres incrustar un sparkline de fondo o personalizar el tema.

Un value box no es solo el número. Es número + variación + color. "Headcount = 874" es trivia. "874, +3% vs. el año pasado, dentro del plan" es un KPI. El contexto (la variación y el referente) es lo que convierte un dato en información para decidir.

5. Métricas, KPIs y tipos de dashboard

No todo lo que se mide es clave. Una métrica es cualquier número medible. Un KPI (Key Performance Indicator) es una métrica que además está vinculada a un objetivo, es accionable, tiene dueño y tiene referente. Un dashboard estratégico bien hecho tiene entre 4 y 8 KPIs: más que eso, deja de ser "lo clave".

Eckerson (2010) distingue tres tipos de dashboard según el horizonte de decisión:

TipoAudienciaPregunta
EstratégicoDirectorio¿Vamos por buen camino?
OperacionalJefaturas¿Qué pasa ahora que requiere acción?
AnalíticoAnalistas¿Por qué está pasando?

El de Montt es estratégico: pocos KPIs, alto nivel de agregación, foco en la tendencia. Por eso privilegiamos value boxes y líneas de tendencia, y mandamos el detalle al drill-down (pestañas, tablas interactivas).

Adelantados vs. rezagados

Un indicador rezagado (lagging) mide algo que ya ocurrió (rotación, brecha): certero pero tardío. Un indicador adelantado (leading) predice un resultado futuro (eNPS, satisfacción): menos certero pero permite actuar antes. La rotación es el espejo retrovisor; el eNPS es el parabrisas. El dashboard potente combina ambos (Kaplan & Norton, 1996; Becker, Huselid & Ulrich, 2001).

6. Integridad de métricas: el caso del eNPS

El eNPS (employee Net Promoter Score) adapta el NPS de Reichheld (2003). Se basa en una pregunta: "De 0 a 10, ¿qué tan probable es que recomiendes InnovaCo como lugar para trabajar?"

eNPS = %Promotores (9–10) − %Detractores (0–6)

Va de −100 a +100. Los pasivos (7–8) no suman ni restan.

No fabriques el eNPS desde la satisfacción 1–5. Es tentador mapear "5 = promotor, 1–3 = detractor" sobre la satisfaccion_laboral del dataset. No lo hagas. El titular cambia por completo según dónde cortes los buckets, así que el número resultante no significa nada. El eNPS requiere su propia pregunta 0–10. En InnovaCo, el eNPS vive en innovaco_kpis_mensuales.csv (la encuesta de pulso); la satisfacción 1–5 se reporta aparte como "índice de satisfacción". Confundirlas es el error de integridad de métrica más común en RRHH — y se evalúa en la tarea.

7. ¿Qué tan interactivo es un dashboard de Quarto?

Conviene ser preciso, porque "interactivo" significa tres cosas distintas. Lo que produce quarto render (sin servidor) es un HTML estático; sobre esa base hay tres niveles de interactividad:

Nivel¿Servidor?Qué permiteHerramientas
(a) WidgetNoHover, zoom, ordenar, buscar, descargar dentro de cada gráfico/tablaplotly, DT, leaflet
(b) CrosstalkNoFiltrado cruzado: seleccionar en una tabla filtra un gráficocrosstalk, ojs
(c) ShinySí (R vivo)Reactividad total: un input vuelve a EJECUTAR código Rserver: shiny

El dashboard de esta clase usa el nivel (a): plotly y DT traen su propia interactividad en JavaScript y funcionan perfectamente en el HTML estático, sin servidor. Puedes hacer hover, zoom, ordenar, buscar y —como veremos— descargar.

Dos cosas que NO son interactividad reactiva. (1) La parametrización (params$depto) genera un archivo distinto por departamento al renderizar; el usuario no filtra en vivo, abre el archivo de su área. (2) Los widgets plotly/DT son interactivos pero no recalculan en R: si quieres que mover un control re-ajuste un modelo o recompute un agregado, necesitas Shiny (server: shiny). Para "filtrado cruzado" ligero sin servidor, crosstalk enlaza widgets entre sí del lado del cliente.
library(plotly)

# Nivel (a): widget interactivo a partir de un ggplot
g <- kpis_mes |> filter(departamento != "Global") |>
  ggplot(aes(mes, enps, color = departamento)) + geom_line()

ggplotly(g, tooltip = c("x", "y", "colour"))
Interactivo no siempre es mejor. Para el directorio que ve la pantalla una vez en una reunión, un gráfico estático que controlas cuenta mejor la historia: nadie se distrae haciendo zoom. La interactividad de widget gana cuando el usuario va a explorar repetidamente. Decide por audiencia, no por reflejo.

8. Tablas estáticas de presentación: gt

El paquete gt ("grammar of tables") construye tablas de calidad de publicación. Su lógica es la del ggplot: empiezas con gt() y vas agregando capas de formato con el pipe.

library(gt)

tabla |>
  gt() |>
  cols_label(rotacion = "Rotación", ingreso = "Ingreso medio") |>
  fmt_percent(rotacion, decimals = 1) |>
  fmt_currency(ingreso, currency = "CLP", decimals = 0, sep_mark = ".") |>
  data_color(                       # color condicional
    columns = rotacion,
    fn = scales::col_numeric(c("#0D9488", "#B85042"), domain = NULL)
  ) |>
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_column_labels()
  )

Funciones clave: fmt_number(), fmt_percent(), fmt_currency() formatean; cols_label() renombra encabezados; data_color() pinta celdas según su valor (el departamento con peor rotación se ve rojo); tab_header() y tab_source_note() agregan título y nota al pie (Iannone et al., 2024).

Cuándo gt. Para tablas que van a un informe, un slide o un PDF: el output es una imagen pulida, fija. Es la tabla que tú controlas al 100%.

9. Tablas interactivas: DT

El paquete DT envuelve la librería JavaScript DataTables: produce tablas con búsqueda, ordenamiento, paginación y filtros por columna. Ideal para que el usuario explore un detalle grande.

library(DT)

activos |>
  select(nombre, departamento, cargo, antiguedad_anios) |>
  datatable(
    filter  = "top",                 # filtros por columna
    options = list(pageLength = 10),  # filas por página
    rownames = FALSE
  )
gtDT
NaturalezaEstática (imagen)Interactiva (JS)
FortalezaFormato fino, publicaciónBuscar, ordenar, filtrar
DestinatarioDirectorio, informe, PDFAnalista, exploración
TamañoPocas filas, curadasMuchas filas, navegables

10. Parametrización: un archivo, muchos dashboards

Montt pidió la versión de Concepción, la de cada gerente, "que se genere sola". Eso son los parámetros de Quarto. Se declaran en el YAML:

---
format: dashboard
params:
  depto: "Global"     # valor por defecto
---

Dentro del documento, params$depto es un objeto R. Filtras los datos por él una vez, al inicio, y todo el dashboard se recalcula para esa área:

es_global <- params$depto == "Global"

emp_f <- if (es_global) empleados
         else filter(empleados, departamento == params$depto)

Para generar todas las versiones de una vez, renderizas en lote con quarto_render() dentro de un purrr::walk():

library(quarto)

areas <- c("Global", "Ventas", "Soporte Técnico")

purrr::walk(areas, \(d) {
  quarto_render(
    input          = "clase16_dashboard.qmd",
    execute_params = list(depto = d),
    output_file    = paste0("dashboard_", d, ".html")
  )
})
Esta es la diferencia entre copiar-pegar y automatizar. El analista junior arma siete dashboards a mano y reza para que los siete queden iguales. El analista que automatiza tiene un archivo, lo corrige una vez, y los siete se regeneran idénticos. Cuando los datos del próximo trimestre lleguen, vuelve a correr una línea. La pregunta nunca es "¿cuánto cuesta automatizar?" sino "¿cuántas veces voy a repetir esto?".

11. Datos incrustados y descarga: bajar la tabla como Excel

Pregunta frecuente del directorio: "¿puedo bajarme esta tabla a Excel?". La respuesta es sí, sin servidor. El truco tiene dos piezas: (1) hacer el HTML autocontenido con embed-resources: true en el YAML, de modo que los datos viajen dentro del archivo; y (2) ofrecer un mecanismo de descarga del lado del cliente.

Vía A — botones en la propia tabla (DT + extensión Buttons)

La extensión Buttons de DT agrega una barra con Copiar / CSV / Excel / PDF sobre la tabla. La descarga ocurre en el navegador (usa JSZip), con los datos ya incrustados en el widget:

tabla |>
  datatable(
    extensions = "Buttons",
    options = list(
      dom = "Bfrtip",            # B = barra de botones
      buttons = list(
        list(extend = "excel", text = "Descargar Excel"),
        list(extend = "csv",   text = "CSV"),
        list(extend = "copy",  text = "Copiar")
      )
    )
  )

Vía B — un botón de descarga para cualquier objeto (downloadthis)

Cuando quieres ofrecer "Descargar la base completa (.xlsx)" como un botón independiente (no atado a una tabla visible), downloadthis incrusta el archivo como base64 dentro del HTML:

library(downloadthis)

activos |>
  download_this(
    output_name      = "InnovaCo_activos",
    output_extension = ".xlsx",        # también ".csv"
    button_label     = "Descargar base (Excel)",
    button_type      = "success", has_icon = TRUE
  )
Un botón para un archivo YA creado. Si construiste un libro Excel de varias hojas con openxlsx (Apunte 36, sección 5), no lo vuelvas a armar: ofrécelo con downloadthis::download_file(path = "Reporte.xlsx", ...), que incrusta el archivo tal cual, con sus hojas y formatos. download_this() es para un objeto de R (una hoja); download_file() es para un archivo en disco (varias hojas).
Estos botones solo aparecen al RENDERIZAR a HTML. download_this() y download_file() producen un botón HTML: se ven en el .html renderizado (o en el Viewer de RStudio), no al correr la línea suelta en la consola. El demo que funciona está en clase16_mini_reporte.qmd.
El dashboard autocontenido se convierte en un “mini-portal”. Con embed-resources: true + botones de descarga, entregas un solo archivo HTML por correo o por Canvas. El directorio lo abre sin instalar nada, mira los KPIs, explora la tabla y se baja la planilla que necesita — todo desde el mismo archivo, sin servidor, sin pedirte nada.
Cómo obtener ese HTML (y no un "localhost"). Renderiza con quarto render dashboard.qmd o quarto::quarto_render("dashboard.qmd"): se escribe el archivo dashboard.html en disco. El localhost aparece solo con quarto preview, que es la vista previa en vivo. Para compartir, usa el .html generado por render (con embed-resources: true es un único archivo).
Sin bslib ni bsicons. Los value boxes de este dashboard se declaran con #| content: valuebox devolviendo una list(value=, icon=, color=). Quarto resuelve el ícono por su nombre de Bootstrap; no hay que cargar bslib ni bsicons. Si ves el error "no hay paquete llamado 'bsicons'", es porque un library(bsicons) quedó en el setup: bórralo.
Cuidado con el peso. Incrustar datos grandes (decenas de miles de filas) infla el HTML. Para el dashboard ejecutivo —pocas filas curadas— es ideal. Para volúmenes grandes, ofrece la descarga de un agregado, no de la base cruda.

12. Checklist del dashboard

Más allá de Quarto Dashboards. Cuando necesites cómputo reactivo en vivo del lado del servidor (inputs que disparan modelos pesados), el siguiente escalón es Shiny (Wickham, 2021, Mastering Shiny). Quarto Dashboards cubre el 90% de las necesidades ejecutivas sin servidor; Shiny entra cuando el 10% restante lo exige. Y en el mundo corporativo conviven con Power BI, Tableau y Looker: la ventaja del flujo en R/Quarto es la reproducibilidad y la integración con tu análisis.

13. Referencias

Becker, B. E., Huselid, M. A., & Ulrich, D. (2001). The HR scorecard: Linking people, strategy, and performance. Harvard Business School Press.
Eckerson, W. W. (2010). Performance dashboards: Measuring, monitoring, and managing your business (2.ª ed.). Wiley.
Few, S. (2013). Information dashboard design: Displaying data for at-a-glance monitoring (2.ª ed.). Analytics Press.
Iannone, R., Cheng, J., Schloerke, B., Hughes, E., Lauer, A., & Seo, J. (2024). gt: Easily create presentation-ready display tables [Paquete de R].
Kaplan, R. S., & Norton, D. P. (1996). The balanced scorecard: Translating strategy into action. Harvard Business School Press.
Reichheld, F. F. (2003). The one number you need to grow. Harvard Business Review, 81(12), 46–54.
Sievert, C. (2020). Interactive web-based data visualization with R, plotly, and shiny. Chapman & Hall/CRC.
Wickham, H. (2021). Mastering Shiny: Build interactive apps, reports, and dashboards powered by R. O'Reilly.