1 Plotly en R

El paquete Plotly en R surge como la adaptación de plotly.js, una librería desarrollada en JavaScript en 2012 con el objetivo de generar grÔficos interactivos basados en tecnologías web. En sus inicios fue una plataforma cerrada, pero a partir de 2015 se liberó su código bajo licencia MIT, lo que permitió su rÔpida expansión y la creación de interfaces en diferentes lenguajes como Python, Julia y R.

En el ecosistema de R, Plotly se integra mediante el paquete plotly, que utiliza la infraestructura de htmlwidgets. Esto le permite generar grƔficos dinƔmicos que pueden visualizarse tanto en RStudio como en entornos reproducibles como R Markdown y aplicaciones interactivas en Shiny.

La finalidad principal de este paquete es clara: convertir la visualización en una herramienta de exploración y no en un producto terminado. Mientras que librerías como ggplot2 o lattice generan grÔficos estÔticos muy estéticos, Plotly añade una capa extra al permitir acercar, desplazar, filtrar y obtener detalles al pasar el cursor sobre los datos.

De esta forma, Plotly representa una evolución natural en el flujo de trabajo de quienes ya utilizan grÔficos en R, pues incluso permite transformar un grÔfico de ggplot2 en uno interactivo con una sola línea de código mediante la función ggplotly().

2 LibrerĆ­a plotly

library(plotly)
## Cargando paquete requerido: ggplot2
## 
## Adjuntando el paquete: 'plotly'
## The following object is masked from 'package:ggplot2':
## 
##     last_plot
## The following object is masked from 'package:stats':
## 
##     filter
## The following object is masked from 'package:graphics':
## 
##     layout

2.1 Base de datos

La base de datos gapminder contiene información sobre el desarrollo socioeconómico y demogrÔfico de distintos países del mundo entre los años 1952 y 2007, en intervalos de cinco años.

Fue creada por la organización Gapminder Foundation, fundada por el estadístico sueco Hans Rosling, con el propósito de mostrar cómo el progreso global en salud y economía ha cambiado a lo largo del tiempo.

library(gapminder)
head(gapminder)
  • country: Nombre del paĆ­s.
  • continent: Continente al que pertenece el paĆ­s.
  • year: AƱo de observación, en intervalos de 5 aƱos, desde 1952 hasta 2007.
  • lifeExp: Esperanza de vida promedio al nacer, medida en aƱos.
  • pop: Población total del paĆ­s en ese aƱo.
  • gdpPercap: Producto Interno Bruto (PIB) per cĆ”pita en dólares internacionales ajustados por poder adquisitivo (PPP).

3 Estructura de la función plot_ly()

La función plot_ly() es la función principal del paquete Plotly en R.
Permite construir grƔficos interactivos especificando los datos, las variables que se asignan a los ejes y el tipo de grƔfico a generar.

plot_ly(
  data = gapminder,
  x = ~continent,        
  y = ~lifeExp,     
  type = "box"           
)
Categoría Valor de type Descripción
GrĆ”ficos bĆ”sicos (2D) scatter GrĆ”fico de dispersión o lĆ­neas. Admite modos como ā€œmarkersā€ o ā€œlinesā€.
bar GrƔfico de barras (vertical u horizontal).
box Diagrama de cajas que muestra mediana, cuartiles y outliers.
histogram Histograma para distribución de frecuencias.
violin GrÔfico de violín (densidad y distribución).
pie GrƔfico circular o de pastel.
funnel GrƔfico de embudo (etapas de un proceso).
GrƔficos geogrƔficos scattergeo Puntos o lƭneas en mapas geogrƔficos (latitud/longitud).
choropleth Mapa coloreado por regiones o paĆ­ses.
scattermapbox Puntos o lĆ­neas sobre mapas de Mapbox.
densitymapbox Mapa de densidad geogrƔfica.
scattergl Versión optimizada de dispersión para grandes volúmenes de datos (WebGL).
GrÔficos 3D scatter3d Dispersión tridimensional (x, y, z).
surface Superficie tridimensional generada por una matriz de valores.
mesh3d Malla tridimensional para formas o volĆŗmenes.
line3d LĆ­neas tridimensionales.
Calor y densidad heatmap Mapa de calor bidimensional (x, y, z).
contour GrƔfico de curvas de nivel o isovalores.
density Visualización de densidad de puntos.
JerƔrquicos y radiales sunburst Diagrama jerƔrquico radial tipo sol.
treemap Jerarquƭas en rectƔngulos anidados.
icicle Diagrama jerƔrquico descendente.
scatterpolar GrƔfico polar (coordenadas angulares y radiales).
Financieros candlestick GrƔfico de velas (precios de apertura, cierre, mƔximo, mƭnimo).
ohlc GrƔfico financiero Open-High-Low-Close.
waterfall GrÔfico de cascada (acumulación de ganancias/pérdidas).
Especiales parcoords Coordenadas paralelas para variables multivariadas.
sankey Diagrama de flujo entre categorĆ­as.
table Tabla interactiva.
indicator Indicadores numƩricos o KPIs (paneles de control).
plot_ly(
  data = gapminder,
  x = ~continent,         
  y = ~lifeExp,    
  type = "box",
  color = ~continent
) %>%
  layout(
    title = "Distribución de la esperanza de vida por continente",
    xaxis = list(title = "Continente"),
    yaxis = list(title = "Esperanza de vida (aƱos)")
  )

