16  shiny - desarrollo de aplicaciones web interactivas

16.1 Resumen

Shiny es un paquete de R que facilita el desarrollo de aplicaciones web interactivas. Las aplicaciones shiny se componen de una interfaz de usuario y de un servidor. En la interfaz de usuario, este puede realizar operaciones como filtros, búsquedas y ordenamientos de datos, entre otras. El servidor se encarga de procesar los datos de acuerdo con los parámetros especificados y de retornar los resultados a la interfaz de usuario.

Además de los bloques de código en R para la interfaz de usuario y el servidor, shiny proporciona bloques para compartir código y datos. También para personalizar la interfaz de usuario.

Las funciones reactivas de shiny se ejecutan cada vez que el usuario cambia los controles de la interfaz, lo que permite a las aplicaciones shiny responder dinámicamente a las entradas del usuario.

16.2 Trabajo previo

16.2.1 Lecturas

Quarto - Dashboards with Shiny for R. (s.f.). Quarto. Recuperado el 1 de marzo de 2024, de https://quarto.org/docs/dashboards/interactivity/shiny-r.html

16.2.2 Instalación y carga de paquetes

# Instalación de shiny
install.packages("shiny")

# Instalación de rsconnect
install.packages("rsconnect")
# Carga de paquetes
library(shiny)
library(rsconnect)

16.3 Introducción

En una aplicación interactiva, el usuario puede configurar las salidas, usualmente mediante una interfaz que le permite realizar operaciones como filtros, búsquedas y ordenamientos, entre otras. Shiny es un paquete de R que facilita el desarrollo de este tipo de aplicaciones.

Las aplicaciones shiny requieren un servidor, el cual es un proceso que puede alojarse en cualquier computador habilitado para ejecutar código en R (o Python) como, por ejemplo, la estación de trabajo del programador (esta opción se usa principalmente para efectos de desarrollo y pruebas), un servidor ubicado en la red de una organización o un servidor en la nube (ej. shinyapps.io, Posit Connect, Posit Cloud).

Para ejemplos de aplicaciones shiny, puede visitar la siguiente galería.

16.4 Arquitectura

Una aplicación shiny tiene dos componentes principales:

  1. Interfaz de usuario: despliega controles de entrada y salida (widgets), los cuales eventualente convierte a Lenguaje de Marcado de Hipertexto (HTML).
    • Widgets de entrada: campos de texto, listas de selección, botones de radio, etc.
    • Widgets de salida: tablas, gráficos, mapas, etc. Muchos de estos controles están incluídos en los paquetes que generan los diferentes tipos de salidas (ej. DT, plotly, leaflet).
  2. Servidor: es un proceso que recibe las entradas y realiza el procesamiento necesario para generar las salidas y retornar los resultados a la interfaz de usuario.

Para más información sobre la arquitectura de aplicaciones shiny, se recomienda leer The Anatomy of a Shiny Application.

16.5 Ejemplo de aplicación básica

El siguiente documento Quarto contiene una aplicación shiny con un tablero de control interactivo para el conjunto de datos diamonds. Esta aplicación se incluye como ejemplo en:

https://quarto.org/docs/dashboards/interactivity/shiny-r.html.

Puede acceder a la aplicación ejecutable en:

https://mfvargas.shinyapps.io/2024-i-tablero-interactivo-diamantes/.

---
title: "Diamonds Explorer"
author: "Barkamian Analytics"
format: dashboard
server: shiny
---

```{r}
#| context: setup
library(ggplot2)
dataset <- diamonds
```

# {.sidebar}

```{r}
sliderInput('sampleSize', 'Sample Size', 
            min=1, max=nrow(dataset),
            value=min(1000, nrow(dataset)), 
            step=500, round=0)
br()
checkboxInput('jitter', 'Jitter')
checkboxInput('smooth', 'Smooth')
```

```{r}
selectInput('x', 'X', names(dataset)) 
selectInput('y', 'Y', names(dataset), names(dataset)[[2]])
selectInput('color', 'Color', c('None', names(dataset)))
```

