Usando módulos en shiny: Guía de inicio rápido

Shiny
R

¿Para qué usar módulos en shiny? ¿Cómo se usan? Esta guía trata de explicar en breve el concepto de módulos en shiny para que pueda empezar a implementarlos en su proyecto.

Published

October 31, 2019

Acompañamos en iniciativas y proyectos de ciencia de datos, ingeniería e infraestructura. Visita nuestra página ixpantia y contáctanos.

Estructura del tutorial

Este tutorial consiste en un paso a paso sobre cómo usar shiny modular, con código que podrá ir corriendo para ejecutar una aplicación sencilla pero útil conceptualmente para comprender el uso de módulos en shiny.

¿Para qué usar los módulos de shiny?

Si ya tienen experiencia usando shiny, se habrán dado cuenta que una aplicación sencilla puede crecer en lineas de código muy rápido. Esto implica un problema: es más fácil perder el hilo del código cuando tenemos muchas lineas frente a nuestros ojos, con la consecuencia de complicar nuestros procesos para depurar y hacer cambios en nuestra aplicación.

Así mismo, sin módulos tendemos a escribir muchas veces el mismo código, fallando al principio de Do not repeat yourself

Para solventar esto, se han creado los módulos en shiny. Una estrategia para crear nuestras aplicaciones de manera en la que tenemos segmentos de nuestra aplicación de shiny.

¿Qué son los modulos de shiny?

De una manera simple son archivos que contienen el código de cada uno de los elementos que constituyen la aplicación completa.

Se trabajan bajo la carpeta modules dentro del proyecto de la apliacación. Cada archivo que tenemos dentro de modules tiene una pieza de UI y otra pieza del SERVER, de la misma manera que una aplicación de shiny tendría su estructura.

La estructura del proyecto luciría como ésta:

Componentes de la aplicación modulada:

Tenemos que iniciar con tres archivos principales:

  • global.R
  • server.R
  • ui.R

Estos tendrán una estructura general que si ya hemos construido aplicaciones en shiny nos será fácil de reconocer, a excepción del server.R

server.R

Podemos pensar en el archivo server como la parte funcional de la aplicación. Por ende, en el caso de la aplicación modular variará su contenido y será el encargado de hacer las conexiones con los modulos correspondientes.

El código de un archivo server en una aplicación de shiny modular luce similar al siguiente:

server <- function(input, output, session) {
  callModule(module = deslizador,
             id = "deslizador_segundo")
}

En este código lo que tenemos es un llamado al modulo deslizador que debe de encontrarse en nuestra carpeta de módulos y además un id que nos dará una identidad única para este módulo dentro de nuestra aplicación. Este id es una de las partes fundamentales de una aplicación de shiny modularizada ya que nos permite reutilizar módulos que respondan a distintas acciones dentro de la aplicación.

Es decir, podríamos aprovechar el deslizador construido en un módulo en dos partes de la aplicación. Llamaríamos en este server al mismo módulo (deslizador), pero le daríamos una identidad diferente al segundo (en lugar de deslizador_segundo le llamaremos deslizador_tercero). Con esto tendríamos dos deslizadores en nuestra aplicación que responden a diferentes interacciones pero se construyeron con el mismo código contenido en un único módulo. (Haremos un ejemplo durante el desarrollo de la aplicación)

global.R

El archivo global.R es el primer archivo en leerse y ejecutarse cuando la aplicación se shiny inicia. En este podemos colocar la lectura de datos u objetos que se vayan a utilizar a través de toda la aplicación. Una parte importante será el realizar un llamado a los módulos que construyamos para nuestra aplicación. La estructura de un archivo global se verá similar a esta:

# Paquetes ----------------------------------------------------------------
library(shiny)
library(shinydashboard)

# Cargar modulos ----------------------------------------------------------
source("modules/nube.R")
source("modules/deslizador.R")

