Gráficos interactivos con plotly
plotly: ggplot2 + interactividad
plotly añade tres cosas que ggplot2 solo no tiene:
- Zoom y pan sin escribir código adicional.
- Tooltips automáticos al hover.
- Eventos que se capturan en el server de Shiny (click, selección, etc.).
install.packages(c("plotly", "shiny"))
library(plotly)Es el complemento natural de ggplot2 en Shiny. La mayoría de gráficos que enseñas en una app deberían ser plotly, la interactividad es gratuita y los usuarios la esperan.
ggplotly(): conversión directa
library(ggplot2)
library(plotly)
g <- ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
geom_point(size = 3, alpha = 0.7) +
labs(color = "Cilindros")
ggplotly(g)Una línea. El gráfico aparece con todo:
- Zoom: arrastra para hacer zoom rectangular.
- Pan: shift + arrastrar para moverte.
- Doble click: resetea el zoom.
- Hover: tooltip con los valores.
- Leyenda interactiva: click en un nivel oculta/muestra ese grupo.
Funciona con casi cualquier geom de ggplot2. Algunos casos exóticos (geoms muy customizados, themes con tipografía rara) pueden renderizar con discrepancias menores, un 95 % de tus gráficos pasan sin problemas.
Personalizar el tooltip
Por defecto, el tooltip muestra todos los aesthetics mapeados. Para controlar qué se muestra:
g <- ggplot(mtcars, aes(wt, mpg,
color = factor(cyl),
text = paste0(
"Modelo: ", rownames(mtcars), "<br>",
"Peso: ", wt, "<br>",
"MPG: ", mpg
))) +
geom_point()
ggplotly(g, tooltip = "text")El aesthetic text (no estándar de ggplot2. Lo añades específicamente para plotly) controla qué aparece en el tooltip. tooltip = "text" le dice a plotly que solo use ese campo.
plot_ly() nativo: cuándo
Para casos donde ggplotly no te da lo que necesitas, plot_ly() es la API nativa:
plot_ly(mtcars,
x = ~wt,
y = ~mpg,
color = ~factor(cyl),
type = "scatter",
mode = "markers",
marker = list(size = 10))La sintaxis usa ~variable (formula) para mapear columnas, distinto a ggplot2. Útil cuando:
- Necesitas plots 3D (
type = "scatter3d"). - Necesitas tipos de gráfico que ggplot2 no tiene (
sunburst,treemap,sankey). - Necesitas control total sobre la interactividad y eventos.
Para el 80 % de los casos, ggplotly(ggplot(...)) es suficiente y aprovechas tu conocimiento de ggplot2.
Capturar eventos: plotlyOutput + event_data
Aquí está la magia que combina con Shiny. Cuando el usuario interactúa con el plot, plotly emite eventos que el server puede capturar:
library(shiny)
library(plotly)
ui <- fluidPage(
plotlyOutput("grafico"),
verbatimTextOutput("info")
)
server <- function(input, output, session) {
output$grafico <- renderPlotly({
plot_ly(mtcars, x = ~wt, y = ~mpg,
color = ~factor(cyl),
type = "scatter", mode = "markers",
source = "scatter_main")
})
output$info <- renderPrint({
click <- event_data("plotly_click", source = "scatter_main")
if (is.null(click)) {
cat("Haz click en un punto para ver los detalles")
} else {
cat("Punto seleccionado:\n")
print(click)
}
})
}
shinyApp(ui, server)Tres detalles importantes:
source = "id_unico"enplot_ly()te permite tener varios gráficos en la misma app y distinguir cuál disparó el evento.event_data("plotly_click", source = "...")devuelve los datos del evento. Es reactivo, al cambiar, recalcula los outputs que lo usan.- Si no hay evento aún (al cargar la app),
event_data()devuelveNULL. Maneja siempre el casoNULLconreq()o unif.
Eventos disponibles
| Evento | Cuándo |
|---|---|
"plotly_click" |
Click en un punto |
"plotly_hover" |
Hover sobre un punto |
"plotly_unhover" |
El cursor sale del punto |
"plotly_selected" |
Selección rectangular o lasso completada |
"plotly_selecting" |
Mientras el usuario está seleccionando |
"plotly_brushed" |
Brush (selección persistente, en algunos modos) |
"plotly_relayout" |
El usuario hace zoom o pan |
Patrón para selección rectangular:
ui <- fluidPage(
plotlyOutput("scatter"),
DTOutput("tabla_seleccion")
)
server <- function(input, output, session) {
output$scatter <- renderPlotly({
plot_ly(mtcars, x = ~wt, y = ~mpg,
type = "scatter", mode = "markers",
source = "main") |>
layout(dragmode = "select") # activar modo selección por defecto
})
output$tabla_seleccion <- renderDT({
sel <- event_data("plotly_selected", source = "main")
req(sel)
# sel$pointNumber contiene los índices de los puntos seleccionados
mtcars[sel$pointNumber + 1, ] # +1 porque plotly usa índice 0-based
})
}El usuario dibuja un rectángulo sobre el gráfico → la tabla de abajo muestra solo las filas seleccionadas. Es el patrón canónico de dashboards exploratorios.
Vinculación entre gráficos
Para que un click en un gráfico filtre otro, dos opciones:
1. Con Shiny + event_data (lo natural si ya tienes app Shiny):
server <- function(input, output, session) {
filtrados <- reactive({
sel <- event_data("plotly_selected", source = "grafico_1")
if (is.null(sel)) return(mtcars)
mtcars[sel$pointNumber + 1, ]
})
output$grafico_1 <- renderPlotly({ ... })
output$grafico_2 <- renderPlotly({
plot_ly(filtrados(), x = ~hp, y = ~qsec, type = "scatter", mode = "markers")
})
}Un reactive() que extrae la selección y los gráficos consumidores la usan.
2. Con crosstalk (sin server, útil para HTML estático):
library(crosstalk)
mt <- SharedData$new(mtcars)
bscols(
plot_ly(mt, x = ~wt, y = ~mpg, type = "scatter", mode = "markers"),
plot_ly(mt, x = ~hp, y = ~qsec, type = "scatter", mode = "markers")
)SharedData$new() crea un objeto compartido. Cuando el usuario selecciona en un widget, el otro se filtra automáticamente, todo en JavaScript, sin necesidad de server R corriendo.
crosstalk es lo que necesitas para dashboards estáticos en Quarto o R Markdown. Si ya estás en Shiny, el patrón con event_data es más flexible y suele ser mejor.
Customización visual
Para ajustar el aspecto del plotly:
ggplotly(g) |>
layout(
title = list(text = "Mi gráfico", x = 0.5),
xaxis = list(title = "Peso (1000 lb)"),
yaxis = list(title = "Millas por galón"),
plot_bgcolor = "#FAF9F6",
paper_bgcolor = "#FAF9F6",
font = list(family = "Inter, sans-serif")
) |>
config(displayModeBar = FALSE) # ocultar barra superiorlayout() controla el aspecto del gráfico. config() controla la barra de herramientas que aparece arriba al hacer hover. displayModeBar = FALSE la oculta, útil cuando el público no técnico no la entiende y solo ensucia la UI.
Trampas habituales
- Mezclar índices 0-based de plotly con 1-based de R.
event_data()$pointNumberempieza en 0. Para indexar en R, suma 1:mtcars[sel$pointNumber + 1, ]. Olvidar el+1te da la fila equivocada, sutil, difícil de depurar. - Olvidar
sourcecon múltiples gráficos. Sinsource, todos losevent_datareciben eventos de todos los plotly de la app. Empieza a ponersource = "id"desde el primer gráfico. - Manejar
event_datacomo si nunca fueraNULL. Al cargar la app, no hay evento todavía.event_data()devuelveNULL. Sinreq()o unif, el código que lo usa falla con error críptico. - Convertir ggplots con
ggplotly()cuando el theme tiene fuentes custom no estándar. El renderer de plotly no cargashowtextautomáticamente. Si has aplicado un theme con fuentes viashowtext, el plotly resultante mostrará la fuente por defecto. Para evitarlo, especifica las fuentes conlayout(font = ...)directamente en plotly.
En la siguiente entrega
Has aprendido a hacer gráficos y tablas interactivos. La siguiente entrega abre el Bloque 3 con validación de inputs y manejo de errores: cómo evitar que tu app muestre errores rojos al usuario y dar mensajes claros cuando algo no se puede calcular. Lo siguiente.