¿Qué es Shiny? ¿Para que nos sirve?

Shiny es un paquete de R. Podemos usar install.packages('shiny') y lo tendremos listo para funcionar en nuestra computadora. Pero no es solo un paquete más: tiene funcionalidades muy interesantes y flexibles.

Su objetivo principal es producir aplicaciones interactivas usando R. Por aplicaciones interactivas hacemos referencia a un programa en el cual hay una interacción continua entre el usuario y el servidor, en el sentido de que el usuario puede interactuar con el contenido que se muestra y modificarlo según un conjunto de reglas preestablecidas.

Estructura básica de una aplicación shiny

Shiny se maneja con dos grandes bloques de código: ui y server. A grandes rasgos, en la primera parte incluimos información sobre el diseño y los objetos que queremos que aparezcan en nuestra aplicación. En el ejemplo Hello levemente modificado que la gente de RStudio en su galería de ejemplos, una ui (proveniente de user interface) puede tener el siguiente código:

ui <- fluidPage(
  titlePanel("Hello Shiny!"),
  sidebarLayout(
    sidebarPanel(
      # Definimos a un INPUT
      sliderInput(inputId = "bins",
                  label = "Number of bins:",
                  min = 1,
                  max = 50,
                  value = 30)
      
    ),
    mainPanel(
      # Definimos un OUTPUT
      plotOutput(outputId = "distPlot")
      
    )
  )
)

Vayamos parte por parte. el objeto ui es una fluidPage(), que no significa otra cosa que una página que tiene filas y (como veremos más adelante) columnas. Es lo que nos permite estructurar los elementos en la página. Luego, en titlePanel() podemos agregar el título que queramos que aparezca en la página.

La estructura básica de fluidPage() tiene una barra izquierda y un panel a la derecha. Intuitivamente, la idea es agregar los controles en el lado izquierdo y las salidas en la derecha. en sidebarLayout() nos indica que vamos a definir cosas dentro de la parte izquierda. en sidebarPanel() definimos que queremeos un panel en la izquierda con un sliderInput, que se llama bins. Finalmente, dentro de mainPanel() estamos diciendo que queremos un plotOutput() que se llama distPlot.

¿Qué significa un output y un input? Es muy intuitivo: todo los inputs son instrumentos por los cuales el usuario interactua con nuestra aplicación para modificar los outputs, que son todos los gráficos, tablas o cualquier otro elemento que queremos mostrar, dado un conjunto de parámeteros seteados por el usuario.

Habiendo definido la parte de la user interface, veamos cómo luce la parte del server

# Acá definimos qué relación hay entre los input y los outputs.
# todo el procesamiento está acá
server <- function(input, output) {
#output es una lista que "anota" qué es lo que tiene que mostrar. En este caso
# le decimos que tieene que hacer un histograma
  output$distPlot <- renderPlot({
    # faithful es un dataset que viene precargado en R.
    datos  <- data.frame(faithful$waiting)
    # lo que hace es discretizar a los waiting times entre erupción según la cantidad
    # de bins que el usuario elija. inputs es una lista que "anota" lo que le manda 
    # el usuario
    ggplot(datos) +
      geom_histogram(aes(x=faithful.waiting),fill="#75AADB",bins=input$bins) +
      labs(title="Histograma de intervalos de espera",
           x="Tiempo de espera hasta la próxima erupción (minutos)",
           y="") +
      theme_minimal()
  })
  
}

Server es una function() de input y output porque recibe información sobre la lista de inputs (en este caso, tenemos un sliderInput) y establece cambios en los outputs (en este caso, un plotOutput) ¿cómo lo hace? usando funciones del tipo render, en este caso renderPlot(), y devolviendo ese plot a una lista que se llama output y que tiene el mismo nombre que el objeto que creamos en la ui, perfecto ¿Pero cómo sabe qué envío el usuario? De la manera análoga: existe una lista input que contiene todos los valores que eligió para los elementos que creamos en ui. En este caso, input$bins tiene la cantidad de categorías que eligió el usuario en en el sliderInput que se llama bins.

Ejecutando nuestra primera shiny app

Una vez que tenemos nuestra ui y server ya estamos en condiciones de ejecutar nuestra primera shiny app. Solo nos falta ejecutar la función runApp() del paquete shiny: copienlo en un archivo nuevo, guardenlo como un .R. Seleccionen todo y ejecuten y pongan en funcionamiento su primerea aplicación !

library(shiny)
library(ggplot2)
# User interface
ui <- fluidPage(
  
  titlePanel("Hello Shiny!"),
  sidebarLayout(
    sidebarPanel(
      # Definimos a un INPUT
      sliderInput(inputId = "bins",
                  label = "Number of bins:",
                  min = 1,
                  max = 50,
                  value = 30)
      
    ),
    mainPanel(
      # Definimos un OUTPUT
      plotOutput(outputId = "distPlot")
      
    )
  )
)