Tenemos en este archivo los paquetes de los que echaremos mano en nuestra aplicación y seguidamente usamos source() para indicar que hay que cargar los modulos nube.R y deslizador.R que están bajo la carpeta modules de nuestro proyecto.

ui.R

El archivo ui lo podemos conocer como el que se encarga de la chapa y pintura. Este no cambia mucho en cuanto al uso que hacemos en cualquier aplicación de shiny en donde acomodamos los elementos de cara al usuario.

Sí tenemos que tomar en cuenta que con respecto a los módulos tenemos que usar una nomenclatura para hacer la relación entre nuestros componentes. El ui hace relación con el server y para esto tenemos que usar en los nombres el UI

El código dentro del archivo del ui.R puede tener una estructura similar al siguiente:

body <- dashboardBody(
  tabItems(
    tabItem(
      tabName = "nube",
      fluidRow(
          nubeUI("nube_palabras")
      ),
      
      fluidRow(
        box(
          deslizadorUI("deslizador_segundo")
        )
      )
    )
  )
)

Lo que tenemos es código que forma parte de la construcción de una aplicación de shinydashboard y lo único que varía en el caso de ser modularizado es que usamos un componente con el nombre deslizadorUI() (Más adelante veremos porqué esto)

módulo

En cuanto al módulo vamos a tener dentro de este dos segmentos: el componente ui y el componente server. Recordemos que un módulo es independiente de la aplicación y por ende tiene su propia chapa y pintura (ui) y su propia funcionalidad (server)

Notar que en el código usamos de nomenclatura el UI y un elemento ns. Durante la construcción del código de la aplicación ahondaremos en el tema sobre estos elementos.

## Segmento del UI
nubeUI <- function(id) {
  ns <- NS(id)
  plotOutput(ns("grafico"))
}

## Segmento del server

nube <- function(input, output, session, frecuencia, maximo) {
  
  output$grafico <- renderPlot({
         wordcloud(words = datos_nube$word, freq = datos_nube$freq,
                   min.freq = frecuencia,
                   max.words = maximo, 
                   random.order = FALSE, rot.per = 0.35,
                   colors = brewer.pal(8, "Dark2"))
      })
}

Hasta aquí teníamos como objetivo queremos entener qué componentes forman parte de una aplicación shiny modularizada y entender de manera esquemática su estructura interna. Estamos listos para ahondar en el proceso de construcción de la aplicación

Construyendo la aplicación modulada:

Ya que tenemos una idea conceptual de cada uno de los componentes que van a formar parte de nuestra aplicación, vamos a iniciar con la construcción.

Para este ejemplo vamos a construir un dashboard que tendrá una nube de palabras la cual podremos manipular a través de un deslizador.

¿Cuál es el primer paso?

Abrir un proyecto en RStudio que sea una aplicación shiny. Esto nos dará un directorio específico en nuestro computador donde podremos guardar todos los elementos necesarios como datos, imágenes y archivos. Para los datos vamos a crear una carpeta con el nombre datos, para las imágenes una carpeta con el nombre imagenes y una carpeta con el nombre modules para lo que serán nuestros módulos de la aplicación. Recordar que el orden en nuestro flujo de trabajo es muy importante para ser eficiente y para comunicar.

Como segunda recomendación: ¡No iniciar la construcción de una aplicación si no tenemos un boceto previo! Si ya lo tenemos podemos empezar a escribir nuestro archivo global.R

Durante el desarrollo es muy probable que estemos modificando cada uno de los archivos, por ende la primera edición de un archivo no será la final. Nuestro archivo global para esta aplicación tendrá todos los paquetes necesarios para construir la nube de palabras, el llamado a los módulos y la ingesta de los datos para construir la estructura necesaria para formar la nube de palabras.

El código es el siguiente:

# Archivo global.R

# Paquetes ----------------------------------------------------------------
library(tm)
library(SnowballC)
library(wordcloud)
library(RColorBrewer)
library(shiny)
library(shinydashboard)