```{r}
selectInput('facet_row', 'Facet Row',
  c(None='.', names(diamonds[sapply(diamonds, is.factor)])))
selectInput('facet_col', 'Facet Column',
  c(None='.', names(diamonds[sapply(diamonds, is.factor)])))
```

# Plot

```{r}
plotOutput('plot')
```

# Data

```{r}
tableOutput('data')
```

```{r}
#| context: server

dataset <- reactive({
  diamonds[sample(nrow(diamonds), input$sampleSize),]
})
 
output$plot <- renderPlot({
  
  p <- ggplot(
    dataset(), 
    aes_string(x=input$x, y=input$y)) + geom_point()
  
  if (input$color != 'None')
    p <- p + aes_string(color=input$color)
  
  facets <- paste(input$facet_row, '~', input$facet_col)
  if (facets != '. ~ .')
    p <- p + facet_grid(facets)
  
  if (input$jitter)
    p <- p + geom_jitter()
  if (input$smooth)
    p <- p + geom_smooth()
  
  p
  
})

output$data <- renderTable({
  dataset()
})
```

Es de vital importancia comprender que el último bloque de código (con la opción #| context: server) se ejecuta en una sesión separada de R. Esto implica que no es posible, en principio, acceder desde ese bloque variables definidas en otros bloques, ni viceversa. Sin embargo, existen varias estrategias para compartir código, como las que se detallan en Sharing Code y también en las secciones siguientes de este documento.

Una forma en la que la interfaz de usuario y el servidor pueden comunicarse, es a través de las listas input y output.

  • input contiene la lista de widgets de entrada (listas de selección, campos de entrada de texto, botones de radio, etc.). Cada uno de estos widgets tiene un inputId único. En el ejemplo, “sampleSize” es el inputId del widget tipo sliderInput. Se referencia como input$sampleSize en el bloque del servidor.
  • output es una lista de componentes que se crean o modifican en el servidor (tablas, gráficos, mapas, etc). y que luego se envían a la interfaz de usuario para su visualización. Para crear o modificar un elemento de output, se utiliza una función render*, y para mostrarlo en la interfaz de usuario, se utiliza una función *Output. En el ejemplo, output$plot es un gráfico que se crea con la función renderPlot() en función de los valores de los widgets de entrada.

Nótese que hay diferencias importantes entre este documento y otros documentos Quarto:

  1. La opción server: shiny: en la sección YAML, la cual le indica a Quarto que debe iniciar un servidor Shiny.

  2. La opción context: server: en el último bloque de código, la cual indica que ese bloque debe ejecutarse en el servidor.

Existen otros posibles valores de context para los bloques de código en aplicaciones shiny, los cuales permiten compartir código y datos.

16.6 Bloques para compartir código y datos

Como puede apreciarse en el ejemplo anterior, las aplicaciones shiny pueden contener bloques de código que se ejecutan en tiempo de despliegue (rendering) de la interfaz de usuario y también bloques que se ejecutan en el servidor en respuesta a las acciones del usuario y a los cambios en los valores de entrada. Estos tipos de bloques se identifican respectivamente mediante las opciones context: render (valor por defecto de context) y context: server.

Existen también otros tipos de bloques, los cuales permiten compartir código y datos.

16.6.1 context: setup

El código de este tipo de bloques se ejecuta tanto en el contexto de la interfaz de usuario como del servidor. Puede usarse para operaciones que se ejecutan al inicio de una aplicación, como la carga de paquetes.

```{r}
#| label: inicio
#| context: setup
#| message: false

# Carga de paquetes
library(tidyverse)
library(DT)
library(sf)
```

16.6.2 context: data

Se utiliza para cargar datos que deben compartirse entre todos los bloques de código. Por ejemplo:

```{r}
#| label: lectura-datos
#| context: data

# Lectura de archivo CSV
felidos <-
  st_read(
    dsn = "felidos.csv",
    options = c(
      "X_POSSIBLE_NAMES=decimalLongitude",
      "Y_POSSIBLE_NAMES=decimalLatitude"
    ),
    quiet = TRUE
  )
```

16.7 Configuración de la interfaz de usuario

16.7.1 Barras laterales

Las barras laterales o sidebars se utilizan generalmente para colocar widgets de entrada. Para incluir una barra lateral, se agrega la clase .sidebar a un encabezado de nivel 2. Por ejemplo:

# {.sidebar}

```{r}
sliderInput('sampleSize', 'Sample Size', 
            min=1, max=nrow(dataset),
            value=min(1000, nrow(dataset)), 
            step=500, round=0)
br()
checkboxInput('jitter', 'Jitter')
checkboxInput('smooth', 'Smooth')
```

```{r}
selectInput('x', 'X', names(dataset)) 
selectInput('y', 'Y', names(dataset), names(dataset)[[2]])
selectInput('color', 'Color', c('None', names(dataset)))
```

```{r}
selectInput('facet_row', 'Facet Row',
  c(None='.', names(diamonds[sapply(diamonds, is.factor)])))
selectInput('facet_col', 'Facet Column',
  c(None='.', names(diamonds[sapply(diamonds, is.factor)])))
```

Puede consultar sobre más recursos para configurar el tablero en https://quarto.org/docs/dashboards/inputs.html.

16.8 Funciones reactivas

Son funciones especiales que se ejecutan cada vez que cambia uno de los datos que utiliza como entrada, como un widget de la interfaz de usuario. En el siguiente bloque de código se define una función reactiva que utiliza el valor especificado por el usuario en una lista de selección para filtrar un conjunto de datos.

```{r}
# Función reactiva para filtrar los registros de presencia de félidos
# de acuerdo con el valor de una lista de selección
filtrar_felidos <- reactive({
  # Valor inicial del objeto que va a retornarse
  felidos_filtrados <- felidos
  
  if (input$especie != "Todas") {
    felidos_filtrados <-
      felidos_filtrados |>
      filter(species == input$especie)
  }

  return(felidos_filtrados)
})  
```

Las funciones reactivas permiten a las aplicaciones shiny responder dinámicamente a las entradas del usuario.

16.9 Ejemplo: Desarrollo de una aplicación interactiva para el Atlas de Desarrollo Humano Cantonal 2024

A manera de ejemplo, se presenta el desarrollo, paso por paso, de una aplicación interactiva para el Atlas de Desarrollo Humano Cantonal 2024.

En el capítulo anterior de este curso, se desarrolló un tablero de control sobre el mismo conjunto de datos. La aplicación interactiva le permite al usuario visualizar y explorar mayores cantidades de datos que en el tablero de control y también especificar filtros.

Esta aplicación interactiva cuenta con controles (widgets) para filtrar los datos por:

  • Provincia.
  • Año.

Los datos se presentan en tres salidas:

  • Una tabla DT.
  • Un gráfico plotly.
  • Un mapa tmap.

Para fines didácticos, el desarrollo se dividió en las siguientes secciones:

  • Creación del proyecto y del archivo principal de la aplicación.
  • Creación de los bloques iniciales de código.
  • Creación de la barra lateral con widgets para filtros.
  • Creación de los controles de salida.
  • Creación de una función reactiva para filtrar los datos.
  • Creación de funciones para generar las salidas.

Seguidamente, se enumeran los pasos detallados de cada una de estas secciones.

16.9.1 Creación del proyecto y del archivo principal de la aplicación

1. Cree un proyecto en RStudio con un nombre apropiado de su elección (ej. aplicacion-interactiva-atlas-desarrollo-humano).

2. Descargue y copie en el directorio de su proyecto los siguientes archivos de datos:

3. En el nuevo proyecto, cree un documento Quarto con un nombre de su elección (ej. aplicacion-interactiva-atlas-desarrollo-humano.qmd). Puede borrar el contenido que RStudio genera automáticamente en el documento.

16.9.2 Creación de los bloques iniciales de código

4. Agregue un encabezado en el nuevo documento. Asegúrese de incluir las opciones format: dashboard y server: shiny. Por ejemplo:

---
title: "Atlas de Desarrollo Humano Cantonal"
format: dashboard
server: shiny
---

5. Agregue un bloque de código en R para cargar las bibliotecas. Asegúrese de incluir la opción context: setup.

```{r}
#| label: carga-bibliotecas
#| context: setup
#| warning: false
#| message: false

# Carga de bibliotecas
library(tidyverse)
library(readxl)
library(DT)
library(plotly)
library(sf)
library(terra)
library(tmap)
library(shiny)

# Especificar modo interactivo para tmap
tmap_mode("view")
```

Todas las bibliotecas deben haber sido instaladas previamente con la función install.packages(). En este bloque se incluye también el llamado a la función tmap_mode("view") para especificar el modo interactivo de tmap.

6. Agregue un bloque de código en R para cargar los datos y realizar cualquier transformación necesaria (filtrados, ordenamientos, uniones, etc.). Asegúrese de incluir la opción context: data.

```{r}
#| label: carga-datos
#| context: data

# CANTONES

# Cargar datos de cantones
cantones <- st_read("cantones-simplificados.gpkg", quiet = TRUE)


# ÍNDICE DE DESARROLLO HUMANO (IDH)

# Cargar datos del IDH
idh <- read_excel("indice_de_desarrollo_humano.xlsx", sheet = "IDH")

# Crear la columna CÓDIGO_CANTÓN (el mismo nombre que tiene en cantones)
# y renombrar "Cantón" como "CANTÓN", para que quede en mayúscula como las otras
idh <- 
  idh |>
  mutate(
    `CÓDIGO_CANTÓN` = substr(Cantón, 1, 3) |> # toma los 3 primeros caracteres
                      as.integer()            # los convierte a entero
  )


# UNIÓN DE CANTONES E ÍNDICES

# Crear unión de cantones e IDH
datos_anchos <-
  cantones |>
  left_join(
    idh,
    by = "CÓDIGO_CANTÓN" # llave para realizar la unión
  )

# "Alargar" el dataframe al poner todas las columnas de años en una sola 
datos <- datos_anchos |>
  pivot_longer(
    cols      = `2010`:`2022`,   # columnas a “alargar”
    names_to  = "ANIO",          # nueva columna con los nombres (años)
    values_to = "IDH"            # nueva columna con los valores (IDH)
  ) |>
  mutate(ANIO = as.integer(ANIO)) # opcional: convertir a número
```

16.9.3 Creación de la barra lateral con widgets para filtros

7. Agregue un encabezado Markdown para crear una barra lateral para los widgets de filtros de datos.

# {.sidebar}

8. Agregue un bloque de código en R para agregar una lista de selección (selectInput()) de provincia.

```{r}
#| label: selectinput-provincia

# Lista ordenada de provincias con un elemento adicional = "Todas"
lista_provincias <- unique(datos$PROVINCIA)
lista_provincias <- sort(lista_provincias)
lista_provincias <- c("Todas", lista_provincias)

# Widget de lista de selección ("selectInput") de provincias
selectInput(
  inputId = "provincia",      # identificador del widget
  label = "Provincia",        # etiqueta de la lista (la que ve el usuario)
  choices = lista_provincias, # lista de opciones para seleccionar de la lista
  selected = "Todas"          # opción seleccionada por defecto
)
```

Note el argumento inputId = "provincia" de la función selectInput(). Este es el identificador de este widget y luego se utilizará en las funciones de filtrado.

9. Agregue un bloque de código en R para agregar un widget deslizador de selección (sliderInput()) de año.

```{r}
#| label: slider-anio

# Año mínimo y año máximo
anio_minimo <- min(datos$ANIO)
anio_maximo <- max(datos$ANIO)

# Widget deslizador de selección ("sliderInput") de año
sliderInput(
  inputId = "anio",
  label = "Año",
  min = anio_minimo,   # año mínimo permitido
  max = anio_maximo,   # año máximo permitido
  value = round((anio_minimo + anio_maximo) / 2), # año inicial seleccionado
  step    = 1,    # solo incrementos de un año (evita decimales)
  round   = TRUE, # redondea al entero más cercano
  sep     = ""    # sin separador de miles
)
```

Note el argumento inputId = "anio" de la función sliderInput(). Este es el identificador de este widget y luego se utilizará en las funciones de filtrado.

16.9.4 Creación de los controles de salida

10. Agregue un encabezado Markdown para crear una página y un bloque de código en R para crear el control de salida de una tabla DT.

# Tabla
```{r}
#| label: salida-tabla
#| title: "Tabla"

# Tabla
dataTableOutput(
    outputId =  "tabla" # identificador del widget
)
```

Note el argumento outputId = "tabla" de la función dataTableOutput(). Este es el identificador de este control.

11. Agregue un encabezado Markdown para crear una página y un bloque de código en R para crear el control de salida de un gráfico plotly.

# Gráfico
```{r}
#| label: salida-grafico
#| title: "Gráfico"

# Gráfico
plotlyOutput(
    outputId =  "grafico" # identificador del widget
)
```

Note el argumento outputId = "grafico" de la función plotlyOutput(). Este es el identificador de este control.

12. Agregue un encabezado Markdown para crear una página y un bloque de código en R para crear el control de salida de un mapa tmap.

# Mapa
```{r}
#| label: salida-mapa
#| title: "Mapa"

# Mapa
tmapOutput(
    outputId =  "mapa" # identificador del widget
)
```

Note el argumento outputId = "mapa" de la función tmapOutput(). Este es el identificador de este control.

16.9.5 Creación de una función reactiva para filtrar los datos

13. Agregue un bloque de código en R para definir una función reactiva que filtre los datos. Asegúrese de incluir la opción context: server.

```{r}
#| label: servidor
#| context: server

# Función reactiva para filtrar los datos
# de acuerdo con los filtros especificados por el usuario
filtrar_datos <- reactive({
  # Para comenzar, se toman los datos sin filtrar
  datos_filtrados <- datos
  
  # Filtro por provincia
  if (input$provincia != "Todas") {
    datos_filtrados <-
      datos_filtrados |>
      filter(PROVINCIA == input$provincia)
  }
  
  # Filtro por año
  datos_filtrados <-
    datos_filtrados |>
    filter(ANIO == input$anio)
  
  return(datos_filtrados)
}) 
```

Note el uso de los identificadores de los widgets de entrada: input$provincia e input$anio.

16.9.6 Creación de funciones para generar las salidas

14. En el mismo bloque de código de la función reactiva, defina una función para generar la tabla.

output$tabla <- renderDataTable({
  # Filtrado del conjunto de datos
  datos <- filtrar_datos()
  
  # Definir la tabla
  tabla <- 
    datos |>
      st_drop_geometry() |>
      dplyr::select(PROVINCIA, CANTÓN, ANIO, IDH) |>
      mutate(IDH = round(IDH, 3)) |>
      arrange(desc(IDH)) |>                 
      datatable(
        rownames = FALSE,
        colnames = c("Provincia", "Cantón", "Año", "IDH"),
        options = list(                 
          pageLength = 8,
          language = list(url = '//cdn.datatables.net/plug-ins/1.10.11/i18n/Spanish.json')
        )    
      )
  
  # Mostrar la tabla
  tabla
})

**15. En el mismo bloque de código de la función reactiva, defina una función para generar el gráfico.**

```{r}
output$grafico <- renderPlotly({
  # Filtrado del conjunto de datos
  datos <- filtrar_datos()
  
  # Definir el gráfico ggplot2
  grafico_ggplot2 <-
    datos |>
    slice_head(n = 15) |>
    ggplot(aes(x = reorder(CANTÓN, -IDH), y = IDH)) +
    geom_col(
      aes(
        text = paste0(
          "Cantón: ", CANTÓN, "\n",
          "IDH: ", round(IDH, 3)
        )
      ),
      fill = "#1f77b4"
    ) +
    xlab("Cantón") +
    ylab("IDH") +
    theme_classic() +
    theme(axis.text.x = element_text(angle = 45, hjust = 1)) # etiquetas a 45°
  
  # Mostrar el gráfico plotly
  ggplotly(grafico_ggplot2, tooltip = "text") |> 
    config(locale = 'es')
})
```

Note como el valor que retorna la función renderPlotly() es asignado a output$grafico, el identificador del control de salida del gráfico.

16. En el mismo bloque de código de la función reactiva, defina una función para generar el mapa.

output$mapa <- renderTmap({
  # Filtrado del conjunto de datos
  datos <- filtrar_datos()
  
  # Definir y configurar el mapa
  mapa <-
    tm_basemap(c("Esri.WorldGrayCanvas", "OpenStreetMap", "Esri.WorldImagery")) +
    tm_shape(datos, name = "Cantones") +
      tm_fill(
        fill = "IDH",
        fill.scale = tm_scale_intervals(
        style  = "quantile",
        values = "Blues"
      ),
      fill.legend = tm_legend(title = "IDH"),
      id = "CANTÓN",
      popup.vars  = c(
        "IDH" = "IDH"
      )
    ) +
    tm_borders(col = "black", lwd = 0.5) +
    tm_scale_bar(position = c("left", "bottom"))
  
  # Mostrar el mapa
  mapa
})

````

Note como el valor que retorna la función renderTmap() es asignado a output$mapa, el identificador del control de salida del mapa.

16.10 Ejercicios

  1. Ejecute en su computadora el código del tablero de control sobre diamantes mostrado anteriormente.

    1. Cree un proyecto en RStudio.
    2. Cree un documento Quarto.
    3. Copie en el nuevo documento el código de la aplicación.
    4. Ejecute el documento con el botón Run Document.
  2. Publique el tablero de control en shinyapps.io (puede consultar Quarto - Running Documents y How to Deploy R Shiny App for Free on Shinyapps.io).

    1. Cree una cuenta en shinyapps.io.
    2. Obtenga su token de autenticación de shinyapps.io en Accounts - Tokens - Show- Show secret - Copy to clipboard.
    3. Ejecute la aplicación en su computadora y publíquela en shinyapps.io con el botón Publish. Elija la opción shinyapps.io e ingrese el token cuando se le solicite. Debe seleccionar todos los archivos requeridos para que su aplicación funciones (archivos .qmd, archivos .html, archivos de datos, etc).
  3. Ejecute en su computadora y luego publique en shinyapps.io la aplicación Iris K-Means Clustering.

  4. Estudie el resto de los ejemplos en Quarto - Shiny - Examples.

  5. Analice el siguiente tablero de control sobre datos de mamíferos:

    1. Agregue un gráfico y un mapa al tablero de control.

16.11 Recursos de interés

Quarto - Running Documents. (s. f.). Recuperado 20 de noviembre de 2022, de https://quarto.org/docs/interactive/shiny/running.html

Shiny. (s. f.). Recuperado 20 de noviembre de 2022, de https://shiny.rstudio.com/

Shiny - Gallery. (s. f.). Recuperado 20 de noviembre de 2022, de https://shiny.rstudio.com/gallery/

The Anatomy of a Shiny Application | R-bloggers. (2021). Recuperado 21 de noviembre de 2022, de https://www.r-bloggers.com/2021/04/the-anatomy-of-a-shiny-application/

1littlecoder. (2020). How to Deploy R Shiny App for Free on Shinyapps.io. https://www.youtube.com/watch?v=2QstfyGX4ZU