# Acá definimos qué relación hay entre los input y los outputs.
# todo el procesamiento está acá
server <- function(input, output) {
#output es una lista que "anota" qué es lo que tiene que mostrar. En este caso
# le decimos que tieene que hacer un histograma
  output$distPlot <- renderPlot({
    # faithful es un dataset que viene precargado en R.
    datos  <- data.frame(faithful$waiting)
    # lo que hace es discretizar a los waiting times entre erupción según la cantidad
    # de bins que el usuario elija. inputs es una lista que "anota" lo que le manda 
    # el usuario
    ggplot(datos) +
      geom_histogram(aes(x=faithful.waiting),fill="#75AADB",bins=input$bins) +
      labs(title="Histograma de intervalos de espera",
           x="Tiempo de espera hasta la próxima erupción (minutos)")
  })
  
}
shinyApp(ui, server)

Agregando más interacciones con el usuario: textbox

Ahora generemos un pequeño cambio en nuestra aplicación: permitámosle que cambien el título del gráfico ¿Cómo podemos hacer esto? Primero, debemos dejar que introduzcan texto, por lo cual debemeos crear un nuevo elemento de input. Pero también necesitamos que se comunique con el el objeto que crea output, es decir nuestro ggplot. Vayamos por partes entonces:

En ui, agreguemos un textInput() luego del sliderInput():

      sliderInput(inputId = "bins",
                  label = "Number of bins:",
                  min = 1,
                  max = 50,
                  value = 30),
textInput(inputId = "titulo",label = "Titulo",placeholder ="Escribí tu título")

El nombre del input es titulo, por lo que lo qu sea que se escriba ahí, shiny lo almacenara en la lista input. Usemosla para ponerlo como título de nuestro gráfico en la parte del server:

  output$distPlot <- renderPlot({
    # faithful es un dataset que viene precargado en R.
    datos  <- data.frame(faithful$waiting)
    # lo que hace es discretizar a los waiting times entre erupción según la cantidad
    # de bins que el usuario elija. inputs es una lista que "anota" lo que le manda 
    # el usuario
    ggplot(datos) +
      geom_histogram(aes(x=faithful.waiting),fill="#75AADB",bins=input$bins) +
      labs(title=input$titulo,
           x="Tiempo de espera hasta la próxima erupción (minutos)")
  })

Creen de nuevo el objeto ui, server y usen runApp() para ver la nueva aplicación

Frenando la “reactividad” en shiny: isolate()

Muchas veces el procesamiento de las aplicaciones es muy pesado y queremos evitar que cambios menores, como cambiar un título de un gráfico, haga que tengan que calcularse nuevamente tablas y generarse los gráficos. Una solución a esto es usar isolate(). Se trata de una función de shiny que evita que cambios en ciertos inputs colocado se propaguen al server. Veamos cómo funciona.

En su parte de server, cambien input$titulo por isolate({ input$titulo }) y ejecuten de nuevo el código.

Ahora escriban algo en el textbox ¿Cambió título de nuestro gráfico? No: isolate desactivó la propagación. ¿Pero qué pasa si movemos el sliderInput, que sí tienen comunicación con output? El gráfico se actualiza, incluso también cambia el título. Como ven, isolate() es una muy buena herramienta para esperar hasta que un conjunto de controles se hayan elegido para correr un código, en lugar de hacerlo ante cada pequeño cambio en los inputs.

Mejorando la apariencia de nuestra shinyapp con shinydashboard y shinyWidgets

Aunque nosotros estemos escribiendo en R, por detrás shiny lo traduce a HTML y javascript, que son algunos de los lenguajes involucrados en la construcción y el funcionamiento de las páginas web. Para hacer aplicaciones más avanzadas en las definiciones visuales y en funcionalidades, definitivamente hay que combinar un poco de los dos mundos. Por suerte, dos paquetes nos ayudan mucho con temas de apariencia: shinydashboard y shinyWidgets

Shinydashboard

Shinydashboard no es otra cosa que una distribución diferente del espacio de nuestra aplicación. Se trata de una alternativa a fluidPage(). Veamos cómo luce visualmente una pagina de este estilo ejecutando lo siguiente:

library(shiny)
library(shinydashboard)

ui <- dashboardPage(
  dashboardHeader(),
  dashboardSidebar(),
  dashboardBody()
)

server <- function(input, output) { }

shinyApp(ui, server)