4 GrƔfico 3D

El paquete Plotly también permite representar funciones tridimensionales de forma interactiva mediante el parÔmetro type = "surface". Este tipo de visualización es especialmente útil para explorar superficies matemÔticas, modelos predictivos o relaciones no lineales entre variables.

x <- seq(-5, 5, length.out = 50)
y <- seq(-5, 5, length.out = 50)
z <- outer(x, y, function(a, b) a^2 + b^2)   # Paraboloide

# GrƔfico 3D de superficie
plot_ly(x = ~x, y = ~y, z = ~z, type = "surface",
        colorscale = "Viridis") %>%
  layout(title = "Superficie 3D del paraboloide (z = x² + y²)",
         scene = list(
           xaxis = list(title = "Eje X"),
           yaxis = list(title = "Eje Y"),
           zaxis = list(title = "Eje Z")
         ))

5 Mapa

Otra de las capacidades destacadas del paquete Plotly es la generación de mapas interactivos, los cuales permiten representar información geogrĆ”fica mediante visualizaciones dinĆ”micas. En este ejemplo se utiliza la función plot_ly() con el argumento type = ā€œchoroplethā€ para construir un mapa mundial que muestra la esperanza de vida por paĆ­s en el aƱo 2007, utilizando los datos del conjunto gapminder.

# Filtramos un aƱo (2007)
gap_map <- subset(gapminder, year == 2007)

plot_ly(
  data = gap_map,
  type = "choropleth",
  locations = ~country,       # Nombre del paĆ­s
  locationmode = "country names",  # Coincidir con nombres de paĆ­ses
  z = ~lifeExp,               # Variable a representar
  text = ~paste(country, "<br>Esperanza de vida:", round(lifeExp, 1)),
  colorscale = "Viridis",
  colorbar = list(title = "AƱos"),
  marker = list(line = list(color = "gray", width = 0.3))
) %>%
  layout(
    title = "Esperanza de vida por paĆ­s (2007)",
    geo = list(
      showframe = FALSE,
      showcoastlines = TRUE,
      projection = list(type = "natural earth")
    )
  )

6 Mapa de calor de correlaciones

El paquete Plotly también permite representar matrices de datos mediante grÔficos de calor, utilizando el argumento type = "heatmap". En este caso, se construyó un mapa de calor de correlaciones a partir de tres variables numéricas del conjunto gapminder: la esperanza de vida (lifeExp), la población (pop) y el PIB per cÔpita (gdpPercap).

# Seleccionamos variables numƩricas
gap_corr <- gapminder %>%
  select(lifeExp, pop, gdpPercap)

# Calculamos la matriz de correlación
mat_corr <- cor(gap_corr, method = "pearson")

# Visualización con plotly
plot_ly(
  x = colnames(mat_corr),
  y = rownames(mat_corr),
  z = mat_corr,
  type = "heatmap",
  colorscale = "Viridis",       # Escala de color
  reversescale = TRUE
) %>%
  layout(
    title = "Mapa de calor de correlaciones (Gapminder)",
    xaxis = list(title = ""),
    yaxis = list(title = "")
  )

7 Motor WebGL

Una de las innovaciones mÔs relevantes en Plotly es la incorporación de WebGL (Web Graphics Library), un motor de renderizado que aprovecha la aceleración grÔfica del navegador para trabajar con grandes volúmenes de datos de manera fluida.

En grÔficos tradicionales, representar mÔs de unos pocos miles de puntos puede ralentizar la visualización, dificultando el zoom, el desplazamiento o incluso la carga del grÔfico. Con WebGL, es posible manejar decenas de miles o incluso millones de observaciones sin pérdida de rendimiento, lo que resulta clave en contextos de Big Data o anÔlisis exploratorio de grandes bases de datos.