# Cargar modulos ----------------------------------------------------------
source("modules/nube.R")
source("modules/deslizador.R")

# Leer archivo de datos ---------------------------------------------------
text <- readLines("words")
docs <- Corpus(VectorSource(text))
docs <- tm_map(docs, content_transformer(tolower))
docs <- tm_map(docs, removePunctuation)
dtm <- TermDocumentMatrix(docs)
matriz <- as.matrix(dtm)
matriz_ordenada <- sort(rowSums(matriz), decreasing = TRUE)
datos_nube <- data.frame(word = names(matriz_ordenada), freq = matriz_ordenada)

Si no se comprende el código para formar la nube de palabras, no hay que preocuparse. A este punto lo que necesitamos comprender es que el archivo global.R contiene el código que debe de leerse y correrse de primero al iniciar la aplicación. En este caso necesitamos esos paquetes, cargar los módulos y leer los archivos de los datos.

En este caso el código ejemplo del archivo es código final. Los módulos ya los tenemos construidos pero es muy probable que al iniciar la escritura de nuestra aplicación no tengamos idea del nombre de nuestros módulos. No importa, podemos volver cuando los tengamos listos para dejarlos dentro del llamado inicial de la aplicación, lo cual sí es esencial para el funcionamiento de esta.

Los módulos los llamamos con la función source() que lleva como argumento el camino al modulo. En este ejemplo tenemos dos módulos bajo la carpeta llamada modules y cuyos nombres de archivo son nube.R y deslizador.R

Segundo paso: el UI

No hay forma establecida de flujo de trabajo para una aplicación en shiny, pero en mi caso, me gusta iniciar con el UI ya que lo veo como dar la estructura inicial o andamio donde iremos colocando en el espacio cada uno de los elementos de nuestra aplicación: botones, gráficos, deslizadores, cuadros etc.

El código del UI de nuestra aplicación se verá como el siguiente:

# ui

# Encabezado --------------------------------------------------------------

header <- dashboardHeader(
  title = "Wordcloud"
)

# Sidebar -----------------------------------------------------------------

sidebar <- dashboardSidebar(
  sidebarMenu(
    menuItem("Nube de palabras", tabName = "nube", icon = icon("dashboard")),
    menuItem("Texto", tabName = "texto", icon = icon("align-left"))
  )
)

# Cuerpo ------------------------------------------------------------------
body <- dashboardBody(
  tabItems(
    tabItem(
      tabName = "nube",
      fluidRow(
        nubeUI("nube_palabras")
      ),
      
      fluidRow(
        deslizadorUI("deslizador_nuevo")
      )
    )
  )
)

## App completo ----------------------------------------------------------------
dashboardPage(
  skin = "black",
  header,
  sidebar,
  body
)

Tenemos un encabezado que nos define simplemente el título que llevará la aplicación. En este caso lo ponemos como objeto llamado header.

Luego continuamos con la construcción de lo que es el sidebar. Esta es la columna que aparece a la izquierda del dashboard y contiene los botones de los tabs a los cuales les toca un panel respectivamente. Así tenemos diferentes “páginas” para desplegar nuestro contenido.

Notar que esto debe de coordinarse con el contenido a desplegar. Es decir, si yo quiero que el tab que se llama “Nube de palabras” muestre la nube de palabras, lo debo de conectar con el contenido correspondiente. Por ende en la aplicación a este tab lo vamos a conocer como “nube” y si miramos en el cuerpo, notaremos que hacemos referencia al tab “nube”.

Seguimos con el cuerpo de la aplicación. Esto en el UI significa que vamos a trabajar en la estructura que contendrá los elementos de nuestra aplicación. Aquí estamos trabajando con un dashboard por lo que estamos creando tabs o secciones que tendrán paneles en los que desplegamos el contenido.

