Tutorial: Crea aplicaciones web interactivas en R con Shiny
14/5/2026
Shiny es un paquete de R para desarrollar aplicaciones web interactivas. Con Shiny puedes crear apps que tus usuarios/as podrán ver desde su navegador, y pueden contener todo tipo de contenido: gráficos, tablas, botones, textos dinámicos, mapas, etc. Es una forma de usar R para crear aplicaciones web centradas en datos que simplifica muchos aspectos complejos del desarrollo web.
Shiny le da vida a todo lo que hayas desarrollado con R sin cambiarte a otro lenguaje ni aprender uno distinto. Esto significa que la distancia se acorta entre el gráfico que hiciste o el resultado de tus estudios y el tener un producto presentable para tu público o clientes.
Índice
Algunos de los beneficios de usar Shiny son:
Control del stack completo de la aplicación desde un mismo lenguaje:
- Shiny se encarga de que todas las piezas (HTML, CSS, JavaScript) funcionen desde R
- No necesitas ningún aprender ningún lenguaje nuevo porque todo funciona con R
- Todo lo que hayas hecho en R se podrá incluir en tu aplicación sin necesidad de cambios, como tus gráficos, tablas y más.
Tiempo de desarrollo reducido incluso para personas sin experiencia en informática:
- El trabajo necesario para producir una aplicación es muy bajo; en unos minutos puedes tener algo funcional
- No necesitas expertos o equipos externos para desarrollar una aplicación, ya que Shiny simplifica al máximo el proceso
Algunas aplicaciones hechas con Shiny
En este tutorial veremos cómo crear una aplicación Shiny desde cero!
Estructura de una app Shiny
Las aplicaciones Shiny son, en su forma más básica, un solo archivo: app.R.
En este script estará todo lo que necesita la aplicación para funcionar!
Obviamente este script debe estar dentro de una carpeta, idealmente un
proyecto de R. Todo lo que esté en la carpeta donde se encuentre app.R estará al alcance de tu app.
Dentro de app.R, la aplicación se divide en tres secciones principales:
shinyApp()
En cada una de estas secciones usaremos código de R para establecer lo que necesita la aplicación, como paquetes, datos, configuraciones (global), para diseñar la interfaz gráfica (UI), y para procesar los datos y outputs de la aplicación (server).
Al final de app.R, una línea mágica transformará el script en una aplicación: shinyApp()
Creando una aplicación vacía
Si no tienes Shiny instalado, instálalo con:
install.packages("shiny")
Todas las aplicaciones Shiny empiezan muy sencillas, con unas cuantas líneas que definen lo mínimo para que funcione, y después van creciendo en complejidad.
Para entender los conceptos básicos, haremos una aplicación completamente vacía. Empezamos creando un nuevo script, que llamaremos app.R.
Global
Lo inicial siempre va a ser cargar los paquetes necesarios en la sección global de la aplicación, que corresponde a las primeras líneas del script app.R.
En la primera línea de app.R agrega:
library(shiny)
Interfaz
Luego tenemos que crear la interfaz de la aplicación. Esto es todos los aspectos visuales e interactivos de la aplicación, lo que las y los usuarios/as verán y usarán.
La interfaz de la aplicación se crea con una función. Hay varias disponibles, pero usaremos una muy básica, page_fillable()
del paquete {bslib}.
library(shiny)
library(bslib)
ui <- page_fillable()
La función page_fillable() creará el objeto ui, que es necesario para que la aplicación funcione. Este objeto va a contener toda la interfaz de la aplicación.
Agreguemos un título y un texto a la interfaz de nuestra aplicación:
library(shiny)
library(bslib)
ui <- page_fillable(
h1("Título"),
p("Texto dentro de la app")
)
Los elementos que pongamos dentro de la ui tienen que ir separados por comas!
Server
En la sección server de la aplicación es donde ocurren todos los cálculos que normalmente hacemos en R. Por ejemplo, filtrar datos, procesar información y crear gráficos.
El server es simplemente una función:
server <- function(input, output) {
}
Dentro de server iremos poniendo más funciones que creen las partes de la aplicación que requieren de cálculos y/o datos. Podemos dejarla vacía por mientras.
Aplicación vacía
Unamos las piezas en un solo script app.R, agregando la última pieza para que la aplicación sea ejecutable: shinyApp()
# global
library(shiny)
library(bslib)
# interfaz
ui <- page_fillable(
h1("Título"),
p("Texto dentro de la app")
)
# servidor
server <- function(input, output) {
}
# ejecutar la app
shinyApp(ui, server)
Estas son las partes básicas de una app Shiny. Si guardas esto en un script, notarás que aparece el botón Run App en la parte superior derecha del panel de scripts:
Al presionarlo, tu aplicación se ejecutará en el panel Viewer de RStudio. Si hiciste todo bien (cargar los paquetes, separar elementos de UI con comas, poner shinyApp() al final) debería aparecer la aplicación en el panel!
Hasta ahora hemos hecho lo más simple posible: una aplicación con un poco de texto, pero sin datos, sin interacción, ni nada.
Ver código de la app vacía
Pon este código en el script app.R:
# global
library(shiny)
library(bslib)
# interfaz
ui <- page_fluid(
h1("Título"),
p("Texto dentro de la app")
)
# servidor
server <- function(input, output) {
}
# ejecutar la app
shinyApp(ui, server)
Tutorial básico recomendado
Creando una aplicación básica con datos
La gracia de las aplicaciones Shiny es que usen datos y permitan a sus usuarios explorar y visualizar información de manera interactiva.
Ahora iremos poco a poco construyendo una app simple que cargue datos y los use para demostrar conceptos básicos de Shiny.
Cargar datos en la app
Bajemos un conjunto de datos para probar: el catastro de campamentos 2024 realizado por el Ministerio de Vivienda y Urbanismo de Chile. Esta versión de los datos corresponde a una base previamente limpiada con código de este repositorio.
El archivo Excel se llama datos.xlsx. Guardamos el archivo en la carpeta de nuestra aplicación:
Al estar el archivo de datos en la misma carpeta que app.R, se puede cargar tan solo llamando su nombre. Entonces, cargamos con la función apropiada antes de la interfaz de la app:
library(shiny)
library(bslib)
library(readxl)
datos <- read_excel("datos.xlsx")
ui <- page_fillable(
# ...
Todo lo que se ejecute en la sección global (al inicio de la app y antes de la UI) estará disponible constantemente durante tu aplicación.
Código de la app hasta ahora
library(shiny)
library(bslib)
library(readxl)
datos <- read_excel("datos.xlsx")
# interfaz
ui <- page_fillable(
h1("Título"),
p("Texto dentro de la app")
)
# servidor
server <- function(input, output) {
}
# ejecutar la app
shinyApp(ui, server)
Textos básicos
Agreguemos un título y una descripción breve a la app. Estos cambios los hacemos en la interfaz de la app:
ui <- page_fillable(
# título
h1("Campamentos en Chile"),
# párrafo
p("Aplicación Shiny para explorar datos
de campamentos registrados en Chile.")
)
En HTML (el código usado para construir páginas web) los titulares se definen como h1, h2, hasta h5, donde el número representa la jerarquía, en 1 correspondiendo al título principal de la página.
Shiny también crea sus aplicaciones web con HTML, pero nos ayuda a usarlo desde R. Creamos títulos con las funciones h1(), h2(), h3(), etc.
Los párrafos se crean con la función p().
Componer textos con datos
Podemos usar objetos de R para crear textos estáticos (que no cambian) en la app. Para ello podemos ayudarnos de la función glue() que nos facilita pegar textos. Veamos la comparación entre usar paste() versus glue() para componer textos:
x <- "gatos"
y <- "patos"
paste("me gustan los", x, "y los", y, "también")
glue::glue("me gustan los {x} y los {y} también")
Con glue() el código resulta mucho más legible, porque te ahorra tener que poner tantas comas y comillas!
Publicaciones relacionadas
Mejoremos entonces el párrafo p() de la app:
library(glue)
ui <- page_fillable(
# título
h1("Campamentos en Chile"),
# párrafo
p(
glue("Aplicación Shiny para explorar los datos de
los {nrow(datos)} campamentos registrados en Chile.")
),
)
Ahora el texto del párrafo muestra el número de filas de la tabla de datos!
Textos con Markdown
Escribir textos con estilos como negrita o itálicas, o textos con enlaces en HTML puede ser engorroso, pero podemos usar Markdown, un lenguaje de marcado que nos facilita la escritura de HTML mediante símbolos que representan las propiedades del texto:
| texto | código | resultado |
|---|---|---|
| Negrita | **hola** |
hola |
| Itálica | _hola_ |
hola |
| Enlace | [hola](www.com) |
hola |
| Tachado | ~~hola~~ |
|
| Código | `hola` |
hola |
La función markdown() nos permite incluir este tipo de textos en nuestras apps Shiny. En una sola función, y usando algunos símbolos, podemos escribir un párrafo con enlaces, negritas e itálicas:
# párrafo
p(
glue("Aplicación Shiny para explorar los datos de
los {nrow(datos)} campamentos registrados en Chile.")
),
# párrafo markdown
markdown("Los **campamentos** son definidos por el
[Minvu](https://www.minvu.gob.cl/catastro-campamentos-2022/)
como _Asentamientos de ocho o más viviendas precarias que
habitan en posesión irregular un terreno, con carencia de
servicios básicos, agrupadas y contiguas._")
Así va quedando la app:
Fíjate que los elementos dentro de la UI van separados por comas (porque en realidad son argumentos dentro de la función que genera la UI!), así que después de cada cierre de paréntesis no olvides la coma!
Código de la app hasta ahora
# global
library(shiny)
library(bslib)
library(readxl)
library(dplyr)
library(glue)
# cargar datos
datos <- read_excel("datos.xlsx")
# interfaz
ui <- page_fillable(
# título
h1("Campamentos en Chile"),
# párrafo
p(
glue("Aplicación Shiny para explorar
los datos de los {nrow(datos)}
campamentos registrados en Chile.")
),
# párrafo markdown
markdown("Los campamentos son definidos por el
[Minvu](https://www.minvu.gob.cl/catastro-campamentos-2022/)
como _Asentamientos de ocho o más viviendas precarias que
habitan en posesión irregular un terreno, con carencia de
servicios básicos, agrupadas y contiguas._")
)
# servidor
server <- function(input, output) {}
# ejecutar la app
shinyApp(ui, server)
Interacción básica con inputs
Hasta ahora la aplicación es estática. Pero la gracia es agregar elementos interactivos con los que l@s usuari@s puedan explorar los datos.
Un elemento con el que los usuarios interactúan en la app se denomina input. Los inputs se crearon con funciones, y se ponen en la interfaz de la aplicación.
Al crear cualquier input, el primer argumento de la función es su inputId, que es el identificador o nombre del input, que usaremos luego para obtener sus resultados.
Crear un selector de alternativas
Quizás el input más simple es selectInput(), que agrega un selector de opciones. Estas opciones podemos escribirlas a mano en el argumento choices, o podemos usar un vector o la columna de un dataframe como opciones para el selector.
Crearemos un input que permita seleccionar una opción desde una lista de regiones. Haremos que las regiones posibles de elegir vengan directamente de los datos. El conjunto de datos
que descargamos (y que cargamos como datos) tiene el nombre de la región a la que pertenece cada observación en la columna region, así que si usamos unique(datos$region) obtendremos un vector con sus valores únicos.
Entonces creamos el input con selectInput(), le damos el ID "region" (porque es un selector de regiones), y las elecciones posibles (choices) serán las regiones que vienen en el conjunto de datos:
# interfaz
ui <- page_fillable(
h1("Campamentos en Chile"),
...
selectInput(
inputId = "region", # ID único del input
label = "Explorar regiones", # titular del input
choices = sort(unique(datos$region)) # vector con regiones desde los datos
)
...
)
Si probamos la app, veremos que aparece un cuadro donde se puede hacer la selección:
Inputs de una app
Por ahora, nuestro selector no hace nada, pero internamente, cuando seleccionamos una opción, Shiny actualiza un objeto especial: el objeto input. Este objeto contiene los valores de todos los inputs de nuestra app, según el ID que le dimos.
selectInput("selector")→
input$selectorAl seleccionar una opción de un selector, inmediatamente se actualiza el objeto input${inputId}, donde {inputId corresponde al ID que le dimos al input. En nuestro ejemplo del selector de regiones, el valor elegido estaría en input$region, dado que el ID del input era region.
Pronto veremos cómo usar este valor!
Uniendo inputs y datos en server
Ahora que tenemos datos y un input, pasamos a hacer cosas con eso datos y esos inputs! ✨
Dentro de la sección server de una app (la función server) es donde se conectan los inputs con los datos.
En la función server es donde vamos a usar R como el motor de la app.
En server, lo que principalmente haremos será crear outputs o salidas. Los outputs son la forma de hacer que un cálculo que haga la aplicación se muestre en la interfaz.
Considerando las secciones UI y server, el ciclo de interacción de una app es:
- En la interfaz de la app hay inputs
- El/la usuario/a interactúa con un input
- El input se usa en server para producir una salida
- La salida se muestra en la interfaz de la app.
Naturalmente pueden haber pasos intermedios en operaciones más complejas, pero en resumen lo anterior describe el funcionamiento básico de una app Shiny.
Usando inputs dentro de server
Pasemos un input al server para hacer algo con su valor. Cuando hacemos esto, tenemos que tener una idea de qué es lo que queremos lograr, o para qué usaremos el input. Esto significa que tenemos que pensar en el resultado que esperamos poner en la app; es decir, en el output que se mostrará en la interfaz de la app.
Pensemos en un caso simple: queremos contar la cantidad de observaciones por región. Tenemos nuestro dataframe datos, el cual podemos filtrar por región y luego contar las filas para saber la cantidad de campamentos en una región:
casos_region <- datos |>
filter(region == "Maule") |>
nrow()
casos_region
[1] 25
Entonces, lo que haremos en la sección server de la app será algo equivalente a esto:
# definir input de prueba
region_elegida <- "Biobío"
casos_region <- datos |>
# aplicar input de prueba en el filtro
filter(region == region_elegida) |>
nrow()
casos_region
[1] 225
En este ejemplo creamos un input “falso” en el objeto region_elegida para poder probar la lógica en un scirpt aparte, pero en la app podremos usar el objeto input$region:
casos_region <- datos |>
filter(region == input$region) |>
nrow()
Esto lo haremos en server a continuación.
Generando salidas o outputs
En la sección server de la app, creamos el output que será la salida con lo que vamos a mostrar en la interfaz de la app. Aquí es donde podemos usar R normalmente para crear lo que queramos, pero dentro de ciertos marcos que circunscriben el código de R a la lógica de una aplicación interactiva Shiny.
Uno de estos marcos para apps interactivas es que, a diferencia de un script normal donde podemos generar resultados (cifras, tablas, gráficos) en cualquier parte, en una app tenemos que definir las salidas de manera clara, con un nombre y una función apropiada para crear la salida, y luego ubicar las salidas en algún lugar de la interfaz de la aplicación.
La salida que crearemos (el conteo de casos por región) se va a llamar casos_region, y como hay que explicitar que el código será una salida para la app, asignamos el resultado que queremos mostrar a un objeto preexistente llamado output que se encargará de recibir todas las salidas de nuestra app. Empecemos simplemente creando el objeto casos_region asignado a output, lo que quedaría así: output$casos_region
) # final de la ui
...
# servidor
server <- function(input, output) {
# conteo de casos filtrados por región
output$casos_region <- ... # aquí vamos...
}
# ejecutar la app
shinyApp(ui, server)
Ahora tenemos que pensar qué tipo de output crearemos, porque dependiendo del tipo de salida será la función que necesitamos usar para convertir el resultado de R en algo visible en nuestra app. En nuestro caso crearemos un texto con el conteo de casos, así que usaremos renderText() para crear o renderizar la cifra en un texto legible para la aplicación.
) # final de la ui
...
# servidor
server <- function(input, output) {
# conteo de casos filtrados por región
output$casos_region <- renderText({
datos |>
filter(region == "Maule") |>
nrow()
})
}
# ejecutar la app
shinyApp(ui, server)
La función renderText() convertirá el texto en un objeto HTML apropiado para la aplicación. Si quisiéramos hacer un gráfico, usaríamos renderPlot(), y así.
Ahora podemos reemplazar el filtro de prueba que hicimos por un filtro que use el input:
output$casos_region <- renderText({
datos |>
filter(region == input$region) |>
nrow()
})
Esto significa que el/la usuario/a eligirá qué es lo que se filtra, y que cada vez que se cambie el selector, se actualizará el objeto input$region, y por consiguiente se volverá a ejecutar el código de output$casos_region para entregar el nuevo resultado!
Hasta ahora tenemos algo como esto:
output$casos_region que identifica la salida para poder usarla
renderText() que renderiza el código de R en una salida HTML para la app
El último paso es conectar el output con el UI para que nuestra salida aparezca en alguna parte de la app. Para ello, invocamos el nombre del output en la función textOutput(), y la ubicamos en la UI de nuestra app; en este caso, debajo del selector mismo:
selectInput(
inputId = "region",
label = "Explorar regiones",
choices = sort(unique(datos$region))
),
# salida de texto de conteo de observaciones
textOutput("casos_region")
Lo que hicimos fue:
- Crear un input
- Crear un output
- Hacer que el output use el input
- Poner el output en la parte visible de nuestra app (UI)
Pasamos a tener esto:
textOutput() que ubica la salida en la interfaz de la app
renderText() que renderiza la salida a partir de código R
Veamos cómo funciona la aplicación!
Mejoremos la salida de texto para que sea más descriptiva: volvamos a server, al output que creamos, y hagamos que incluya más texto y el nombre de la región seleccionada:
output$casos_region <- renderText({
conteo <- datos |>
filter(region == input$region) |>
nrow()
glue("La región {input$region} tiene registrados {conteo} casos.")
})
Siguiendo estas mismas ideas podemos crear un párrafo de texto más complejo usando distintas variables, combinando múltiples inputs, y más!
Ver código completo de la app hasta ahora
Guarda este código en un script app.R para poder ejecutarlo:
# global
library(shiny)
library(bslib)
library(readxl)
library(dplyr)
library(glue)
# cargar datos
datos <- read_excel("datos.xlsx")
# interfaz
ui <- page_fillable(
# título
h1("Campamentos en Chile"),
# párrafo
p(
glue(
"Aplicación Shiny para explorar
los datos de los {nrow(datos)}
campamentos registrados en Chile."
)
),
# párrafo markdown
markdown(
"Los **campamentos** son definidos por el
[Minvu](https://www.minvu.gob.cl/catastro-campamentos-2022/)
como _Asentamientos de ocho o más viviendas precarias que
habitan en posesión irregular un terreno, con carencia de
servicios básicos, agrupadas y contiguas._"
),
# selector de regiones
selectInput(
inputId = "region",
label = "Explorar regiones",
choices = sort(unique(datos$region))
),
# salida de texto de conteo de observaciones
textOutput("casos_region")
)
# servidor
server <- function(input, output) {
# conteo de casos filtrados por región
output$casos_region <- renderText({
conteo <- datos |>
filter(region == input$region) |>
nrow()
glue("{input$region} tiene registrados {conteo} casos.")
})
}
# ejecutar la app
shinyApp(ui, server)
Agregar un gráfico
Repaso: Ya vimos que, para poner algo en la app, necesitamos generar un output y ponerlo en la interfaz (UI) de la app con una función {x}output(), como textOutput(), mientras que el output se calcula en server, y necesita crearse con una función render{x}(), como renderText().
Hacer un gráfico de prueba primero
Antes de poner un gráfico en una app, recomiendo empezar con una prueba del gráfico en un script separado, y luego portar el script a la app.
Publicaciones relacionadas
Haremos un gráfico de barras que muestre los principales campamentos de cada región elegida por el/la usuario/a.
Cargamos los datos, los filtramos por región (pensando en cómo lo haríamos con el selector input$region), luego reducimos la cantidad de observaciones a las más relevantes con slice_max() para dejar sólo las con más hogares, y al final ordenamos las observaciones por la variable numérica hogares:
library(dplyr)
library(readxl)
library(dplyr)
library(forcats)
datos <- read_xlsx("datos.xlsx")
# filtrar una región
region_elegida = "Biobío"
datos_region <- datos |>
filter(region == region_elegida)
# filtrar máximo de observaciones, y ordenarlas
datos_region_grafico <- datos_region |>
slice_max(hogares, n = 6) |>
mutate(nombre = fct_reorder(nombre, hogares))
Ahora procedemos a hacer el gráfico:
library(ggplot2)
datos_region_grafico |>
ggplot() +
aes(hogares, nombre) +
geom_col(width = 0.7) +
geom_text(
aes(label = hogares),
hjust = 1.3, color = "white", fontface = "bold") +
scale_x_continuous(expand = 0) +
theme_minimal() +
theme(
axis.title.y = element_blank(),
axis.text.y = element_text(face = "bold"))
Publicaciones relacionadas
Poner el gráfico en la app
Para poner el gráfico en la app Shiny, principalmente hay que hacer dos cosas:
- Ubicar el output en donde queremos que aparezca en la interfaz de la app con
plotOutput() - Generar (render) el gráfico desde
serverusando los datos e inputs necesarios.
Empecemos ubicando el gráfico (todavía inexistente) en algún lugar de la UI nuestra app, como debajo del selector y del texto que generamos más arriba.
# selector de regiones
selectInput(
inputId = "region",
label = "Explorar regiones",
choices = sort(unique(datos$region))
),
# salida de texto de conteo de observaciones
textOutput("casos_region"),
# salida de gráfico
plotOutput("grafico_barras_region")
Pusimos plotOutput("grafico_barras_region") para que el output output$grafico_barras_region aparezca debajo del texto.
Si ejecutamos la app hasta ahora, no va a aparecer nada, porque la salida no recibe nada para mostrar.
Ahora vamos a la sección server y calculemos el gráfico. Tenemos que crear un output con el mismo nombre que pusimos arriba, y asignarle el resultado de la función renderPlot():
server <- function(input, output) {
...
# gráfico de barras
output$grafico_barras_region <- renderPlot({
# aquí va el código del gráfico
})
...
}
Dentro de renderPlot(), como tiene paréntesis de llave ({}), podemos escribrir cualquier sintaxis de R que queramos. En este punto podemos copiar y pegar el código del gráfico que hicimos más arriba, incluyendo el procesamiento de sus datos, para generar el mismo gráfico pero dentro de nuestra app:
server <- function(input, output) {
...
# gráfico de barras
output$grafico_barras_region <- renderPlot({
# filtrar por región
datos_region <- datos |>
filter(region == input$region) # usar input!
# limitar cantidad de casos y ordenar
datos_region_grafico <- datos_region |>
slice_max(hogares, n = 6) |>
mutate(nombre = fct_reorder(nombre, hogares))
# gráfico
datos_region_grafico |>
ggplot() +
aes(hogares, nombre) +
geom_col(width = 0.7) +
geom_text(
aes(label = hogares),
hjust = 1.3,
color = "white",
fontface = "bold"
) +
scale_x_continuous(expand = 0) +
theme_minimal(base_size = 13) +
theme(
axis.title.y = element_blank(),
axis.text.y = element_text(face = "bold")
)
})
...
}
La única diferencia es que ponemos input$region en el filter() para que el filtro de regiones funcione sobre el gráfico!
Recapitulando:
- Hicimos un gráfico de prueba usando los mismos datos que la app
- Ubicamos el lugar donde va a ir el gráfico con
plotOutput()enui - Calculamos el gráfico con
renderPlot()enserver - Pusimos el código del gráfico dentro de
renderPlot() - Pusimos
input$regiondentro del código del gráfico para que se pueda cambiar el filtro interactivamente
Probemos la aplicación!
Ver código de la app hasta ahora
# global
library(shiny)
library(bslib)
library(dplyr)
library(readxl)
library(glue)
library(forcats)
library(ggplot2)
# cargar datos
datos <- read_excel("datos.xlsx")
# interfaz
ui <- page_fillable(
# título
h1("Campamentos en Chile"),
# párrafo
p(
glue(
"Aplicación Shiny para explorar
los datos de los {nrow(datos)}
campamentos registrados en Chile."
)
),
# párrafo markdown
markdown(
"Los **campamentos** son definidos por el
[Minvu](https://www.minvu.gob.cl/catastro-campamentos-2022/)
como _Asentamientos de ocho o más viviendas precarias que
habitan en posesión irregular un terreno, con carencia de
servicios básicos, agrupadas y contiguas._"
),
# selector de regiones
selectInput(
inputId = "region",
label = "Explorar regiones",
choices = sort(unique(datos$region))
),
# salida de texto de conteo de observaciones
textOutput("casos_region"),
# salida de gráfico
plotOutput("grafico_barras_region")
)
# servidor
server <- function(input, output) {
# conteo de casos filtrados por región
output$casos_region <- renderText({
conteo <- datos |>
filter(region == input$region) |>
nrow()
glue("{input$region} tiene registrados {conteo} casos.")
})
# gráfico de barras
output$grafico_barras_region <- renderPlot({
# filtrar por región
datos_region <- datos |>
filter(region == input$region)
# limitar cantidad de casos y ordenar
datos_region_grafico <- datos_region |>
slice_max(hogares, n = 6) |>
mutate(nombre = fct_reorder(nombre, hogares))
# gráfico
datos_region_grafico |>
ggplot() +
aes(hogares, nombre) +
geom_col(width = 0.7) +
geom_text(
aes(label = hogares),
hjust = 1.3,
color = "white",
fontface = "bold"
) +
scale_x_continuous(expand = 0) +
theme_minimal(base_family = "Arial",
base_size = 13) +
theme(
axis.title.y = element_blank(),
axis.text.y = element_text(face = "bold")
)
})
}
# ejecutar la app
shinyApp(ui, server)
Publicaciones sobre visualización de datos
Añadir un nuevo input
Dentro del gráfico que hicimos hay otro parámetro que podríamos entregar al usuario: cuando agregamos slice_max() limitamos el máximo de observaciones entregadas a n = 6, filtradas por las de mayor valor en la variable hogares.
Hagamos que el valor del argumento n se cambie con un slider o deslizador, para que las y los usuarios/as puedan elegir cuántas observaciones visualizar en el gráfico.
En la interfaz de la aplicación, pensemos dónde poner el input. Yo creo que 🤓☝🏼 un selector de este tipo debería ir debajo del gráfico, ya que es un ajuste que se haría después de elegir la región, ya que no es tan relevante como para poner antes de la visualización misma.
Vamos al ui y agregamos un nuevo input, separado por comas de los elementos anteriores. En sliderInput(), como todos los inputs, empezamos poniendo el inputId (que es el nombre con el que llamaremos a su valor) y un label que es el título del selector. Los argumentos de este input son el mínimo y máximos, el valor que tiene seleccionado por defecto (value), y otros como step que son los saltos entre valores, y su ancho (width).
# salida de texto de conteo de observaciones
textOutput("casos_region"),
# salida de gráfico
plotOutput("grafico_barras_region"),
# selector de observaciones máximas
sliderInput(
inputId = "maximo",
label = "Cantidad de resultados",
min = 3,
max = 15,
value = 8,
step = 1,
width = "100%"
)
Debajo del gráfico va a aparecer el slider:
Ahora, conectar este nuevo input a cualquier paso de la app es simplemente reemplazar el valor del argumento de cualquier función de R por input${nombre}, en este caso input$maximo:
server <- function(input, output) {
...
# gráfico de barras
output$grafico_barras_region <- renderPlot({
# filtrar por región
datos_region <- datos |>
filter(region == input$region) # usando el input
# limitar cantidad de casos y ordenar
datos_region_grafico <- datos_region |>
slice_max(hogares, n = input$maximo) |> # usando el input
mutate(nombre = fct_reorder(nombre, hogares))
# gráfico
datos_region_grafico |>
ggplot() +
...
})
...
}
Entonces, el argumento n de slice_max() será por defecto 8 (lo que pusimos en el input), pero cambiará cuando muevas el slider!
Ver código de la app hasta ahora
# global
library(shiny)
library(bslib)
library(readxl)
library(dplyr)
library(glue)
library(forcats)
library(ggplot2)
# cargar datos
datos <- read_excel("datos.xlsx")
# interfaz
ui <- page_fillable(
# título
h1("Campamentos en Chile"),
# párrafo
p(
glue(
"Aplicación Shiny para explorar
los datos de los {nrow(datos)}
campamentos registrados en Chile."
)
),
# párrafo markdown
markdown(
"Los **campamentos** son definidos por el
[Minvu](https://www.minvu.gob.cl/catastro-campamentos-2022/)
como _Asentamientos de ocho o más viviendas precarias que
habitan en posesión irregular un terreno, con carencia de
servicios básicos, agrupadas y contiguas._"
),
# selector de regiones
selectInput(
inputId = "region",
label = "Explorar regiones",
choices = sort(unique(datos$region))
),
# salida de texto de conteo de observaciones
textOutput("casos_region"),
# salida de gráfico
plotOutput("grafico_barras_region"),
# selector de observaciones máximas
sliderInput(
inputId = "maximo",
label = "Cantidad de resultados",
min = 3,
max = 15,
value = 6,
step = 1,
width = "100%"
)
)
# servidor
server <- function(input, output) {
# conteo de casos filtrados por región
output$casos_region <- renderText({
conteo <- datos |>
filter(region == input$region) |>
nrow()
glue("{input$region} tiene registrados {conteo} casos.")
})
# gráfico de barras
output$grafico_barras_region <- renderPlot({
# filtrar por región
datos_region <- datos |>
filter(region == input$region)
# limitar cantidad de casos y ordenar
datos_region_grafico <- datos_region |>
slice_max(hogares, n = input$maximo) |>
mutate(nombre = fct_reorder(nombre, hogares))
# gráfico
datos_region_grafico |>
ggplot() +
aes(hogares, nombre) +
geom_col(width = 0.7) +
geom_text(
aes(label = hogares),
hjust = 1.3,
color = "white",
fontface = "bold"
) +
scale_x_continuous(expand = 0) +
theme_minimal(base_family = "Arial", base_size = 13) +
theme(
axis.title.y = element_blank(),
axis.text.y = element_text(face = "bold")
)
})
}
# ejecutar la app
shinyApp(ui, server)
Contenido avanzado
Personalizando la app
Hay muchas funciones que nos ayudarán a agregar elementos útiles a la interfaz (UI) de nuestras aplicaciones:
- La función
br()inserta un salto de líneas - Con
hr()puedes insertar una barra horizontal para separar contenidos - Usando
div()puedes insertar contenidos HTML completamente personalizables; por ejemplo, crear un recuadro redondeado que contenga texto, usando código CSS en el argumentostyle:
div(
style = "margin: 6px; padding: 8px;
border-radius: 5px;
background-color: #EDEDED;",
p("Texto dentro de un recuadro redondeado")
)
Cambiar los colores de la app
Para cambiar el
tema de colores de la aplicación, simplemente agrega el argumento theme en la función que uses para crear la página (como page_fluid(), page_sidebar(), etc.), y mediante la función bs_theme(), agrega los tres colores principales: el color de fondo (bg), el color del texto (fg), y el color primario (primary):
library(shiny)
library(bslib)
ui <- page_fluid(
theme = bs_theme(bg = "#EAD1FA",
fg = "#553A74",
primary = "#8557AB"),
...
Si de tienes y vuelves a ejecutar tu aplicación (para que se reconstruya el código HTML de base), verás el cambio de colores! 🌈
Instrucciones completas en este tutorial
Usar tipografías personalizadas
Puedes
cambiar la tipografía que se use en tus aplicaciones Shiny dentro de bs_theme(), en el argumento theme de la función que uses para constuir la UI de tu app. Ahí puedes definir base_font para la tipografía general de la app, y opcionalmente heading_font si quieres que los títulos (h1, h2, etc.) sean distintos. Con la función font_google() puedes hacer que tu app cargue automáticamente tipografías gratuitas de
Google Fonts.
library(shiny)
library(bslib)
ui <- page_fluid(
theme = bs_theme(
base_font = font_google("Manrope"),
heading_font = font_google("Domine")
),
...
Instrucciones completas en este tutorial
Encuentra aquí las instrucciones para aplicar las tipografías a los gráficos ggplot2!
Publicaciones relacionadas
Personalización avanzada con CSS
CSS es el lenguaje que se usa para especificar el diseño de las páginas web. Como Shiny genera páginas web HTML, podemos usar CSS para personalizar cualquier aspecto de la app.
En cualquier parte de la UI puedes agregar tags$style() para escribir código CSS que se aplicará a toda la aplicación. Sirve para personalizar la apariencia de inputs, aspectos visuales y de espaciado de los temas, o creación de clases personalizadas para aplicar a los elementos de tu app.
También puedes usar una hoja de estilos CSS externa, guardada en un archivo como estilos.css, y cargarla con includeCSS("estilos.css") para aplicarla a tu app.
Si quieres cambiar el aspecto de un input, en un navegador web puedes inspeccionar la app, revisar las clases y estilos del elemento, y reemplazarlos en la hoja de estilo con CSS (usualmente poniéndole !important para que los estilos aplicados tomen preponderancia).
Cambiar la disposición de la app
Hasta ahora vimos una aplicación sencilla donde los elementos aparecen uno debajo del otro. Pero existen varias formas de disponer el contenido de la app para facilitar su navegación.
Por ejemplo, si la aplicación tiene muchos elementos distintos, pero temáticamente agrupados, puedes ordenar los contenidos por pestañas. Si tienes muchos controles o inputs que afectan una o pocas salidas principales, sería conveniente una barra lateral. Si tienes demasiados controles que son opcionales, puedes esconderlos con un acordeón.
Ahora veremos las funciones de Shiny para organizar los elementos de la interfaz de distintas maneras.
Columnas
Con la función layout_columns() puedes organizar lel contenido entolumnas, para que alos elementos parezcan ulado a lado del otro en vez de uno debajo del otro. Cada argumento de layout_columns() será una columna, así que si quieres agrupar varios elementos en una sola columna, envuélvelos en list() o en un div() para tener más control sobre el diseño.
layout_columns(
# columna 1
div(
h2("Columna 1"),
p("Contenido de la columna 1")
),
# columna 2
div(
h2("Columna 2"),
p("Contenido de la columna 2")
)
)
Por defecto, las columnas ocuparán 50% del ancho de la página, pero puedes configurar el ancho de cada columna con el argumento col_widths y usando números que sumen 12: 6 y 6 serían dos columnas del mismo ancho, 4 y 8 sería la columna izquierda más angosta que la derecha, y así.
Organizar contenido en pestañas
Para distribuir el contenido de la aplicación en pestañas, en cualquier lugar de la UI creamos un conjunto de pestañas con navset_tab(), y dentro de esta función vamos creando todos los paneles que queramos con nav_panel():
# panel de pestañas
navset_tab(
# pestaña 1
nav_panel(
title = "Introducción",
div(
...
)
),
# pestaña 2
nav_panel(
title = "Gráficos",
div(
...
)
)
),
En el argumento title va el nombre de cada pestaña. También podemos reemplazar navset_tab() por navset_underline() o navset_pill() para que las pestañas tengan un diseño distinto.
Esto nos sirve para poder estructurar mejor el contenido de la aplicació, distribuyendo la información en paneles ocultos.
Acordeones
Con la función accordion() puedes crear un acordeón, que es un elemento que muestra un título, y al hacer click en el título se despliega un contenido oculto debajo. Esto es útil para esconder controles o información que no es tan relevante, o que tiene que verse por partes.
accordion(
accordion_panel(
title = "Introducción",
... # contenido dentro del acordeón
),
accordion_panel(
title = "Metodología",
...
),
accordion_panel(
title = "Fuentes",
...
)
)
Disposición de tarjetas
Podemos envolver elementos de la interfaz en la función card() para que aparezcan envueltos en una tarjeta con bordes redondeados, lo que ayuda a organizar los contenidos de la aplicación.
card(
card_header(
"Título de la tarjeta"
),
p("Contenido de la tarjetita")
)
Usando la función layout_columns() puedes organizar las tarjetas en columnas, para que aparezcan una al lado de la otra.
layout_columns(
card(
p("tarjeta al lado izquierdo")
),
card(
p("tarjeta al lado derecho")
)
)
Además, puedes poner tarjetas dentro de tarjetas para construir disposiciones más complejas.
Barra lateral
Con page_sidebar() puedes crear una disposición con una barra lateral y un panel principal. Simplemente usa page_sidebar() para crear la UI de tu app, y en el argumento sidebar puedes poner los elementos que quieras que aparezcan en la barra lateral:
ui <- page_sidebar(
# contenido de la app que estará dentro de la barra lateral
sidebar = sidebar(
# selector de regiones
selectInput(
...
),
# selector de observaciones máximas
sliderInput(
...
)
),
Barra de menú
Con page_navbar() puedes crear una barra de menú en la parte superior de la app, donde puedes ubicar distintos elementos como el título de la app, enlaces a otras páginas, y más. Para crear un menú con esta función, simplemente reemplaza la función que usas para crear la UI de tu app por page_navbar(), y dentro de esta función puedes agregar los elementos que quieras que aparezcan en el menú:
ui <- page_navbar(
# panel de introducción
nav_panel(
title = "Introducción",
h1("Campamentos en Chile"),
...
),
# panel con controles
nav_panel(
title = "Controles",
selectInput(
...
),
sliderInput(
...
)
),
# panel de resultados
nav_panel(
title = "Gráficos",
...
)
)
Desde dispositivos móviles o en ventanas pequeñas, el menú superior se transforma en un botón que muestra las opciones:
La disposición de menús se puede combinar con otras disposiciones, como la barra lateral, para crear una app con menú y barra lateral al mismo tiempo!
Control de acceso con usuario y contraseña
Puedes agregar la funcionalidad de usuarios con contraseña en tu aplicación
siguiendo este tutorial del paquete {shinymanager}.
Publicaciones relacionadas
Publicar la aplicación en internet
Hasta ahora, tu aplicación ha vivido localmente en tu computadora. Para poder compartirla con otras personas puedes compartir el código con alguien (aburrido, complicado), o bien puedes subirla a un servicio o hostearla en un servidor para que esté en internet y pueda ser compartida.
Existen muchas formas de subir una aplicación (o desplegarla a producción, como se dice), pero acá veremos la más sencilla, que es usando los servicios de Posit. Antes existía Shinyapps.io, pero el servicio se está descontinuando. En su reemplazo, hoy existe Posit Connect Cloud.
Posit Connect Cloud es un servicio gratuito (con cuentas pagadas opcionales) gestionado por Posit (la empresa detrás de gran parte el ecosistema de R) donde puedes subir distinto tipo de contenido creado con R, directamente desde RStudio; entre ellas, aplicaciones Shiny.
Con el script de tu app abierto, presiona el botón Publish que está en al esquina superior derecha del script:
Se abrirá una ventana preguntándote qué archivos del proycto subir (elige los necesarios, o si no sabes, elige todos), y en el lado derecho podrás elegir el título de la app y la cuenta a la que la vas a subir.
Si no tienes una cuenta, presiona Add new account para conectar o crear una cuenta. Aquí se abrirá una ventana y tienes que elegir Posit Connect Cloud:
Luego podrás conectarte con tu cuenta de Posit, o bien crear una cuenta. Esta opción te permite subir aplicaciones gratis (con un máximo de apps y una capacidad de procesamiento limitados), o bien contratar un plan de pago para subir más aplicaciones y con más capacidad de procesamiento.
Si todo sale bien, en la consola aparecerá el proceso de subida o deployment:
Finalmente encontrarás la aplicación en tu cuenta de Posit Connect Cloud, y podrás compartir el enlace con quien quieras (botón en la barra superior) para que puedan usar tu aplicación!
Publicaciones relacionadas
Conceptos avanzados de Shiny
Profundicemos un poco más en el funcionamiento de Shiny. Para esto tenemos que entender la lógica que opera tras el funcionamiento de apps Shiny.
Reactividad
En los pasos anteriores hicimos una app básica que usa datos para mostrar un selector, y al cambiar el selector se ejecuta código de R para entregar un resultado. En otras palabras, tuvimos código de R que se re-ejecutó automáticamente cada vez que cambió un elemento interactivo del cual dependía.
Esta idea de que el código se ejecute cuando cambia algo se llama reactividad, y es la idea base detrás del funcionamiento de Shiny.
Reactividad es el hecho de que, al cambiar un elemento que es una dependencia de un objeto reactivo, el objeto reactivo se re-ejecuta para adaptarse al cambio en su dependencia.
Entonces ¿qué es un objeto reactivo? Es un bloque de código de R que se envuelve en una función de Shiny, como renderPlot(), renderText(), o reactive(), que les otorga esta cualidad de poder re-ejecutarse automáticamente.
Puedes crear elementos reactivos en una app Shiny envolviendo el código de R con reactive():
objeto_reactivo <- reactive({
...
})
La función reactive() crea un objeto que también es una función (tienes que usarlo con () después de su nombre), y al usarlo dentro de otros objetos reactivos creas una dependencia a este nuevo objeto. Sirve para crear objetos reactivos que no son outputs, sino pasos intermedios dentro de la lógica de la app.
Veamos un ejemplo básico aislado: el objeto_1() es creado con reactive(), que contiene el valor de un input numérico y le suma 10.
objeto_1 <- reactive({
input$numero + 10
})
El objeto reactivo objeto_1() se actualizará cuando cambie input$numero.
input$numero
objeto_1()
Ahora creemos un segundo objeto reactivo: objeto_2(). Este objeto va a contener el valor de objeto_1(), y además le va a sumar 20.
objeto_2 <- reactive({
objeto_reactivo_1() + 20
})
Entonces, si cambia input$numero, el objeto_1() se actualizará, y a continuación objeto_2() se va actualizar también! Creamos una cadena de dependencias.
input$numero
objeto_1()
objeto_2()
Entonces podemos organizar el código para que funcione en pasos, y finalmente el cambio de input$numero afecte varios pasos en la cadena y termine en uno o más outputs.
¿Para qué sirve esto? Para que la lógica de tu aplicación esté ordenada en pasos, y también para optimizar tu aplicación: si distintos outputs necesitan el mismo procesamiento, puedes crear un objeto reactivo que haga ese cálculo, y luego los dos outputs pueden depender del mismo reactive.
input$numero
filtrar_datos()
output$grafico
output$tabla
output$texto
Observadores
Cómo funciona una app Shiny
En términos resumidos, al ejecutar la aplicación por primera vez se crea la interfaz de la aplicación. Luego Shiny identifica todos los outputs que hay en la aplicación. Cada output tiene una cadena de dependencias; es decir, depende de otros elementos de la app, los que a su vez pueden depender de inputs.
Al iniciar la app, se identifica que existen outputs que requieren calcularse:
Pero al querer calcular los outputs se identifica que se requieren calcular otros elementos anteriores primero; es decir, los outputs dependen de otros resultados. En la mayoría de los casos estos son objetos reactivos:
Los objetos reactivos son código de R que son envueltos en funciones de Shiny que hacen que se re-calculen automáticamente cuando cambien sus dependencias. Son básicamente pasos intermedios entre los inputs o los datos y los outputs.
Entonces, estos pasos intermedios pueden requerir los valores de inputs, o bien, el resultado de otros objetos reactivos. En el caso de este ejemplo, los dos outputs dependen de un objeto reactivo, y éste depende de un input:
Con este paso se termina de determinar la cadena de dependencias; es decir, Shiny identifica todos los pasos requeridos para calcular el output. Entonces se empiezan a entregar los datos para iniciar los cálculos. Primero se identifican los inputs:
Gracias a los inputs se pueden calcular los objetos reactivos o pasos intermedios:
Teniendo los objetos reactivos, los outputs ya cuentan con todo lo necesario, y pueden calcularse para mostrarse en la app.
En este punto nuestra app ya está lista y mostrando los resultados!
Cuando se calculan todos los pasos de la cadena, todo vuelve a un estado neutral, donde ya no necesita calcularse nada nuevo 😌
Pero si en la app se cambia un input, toda la cadena se marca como invalidada: los resultados que se tenían ya no son válidos, porque cambiaron elementos de la cadena!
Como la cadena se invalidó, Shiny nuevamente identifica que los outputs deben calcularse, y los marca como pendientes de cálculo, reiniciando el ciclo:
Los outputs requieren recalcularse, pero los outputs dependen de los reactivos, y los reactivos de otros reactivos o de inputs… entonces se navega hacia atrás por la cadena, marcando como pendiente de cálculo a los elementos requeridos por el output, hasta llegar a los inputs o a elementos sin ninguna dependencia, los cuales pueden calcularse y entregar sus resultados a los pasos siguientes.
Podemos ver este proceso en vivo y en directo
usando el paquete {reactlog}:
Publicaciones relacionadas
Revisar y debuggear apps Shiny
Como hemos visto, Shiny hace varias cosas mientras se ejecuta, lo que puede complicar un poquito el desarrollo y la corrección de problemas.
Recibir mensajes a medida que se ejecuta la app
Otra dificultad tiene que ver con entender qué está pasando en la app, y en qué orden. Hay varias formas para abordar esto.
Al introducir message() dentro de los elementos reactivos y outputs de una app, veremos en la consola de R los mensajes en la medida que cada elemento de la app se va ejecutando. Por ejemplo, podemos poner un message("filtrando datos") dentro de un reactive() cuando se use un input para un filtro, y así veremos un mensaje en la consola cada vez que se cambie el filtro.
Acceder a los valores de los inputs
Una de las dificultades más recurrentes es el acceder a los valores de input${x}.
Para esto existe la función browser(), cuya tarea es interrumpir la ejecución de R para inspeccionar el entorno en el que está inserto el browser(). Esto significa que podemos poner una línea con browser() dentro de un elemento reactivo como reactive(), observe() o render{x}(), y cuando ese elemento se ejecute dentro de la app, la app se detendrá y la consola estará dentro de la app. En este punto estás dentro de la app, así que puedes ver lo que hay dentro de input!
Por ejemplo, si ponemos browser() dentro de output$grafico_barras_region en la app de ejemplo que hemos trabajado, la ejecución se detiene y la consola permite ver los valores:
> runApp('app.R')
Listening on http://127.0.0.1:4917
Called from: renderPlot()
Browse[1]> input$region
[1] "Antofagasta"
Browse[1]> input$maximo
[1] 6
Browse[1]> input
<ReactiveValues>
Values: maximo, region
Readonly: TRUE
Esto significa que podemos probar el filtro antes de hacer el gráfico, o ir cambiando cosas del código y probándolas de inmediato. Es literal como entrar en la app a cambiar cosas mientras se ejecuta.
Pero en este método, los gráficos no se van a ver. Esto es porque R seguirá intentando mandar los gráficos al dispositivo (device) de Shiny, así que hay que ejecutar dev.new() en la consola para decirle a R que cree los gráficos en una ventanita nueva, y ahí sí.
Validar automáticamente tu app
Cuando desarrolles aplicaciones complejas, puede pasar que tengas la duda de si realmente todas las combinaciones de inputs e interacciones posibles van a funcionar correctamente. Existen formas de eliminar la ansiedad y confirmar que la app funciona bien, pero sin que tengas que estar revisando cada rincón de tu app cada vez que actualices los datos!
Con el paquete {shinytest2} puedes automatizar el testeo de aplicaciones Shiny, asegurando su correcto funcionamiento a través de capturas de pantalla y otras formas de validación automática.
Ver tutorial completo
Optimizar la velocidad de tu app
Cuando la aplicación esté operativa llega el momento de ir mejorándola. Una posibilidad de mejora es la optimización de los tiempos de carga.
En los ejemplos que vimos a lo largo del tutorial, los datos de la aplicación se cargan desde un archivo Excel, lo cual no es óptimo, ya que existen otros formatos de almacenamiento de datos que cargan la información mucho más rápido. Entonces podemos optimizar la carga de datos al guardar los datos en un formato más eficiente. Uno de estos formatos es .rds, formato nativo de R, o Arrow .parquet, un formato moderno y optimizado para datos columnares.
Veamos una comparación o benchmark de la eficiencia de carga de datos:
| expression | min | median | itr/sec | memoria |
|---|---|---|---|---|
| excel | 21.78ms | 24.02ms | 40.99647 | 1014.12KB |
| rds | 2.4ms | 2.74ms | 351.02840 | 135.86KB |
| parquet | 2.82ms | 3.48ms | 279.31071 | 7.96KB |
Ver código del benchmark
library(dplyr)
library(arrow)
library(readxl)
datos <- read_xlsx("datos.xlsx")
datos |> readr::write_rds("datos.rds")
datos |> arrow::write_parquet("datos.parquet")
bench::mark(
check = FALSE,
"excel" = readxl::read_xlsx("datos.xlsx"),
"rds" = readr::read_rds("datos.rds"),
"parquet" = arrow::read_parquet("datos.parquet")
)
Podemos ver que cargar una planilla de Excel es 10 veces más lento que .rds y también usa 10 veces más memoria. Por otro lado, .parquet usa una cantidad minúscula de memoria comparado con los otros métodos.
Así que este tipo de decisiones permiten mejorar la velocidad de la aplicación.
Otra forma de optimizar la aplicación es implementar cache de sus outputs y elementos, que significa que los resultados se guarden para que, cuando se vuelvan a realizar, se carguen en vez de volver a calcularse. Para esto puedes revisar el siguiente tutorial:
Ver tutorial completo
Otras mejoras para tu app
Aquí dejo otros tutoriales de Shiny para perfeccionar y mejorar tus aplicaciones!
Recursos
Otros materiales para seguir aprendiendo:
- Qué es Shiny y para qué sirve, del blog Atelier de código
- Shiny basics, tutorial oficial de Posit
- Mastering Shiny, libro de Hadley Wickham
- Conferencia de SHiny 2025, con videos de expositores y ponencias
- Ganadores del concurso 2024 de Shiny
- Shiny Assistant, herramienta de IA para prototipar apps
Más publicaciones sobre Shiny
Diapositivas y código de mi curso de Shiny
Espero que te sirva este tutorial! Aprender Shiny es todo un mundo que abre muchísimas posibilidades! Si encuentras cualquier error, o hay algo que no hayas entendido, por favor contáctame.
Si este tutorial te sirvió laboralmente, te agradecería una pequeña donación!