En R, esta capacidad se activa mediante el uso de trazas específicas como scattergl para diagramas de dispersión o heatmapgl para mapas de calor. El beneficio es inmediato: la experiencia interactiva se mantiene Ôgil aunque el número de observaciones sea muy alto.

El siguiente ejemplo muestra un diagrama de dispersión con 20,000 puntos, que gracias a WebGL conserva fluidez al aplicar acciones de zoom y desplazamiento:

library(plotly)

set.seed(42)
n <- 20000
df <- data.frame(
  x = rnorm(n),
  y = rnorm(n),
  grupo = sample(LETTERS[1:5], n, replace = TRUE)
)
library(plotly)

set.seed(123)
n <- 15000
df_comp <- data.frame(
  x = rnorm(n),
  y = rnorm(n),
  grupo = sample(letters[1:3], n, replace = TRUE)
)
# GrƔfico con scatter tradicional
p1 <- plot_ly(df_comp,
              x = ~x, y = ~y,
              type = "scatter", mode = "markers",
              color = ~grupo, colors = "Set1",
              marker = list(size = 4, opacity = 0.5)) %>%
  layout(title = "Scatter clƔsico (sin WebGL)")
p1
# GrƔfico con scattergl (WebGL)
p2 <- plot_ly(df_comp,
              x = ~x, y = ~y,
              type = "scattergl", mode = "markers",
              color = ~grupo, colors = "viridis",
              marker = list(size = 4, opacity = 0.5)) %>%
  layout(title = "Scatter con WebGL (mƔs fluido)")
p2

8 Interactividad y compatibilidad

8.1 Con ggplot2

Una ventaja de Plotly es su capacidad para convertir grÔficos de ggplot2 en versiones interactivas mediante la función ggplotly().

library(ggplot2)

p_ggplot <- ggplot(gapminder, aes(x = continent, y = lifeExp, fill = continent)) +
  geom_boxplot() +
  labs(title = "Esperanza de vida por continente") +
  theme_minimal()
p_ggplot

ggplotly(p_ggplot)

8.2 Con ggridges

Otra de las innovaciones recientes en Plotly es su soporte para librerĆ­as externas como ggridges, que permiten representar distribuciones superpuestas de manera compacta y visualmente atractiva.
En este tipo de grÔficos, cada categoría se representa con una densidad, lo que facilita comparar cómo se distribuyen los valores sin necesidad de recurrir a múltiples paneles.

library(ggridges)

# Seleccionamos un subconjunto para evitar sobrecarga visual
data_ridges <- subset(gapminder, year %in% c(1982, 2007))

# GrƔfico base con ggridges
p_ridges <- ggplot(data_ridges, aes(x = lifeExp, y = factor(year), fill = continent)) +
  geom_density_ridges(alpha = 0.8, scale = 1.5, color = "white") +
  labs(
    title = "Distribución de la esperanza de vida por año y continente",
    x = "Esperanza de vida",
    y = "AƱo"
  ) +
  theme_minimal()

p_ridges
## Picking joint bandwidth of 2.18

ggplotly(p_ridges)
## Picking joint bandwidth of 2.18

8.3 Con ggalluvial

Plotly también soporta la librería ggalluvial, que permite crear diagramas de flujo o diagramas aluviales. Estos grÔficos son ideales para representar cómo cambian los elementos de un grupo a otro a lo largo del tiempo o de distintas etapas. Por ejemplo, se pueden visualizar transiciones de estudiantes entre carreras universitarias o cambios de clientes entre categorías de productos.

library(ggalluvial)

# Datos que simulan estudiantes que cambian de carrera
datos_aluvial <- data.frame(
  AƱo1 = c(rep("Ingenierƭa", 30), rep("Medicina", 25), rep("Derecho", 20)),
  AƱo2 = c(rep("Ingenierƭa", 25), rep("Medicina", 5), 
           rep("Medicina", 20), rep("IngenierĆ­a", 5),
           rep("Derecho", 18), rep("Medicina", 2)),
  Frecuencia = 1
) %>%
  group_by(AƱo1, AƱo2) %>%
  summarise(Freq = sum(Frecuencia), .groups = "drop")

head(datos_aluvial)
# Diagrama aluvial
p_aluvial <- ggplot(datos_aluvial,
             aes(y = Freq, axis1 = AƱo1, axis2 = AƱo2)) +
  geom_alluvium(aes(fill = AƱo1), width = 1/12) +
  geom_stratum(width = 1/12, fill = "gray", color = "white") +
  geom_label(stat = "stratum", aes(label = after_stat(stratum))) +
  scale_x_discrete(limits = c("AƱo 1", "AƱo 2"), expand = c(.05, .05)) +
  labs(title = "Flujo de estudiantes entre carreras") +
  theme_minimal()