Notar que bajo el tabName = “nube” tenemos un nubeUI y más abajo un desplizadorUI. Estos serán los que definen el enlace o conexión a los elementos hacia el archivo server. El identificador para estos elementos son nube_palabras y deslizador_nuevo respectivamente. Esta parte es esencial para comprender cómo comunicamos los archivos entre sí.

Recordemos que la aplicación shiny funciona como un todo. Par nosotros poder “armarla” lo hacemos con diferentes archivos, pero estos archivos deben de comunicarse entre sí.

Por úlitmo, en el UI tenemos un segmento que hemos titulado “app completo”. Lo que hace este segmento es unir los objetos construidos anteriormente (encabezado, sidebar y cuerpo) en lo que podríamos llamar la página completa del dashboard.

Tercer paso: el server

El server usualmente lo tendríamos para colocar todo el código que construye nuestros elementos como gráficos, tablas y otros analisis para resumir nuestros datos.

Cuando usamos shiny modular el server lo usaremos como un “intercomunicador”, el cual llamará al modulo. Así mismo nos será útil para cuando queremos trabajar con reactividad que necesita comunicación entre módulos.

En el caso de nuestra aplicación, el código del server se mira así:

# server

server <- function(input, output, session) {
  
  deslizadores <- callModule(deslizador, "deslizador_nuevo")
  
  observeEvent(
    (deslizadores$frec_input() | deslizadores$max_input()), {
      
      callModule(module = nube,
                 id = "nube_palabras",
                 frecuencia = deslizadores$frec_input(),
                 maximo = deslizadores$max_input())  
    }

  )
}

¿Qué es lo que vemos?

Tenemos una función llamada server que contiene los elementos básicos para nuestra aplicación de shiny como input, output y session (Son estándar para cualquiera aplicación). Luego entre corchetes tenemos un objeto llamado deslizadores que contiene el llamado a un módulo.

callModule(module = deslizador, id = "deslizador_nuevo")

Es importante notar acá que tenemos el nombre del módulo que se llama deslizador y corresponde al nombre que vemos en el environment al hacer el source del módulo. El nombre queda tal cuál cómo definimos el nombre de la función del segmento del server en el módulo.

El segundo argumento que usamos en la función callModule() es el id. Este será el elemento que nos hace conexión con el UI general de la aplicación. Este mismo id que tenemos en el server es el que colocaremos en el UI. Para recordar el código del UI, podemos mirar que tenemos en el cuerpo el mismo id

# Cuerpo ------------------------------------------------------------------
body <- dashboardBody(
  tabItems(
    tabItem(
      tabName = "nube",
      fluidRow(
        nubeUI("nube_palabras")
      ),
      
      fluidRow(
        deslizadorUI("deslizador_nuevo")
      )
    )
  )
)

Este id es valioso, ya que es el que nos permitirá reutilizar módulos en una aplicación con tan solo hacer un mismo llamado a un módulo, pero dándole un id distinto. Más adelante revisaremos un ejemplo con este caso.

Continuando con el código del server, tenemos que notar que ahora sigue un observeEvent(). Esta función de shiny nos es muy útil para cuando necesitamos que la aplicación reaccione a continuas interacciones con el usuario. En nuestro caso buscamos que si el usuario cambia el valor en el selector, la nube de palabras cambie cuantas veces el usuario haga cambios. Esa funcionalidad la estamos dando acá con el observeEvent()

Dentro del observeEvent() tendremos un condicional en el que indicamos que si cualquiera de los dos valores que tenemos en el deslizador cambia, necesitamos volver a crear la nube de palabras.

(deslizadores$frec_input() | deslizadores$max_input()),

Recordemos que | significa “o”. Si el valor de frec_input() o el valor de max_input() cambia (ambos vienen del objeto deslizadores, que a su vez vienen del llamado al módulo “deslizador_nuevo”) pasamos a la siguiente instrucción que es rehacer la nube de palabras con los nuevos valores.