Esta manera de disponer la shinyapp consiste en una sidebar desplegable a la izquierda, donde deberían ir las distintas páginas del dashboard, y un panel a la derecha donde efectivamente mostramos los elementos tanto de input como output. Adaptemos la anterior aplicación a esto. Acá solo muestro el ui, pero es lo único que cambia. No se olviden de cargar las librerías shiny, shinydashboard y ggplot al principio del código

# User interface
ui <- dashboardPage(
  dashboardHeader(),
  dashboardSidebar(),
  dashboardBody(
    box(sliderInput(inputId = "bins",
                                label = "Number of bins:",
                                min = 1,
                                max = 50,
                                value = 30),width = 4),
        box(plotOutput(outputId = "distPlot"),width=6)
  )
)

Las cajas (box) ayudan a organizar nuestro contenido. width hace referencia al sistema de grillas en el cual se divide el ancho de una pantalla en CSS: hay 12 unidades, por lo que un ancho de 4 implica que esa caja ocupará un tercio del panel derecho ¿Cómo hacemos para agregar una nueva página a nuestra aplicación? ¿Dónde aparece?

ui <- dashboardPage(
   dashboardHeader(title = "Dashboard básico"),
    dashboardSidebar(
    # Declaramos los items a poner en la barra de la izquierda
     sidebarMenu(
       menuItem(text = "Dashboard", tabName = "Histograma"),
      menuItem(text = "Widgets", tabName = "Densidad")
     )
    ),
    
   dashboardBody( 
   tabItems(
      # Uno se va llamar Histograma
      tabItem(tabName = "Histograma",
        fluidRow(
          box(plotOutput("HistPlot"), width=6),
         box(sliderInput(inputId = "bins",
                                label = "Number of bins:",
                                min = 1,
                                max = 50,
                                value = 30),width = 4)
          )
        ),
      # Otro se va a llamar Densidad
      tabItem(tabName = "Densidad",
        fluidRow(
          box(plotOutput("DensPlot"), width=6))
          )
      )
    )
)
)

Miren un poco la estructura, es relativamente fácil de entender. En tabItems() definimos cuáles tabItem() vamos a querer. Todos ellos se dispondrán en páginas específicas, nada mal, no?. Veamoslo en funcionamiento. Ejecuten, lo siguiente

library(shiny)
library(shinyWidgets)
library(ggplot2)

# User interface
ui <- dashboardPage(
   dashboardHeader(title = "Dashboard básico"),
    dashboardSidebar(
     sidebarMenu(
       menuItem(text = "Dashboard", tabName = "Histograma"),
      menuItem(text = "Widgets", tabName = "Densidad")
     )
    ),
    # Declaramos los items a poner en la barra de la izquierd
   dashboardBody( 
   tabItems(
      # Uno se va llamar Histograma
      tabItem(tabName = "Histograma",
        fluidRow(
          box(plotOutput("HistPlot"), width=6),
         box(sliderInput(inputId = "bins",
                                label = "Number of bins:",
                                min = 1,
                                max = 50,
                                value = 30),width = 4)
          )
        ),
      # Otro se va a llamar Densidad
      tabItem(tabName = "Densidad",
        fluidRow(
          box(plotOutput("DensPlot"), width=6))
          )
      )
    )
)
)
# Acá definimos qué relación hay entre los input y los outputs.
# todo el procesamiento está acá
server <- function(input, output) {
#output es una lista que "anota" qué es lo que tiene que mostrar. En este caso
# le decimos que tieene que hacer un histograma
output$HistPlot <- renderPlot({
    # faithful es un dataset que viene precargado en R.
    datos  <- data.frame(faithful$waiting)
    # lo que hace es discretizar a los waiting times entre erupción según la cantidad
    # de bins que el usuario elija. inputs es una lista que "anota" lo que le manda 
    # el usuario
    ggplot(datos) +
      geom_histogram(aes(x=faithful.waiting),fill="#75AADB",bins=input$bins) +
      labs(title=isolate({input$titulo}),
           x="Tiempo de espera hasta la próxima erupción (minutos)")
  })

output$DensPlot <- renderPlot({
    # faithful es un dataset que viene precargado en R.
    datos  <- data.frame(faithful$waiting)
    # lo que hace es discretizar a los waiting times entre erupción según la cantidad
    # de bins que el usuario elija. inputs es una lista que "anota" lo que le manda 
    # el usuario
    ggplot(datos) +
      geom_density(aes(x=faithful.waiting),fill="#75AADB") +
      labs(title=isolate({input$titulo}),
           x="Tiempo de espera hasta la próxima erupción (minutos)")
  })
  
}
shinyApp(ui, server)

Shinydashboard tiene un muy buena documentación que les recomiendo que repasen.

ShinyWidgets

Evitando duplicar trabajo: reactive()