p_aluvial

ggplotly(p_aluvial)

8.4 Shinny

Plotly se ha consolidado como un aliado para la construcción de aplicaciones interactivas. En R, su integración con Shiny permite añadir controles dinÔmicos (filtros, menús, sliders) que modifican los grÔficos en tiempo real, sin necesidad de conocimientos avanzados de desarrollo web.

library(shiny)
library(dplyr)

# Datos
datos <- gapminder
datos$continent <- as.character(datos$continent)  

ui <- dashboardPage(
  dashboardHeader(title = "Dashboard Gapminder"),
  dashboardSidebar(
    sidebarMenu(
      menuItem("Visualizaciones", tabName = "viz", icon = icon("chart-bar")),
      
      
      sliderInput("year_sel", "Selecciona el aƱo:",
                  min = min(datos$year), max = max(datos$year),
                  value = 2007, step = 5, sep = ""),
      
     
      selectInput("cont_sel", "Selecciona continente:",
                  choices = c("Todos", unique(datos$continent)),
                  selected = "Todos")
    )
  ),
  dashboardBody(
    tabItems(
      tabItem(tabName = "viz",
              fluidRow(
                box(width = 12, plotlyOutput("bubble"))
              ),
              fluidRow(
                box(width = 6, plotlyOutput("top10")),
                box(width = 6, plotlyOutput("boxplot"))
              ),
              fluidRow(
                box(width = 12, plotlyOutput("line"))
              )
      )
    )
  )
)

server <- function(input, output, session) {
  
  # Datos filtrados por aƱo y continente
  datos_filtrados <- reactive({
    if (input$cont_sel == "Todos") {
      filter(datos, year == input$year_sel)
    } else {
      filter(datos, year == input$year_sel, continent == input$cont_sel)
    }
  })
  
  # GrƔfico de burbujas
  output$bubble <- renderPlotly({
    plot_ly(datos_filtrados(), x = ~gdpPercap, y = ~lifeExp,
            size = ~pop, color = ~continent, text = ~country,
            type = "scatter", mode = "markers",
            sizes = c(10, 70),
            marker = list(opacity = 0.7, line = list(width = 1))) %>%
      layout(title = paste("Esperanza de vida vs PIB per cƔpita -", input$year_sel),
             xaxis = list(title = "PIB per cƔpita"),
             yaxis = list(title = "Esperanza de vida"),
             legend = list(title = list(text = "Continente")))
  })
  
  # Top 10 paƭses mƔs poblados
  output$top10 <- renderPlotly({
    datos_top <- datos_filtrados() %>%
      arrange(desc(pop)) %>%
      slice_head(n = 10)
    
    plot_ly(datos_top, x = ~reorder(country, pop), y = ~pop,
            type = "bar", marker = list(color = "coral")) %>%
      layout(title = paste("Top 10 paƭses mƔs poblados -", input$year_sel,
                           ifelse(input$cont_sel == "Todos", "Global", input$cont_sel)),
             xaxis = list(title = "PaĆ­s", tickangle = -45),
             yaxis = list(title = "Población"))
  })
  
  # Boxplot
  output$boxplot <- renderPlotly({
    plot_ly(datos_filtrados(), x = ~continent, y = ~lifeExp,
            color = ~continent, type = "box") %>%
      layout(title = paste("Esperanza de vida por continente -", input$year_sel),
             xaxis = list(title = "Continente"),
             yaxis = list(title = "Esperanza de vida"))
  })
  
  # LĆ­nea temporal
  output$line <- renderPlotly({
    datos_linea <- if (input$cont_sel == "Todos") {
      datos %>% group_by(year) %>% summarise(promedio_vida = mean(lifeExp))
    } else {
      datos %>% filter(continent == input$cont_sel) %>%
        group_by(year) %>% summarise(promedio_vida = mean(lifeExp))
    }
    
    plot_ly(datos_linea, x = ~year, y = ~promedio_vida,
            type = "scatter", mode = "lines+markers",
            line = list(color = "steelblue", width = 3)) %>%
      layout(title = paste("Tendencia de la esperanza de vida -",
                           ifelse(input$cont_sel == "Todos", "Global", input$cont_sel)),
             xaxis = list(title = "AƱo"),
             yaxis = list(title = "Esperanza de vida promedio"))
  })
}

shinyApp(ui, server)

Link de la publicación en Shiny: https://g-geral.shinyapps.io/Plotly_con_Shiny/