callModule(module = nube,
                 id = "nube_palabras",
                 frecuencia = deslizadores$frec_input(),
                 maximo = deslizadores$max_input())  

Si comparamos este llamado a un módulo con el anterior, notamos que tenemos dos argumentos de más: frecuencia y maximo. Estos son dos argumentos que hemos creado para pasar los valores que vienen del módulo deslizadores. ( Sí, cuando llamamos al módulo tenemos la posibilidad de crear argumentos (nosotros le ponemos los nombres que creamos más convenientes) y estos argumentos serán pasados a la función del módulo. Es en este punto donde creamos la comunicación entre módulos y así habilitamos la posibilidad de pasar valores entre estos.

Recordemos que nosotros tenemos un módulo con un deslizador que entrega los valores que selecciona el usuario y tenemos otro módulo que consume esos valores para construir la nube de palabras de acuerdo a esos parámetros.

En resumen, el server general tenemos que:

  1. Llama módulo deslizador dentro de objeto “deslizadores”
  2. Usa observeEvent para registrar cambios del módulo deslizadores
  3. Para valores del módulo deslizadores al módulo nube.

Cuarto paso: módulo deslizador

Como mencionamos al inicio, un módulo tiene su componente ui y su componente server. El módulo para construir los deslizadores se ve así:

# Modulo deslizador -------------------------------------------------

## Segmento del UI
deslizadorUI <- function(id) {
  ns <- NS(id)
  
  tagList(
    sliderInput(ns("frec_input"),
                "Frecuencia:",
                min = 1,  max = 50, value = 15),
    
    sliderInput(ns("max_input"),
                "Máximo de palabras:",
                min = 1,  max = 300,  value = 100)
  )
}

## Segmento del server
deslizador <- function(input, output, session) {
  return(
    list(
      frec_input = reactive({ input$frec_input }),
      max_input = reactive({ input$max_input })
    )
  )
}

Es necesario prestar atención a que el nombre de la función del ui tiene que llevar las letras UI. Tal como vemos nuestra función ui tiene por nombre deslizadorUI() y nuestra función server deslizador()

UI del módulo

En este segmento vamos a colocar lo que consideramos la “chapa y pintura” del módulo. Aquí necesitamos dos widgets de shiny que son los sliderInput() con los que el usuario final interactuará y de los cuales tomaremos los valores para construir la nube de palabras.

Ahora bien, tenemos dos deslizadores y para que podamos obtener los valores de ambos es necesario hacer uso de la función tagList(). Esta función es especial y nos ayuda a anexar varios valores.

Notar además de que la función del UI tiene un id en donde creamos la definición de un namespace. Esta es la solución que se ha dado para trabajar con módulos. Recordemos que hablamos de que un módulo es un segmento auto-contenido, es decir, trae sus “propias piezas” para desarrollarse (aún así un módulo no puede desplegarse sin el contexto de la aplicación de shiny general). Que sea autocontenido nos permite usar el módulo en diferentes partes de nuestra aplicación sin tener que re-escribir todo el código.

Cuando pensamos en esto, podemos pensar ya en un problema. Si usamos el módulo dos veces en una misma aplicación, ¿Cómo sabemos cuando el uso en un punto difiere del uso en un segundo punto?. Con un identificador solucionamos esto. Ese identificador son los namespace.

Ahora bien, ya definimos la función del UI con un namespace a través del id. Lo que necesitamos después de esta acción es que cada input o output de cualquier tipo en la función UI debe de estar “envuelta” en un ns().

La ventaja es que ahora nuestros objetos frec_input y max_input deben ser únicas en el módulo y no através de toda la aplicación que estamos construyendo.

Dentro de lo que tenemos como sliderInput() son los parámetros que usamos normalmente para construir este tipo de widget.

SERVER del módulo

El segundo componente de nuestro módulo es la función server() donde escribirmos los argumentos que son obligatorios como input, output, session y dentro del cuerpo creamos los objetos que necesitamos. En este caso tenemos los retornos de los valores que selecciona el usuario, pero con una función más: la especificación de que estamos en un entorno reactivo.

Quinto paso: módulo nube

Nuestro segundo módulo luce de la siguiente manera:

# Modulo nube de palabras -------------------------------------------------

## Segmento del UI
nubeUI <- function(id) {
  ns <- NS(id)
  
  plotOutput(ns("grafico"))
  
}

## Segmento del server
nube <- function(input, output, session, frecuencia, maximo) {
  
  output$grafico <- renderPlot({
         wordcloud(words = datos_nube$word,
                   freq = datos_nube$freq,
                   min.freq = frecuencia,
                   max.words = maximo, 
                   random.order = FALSE,
                   rot.per = 0.35,
                   colors = brewer.pal(8, "Dark2"))
      })
}

UI del módulo

Siguiendo el patrón de construcción, le hemos dado por nombre nubeUI(), y en la función definimos el namespace con id. Dentro tenemos la definición de este **id* con la asignación al objeto ns <- NS(id).

En este módulo vamos a crear un gráfico que consiste en una nube de palabras, por ende necesitamos un plotOutput() en nuestro UI. Recordar que al objeto hay que darle su propio namespace, razón por la cual nuestro objeto gráfico lo tenemos dentro de ns().

SERVER del módulo

El nombre de la función de nuestro server es el mismo que el del UI solo que no usamos las letras UI, solamente dejamos nube. En los argumentos de la función tenemos los que dejamos por defecto (input, output, session) pero agregamos un par más: frecuencia y máximo. Recuerden que estos los colocamos ya que los definimos en el server general al hacer el llamado a este módulo.

Recordemos el código del llamado a este módulo y las variables que definimos:

callModule(module = nube,
                 id = "nube_palabras",
                 frecuencia = deslizadores$frec_input(),
                 maximo = deslizadores$max_input())  

Ese mismo frecuencia y maximo son los elementos que usamos para comunicar entre módulos, para pasar valores a través de argumentos en la función. Esta es la conexión para que los valores que vienen del objeto deslizadores como frec_input() y max_input() y los pasamos como argumentos en la función del server.

¿Dónde usamos los valores de los argumentos frecuencia y máximo?

Ahora en la construcción del grafico tenemos el renderPlot({ }) y dentro de este el código para crear la nube de palabras. Esta nube de palabras tiene varios parámetros para su construcción que podemos revisar en la documentación para mayor detalle.

wordcloud(words = datos_nube$word,
                   freq = datos_nube$freq,
                   min.freq = frecuencia,
                   max.words = maximo, 
                   random.order = FALSE,
                   rot.per = 0.35,
                   colors = brewer.pal(8, "Dark2"))

Acá usamos primeramente los datos ya trabajados en su estructura que vienen del global.R como datos_nube$word. Ahora en los casos de min.freq = y max.words = es donde pasamos los valores que vienen de los sliders construidos en el modulo deslizadores. Lo único que hacemos es escribir el mismo nombre del argumento de la función para hacer la conexión. Tal como haríamos en cualquier construcción de una función.

¡Terminamos! ¿Ahora qué hacemos?

Y ¡listo! Ya tenemos todos los componentes de nuestra aplicación shiny modular con interactividad. En este tutorial hemos aprendido sobre

  • Qué resuelven los módulos en shiny
  • Contruir un server y un UI de una aplicación modular
  • Construir los módulos de una aplicación de shiny
  • Comunicar módulos entre sí.
  • Interactividad dentro de los módulos

Recordá que todo el código de la aplicación se encuentra en github


Este blog lo mantiene el equipo de ixpantia y la comunidad de gente interesada en datos de la cual estamos contentos de formar parte ¿Tienes una idea para publicar algo aquí? ¡Escríbenos! Estamos siempre interesados en material e ideas nuevas. © 2019-2022 ixpantia