6 Visualización dinámica

Fecha de compilación: 2026-10-13

Instalación y carga de librerías

if (!require(DT)) install.packages('DT')
library(DT)          # tablas interactivas (DataTables)
if (!require(pxR)) install.packages('pxR')
library(pxR)         # lectura de ficheros PC-Axis
if (!require(gifski)) install.packages('gifski')
library(gifski)      # renderizado de GIF (con gganimate)
if (!require(gganimate)) install.packages('gganimate')
library(gganimate)   # animaciones sobre ggplot2
if (!require(highcharter)) install.packages('highcharter')
library(highcharter) # gráficos interactivos (Highcharts)
if (!require(fpp3)) install.packages('fpp3')
library(fpp3)        # series temporales: tsibble, fable, feasts
if (!require(RColorBrewer)) install.packages('RColorBrewer')
library(RColorBrewer)# paletas de colores
if (!require(openxlsx)) install.packages('openxlsx')
library(openxlsx)    # lectura de Excel
if (!require(leaflet)) install.packages('leaflet')
library(leaflet)     # mapas interactivos
if (!require(sf)) install.packages('sf')
library(sf)          # datos geoespaciales, lectura de GeoJSON
if (!require(plotly)) install.packages('plotly')
library(plotly)      # gráficos interactivos (ggplotly)
if (!require(ggplot2)) install.packages('ggplot2')
library(ggplot2)     # gramática de gráficos
if (!require(tidyverse)) install.packages('tidyverse')
library(tidyverse)   # manipulación y visualización de datos
theme_set(theme_bw()) # tema de los gráficos, igual en todo el libro
source("../utilidades.R") # funciones auxiliares del curso
owid_country <- read.xlsx("https://ctim.es/AEDV/data/owid_country.xlsx",sheet=1) |> # países OWID
  as_tibble()

La visualización dinámica comprende dos modalidades principales: los gráficos interactivos, en los que el usuario puede explorar los datos pasando el cursor por encima de los elementos, filtrando, haciendo zoom o seleccionando subconjuntos; y las animaciones, en las que la representación evoluciona en el tiempo mostrando cómo cambian los datos. Ambas modalidades tienen un gran interés para presentar resultados en páginas web (ficheros HTML) o cuadros de mando, como se verá en el capítulo siguiente. Las tres librerías principales de este capítulo son las siguientes:

Librerías utilizadas en este capítulo

  • plotly: librería de gráficos interactivos para R, adaptada de la librería plotly.js de JavaScript. Destaca especialmente por su integración con ggplot2: la función ggplotly() permite convertir cualquier gráfico estático de ggplot2 en un gráfico interactivo con una sola línea de código.

  • highcharter: interfaz R para la biblioteca Highcharts.js, una de las librerías de gráficos en JavaScript más populares en el ámbito web, conocida por la calidad visual y la riqueza de opciones de sus gráficos interactivos. Atención a la licencia: Highcharts es gratuito solo para uso personal, educativo o sin ánimo de lucro; su uso comercial requiere una licencia de pago. Quien necesite una alternativa completamente libre con una filosofía similar puede usar echarts4r (interfaz R para Apache ECharts).

  • leaflet: interfaz R para la biblioteca Leaflet.js, especializada en la creación de mapas interactivos y en la visualización de información geográfica. Permite crear mapas que pueden desplazarse, hacer zoom y mostrar capas de datos de forma dinámica.

Las tres librerías comparten la ventaja de estar diseñadas para integrarse de forma directa en cuadros de mando, como se verá en el capítulo dedicado a Shiny y flexdashboard.

A ellas se añaden, para las animaciones y el resto de ejemplos del capítulo:

  • gganimate y gifski: las animaciones. La primera describe la animación como una capa más de ggplot2; la segunda es el motor que genera el GIF resultante.
  • DT: tablas interactivas, con búsqueda, ordenación y paginación.
  • sf: datos geoespaciales vectoriales (simple features), con los que se leen los ficheros GeoJSON que dibujan los mapas.
  • RColorBrewer: paletas de colores para las escalas de los mapas y gráficos.
  • fpp3, openxlsx, pxR, ggplot2 y tidyverse: series temporales, lectura de datos y manipulación, ya presentados en capítulos anteriores.

Cuándo usar visualización estática vs. dinámica

No siempre un gráfico interactivo es mejor que uno estático. La elección depende del contexto de uso:

  • Visualización estática (ggplot2): preferible para publicaciones impresas, informes PDF, artículos científicos y presentaciones donde el autor controla la narrativa. Los gráficos estáticos son reproducibles, ligeros y funcionan en cualquier formato de salida.

  • Visualización dinámica (plotly, highcharter, leaflet): preferible para páginas web, cuadros de mandos, documentos HTML y presentaciones interactivas donde el usuario explora los datos por sí mismo. Permite zoom, tooltips, filtros y animaciones.

Es importante tener en cuenta que los gráficos interactivos no se renderizan en formato WORD o PDF; además, generan ficheros HTML más pesados y pueden ser lentos con datasets grandes. Se puede diseñar primero el gráfico estático con ggplot2 y convertirlo a interactivo con ggplotly() cuando se necesite la interactividad; aunque hay que tener en cuenta que no todos los gráficos dinámicos de plotly se pueden generar con ggplotly().

Dos formas de construir un gráfico interactivo

A lo largo del capítulo alternaremos las dos vías que ofrece plotly, y conviene entender desde el principio en qué se diferencian:

  • ggplotly() recibe un gráfico de ggplot2 ya construido y lo convierte en interactivo. Es la forma más cómoda cuando el gráfico ya existe o cuando queremos reutilizar todo lo aprendido en el capítulo anterior: la gramática de capas, los temas, las escalas. Será nuestra opción por defecto.
  • plot_ly() construye el gráfico directamente en plotly, capa a capa, añadiendo cada elemento con funciones add_* (add_lines, add_ribbons, add_pie…). Se recurre a ella cuando el gráfico no se puede expresar bien en ggplot2 —como los circulares o los de donut— o cuando hay que combinar elementos que proceden de tablas distintas, como una serie observada y su predicción.

En ambos casos el resultado es el mismo tipo de objeto, que se puede seguir ajustando con layout() (títulos, ejes, leyenda).

6.1 Diagramas de barras interactivos

Usando ggplotly vamos a crear un gráfico interactivo con el mismo diagrama de barras de la sección 4.4. Como vemos, el gráfico se genera igual. Lo único que cambia es que el gráfico se almacena previamente en la variable p y después se dibuja con ggplotly. Observamos que cuando pasamos el ratón por encima de las barras nos aparece información adicional

p <- owid_country |>  # países OWID
  filter(!is.na(aged_65_older)) |> # solo los países con dato de edad
  mutate(mayores_65 = population*aged_65_older/100, # personas de 65 años o más
         menores_65 = population - mayores_65) |> # el resto de la población
  group_by(continent)  |> # agrupamos por continente
  summarise( # población de cada grupo de edad en el continente
    mayores_65=sum(mayores_65),
    menores_65=sum(menores_65)) |>
  pivot_longer(mayores_65:menores_65, names_to = "edad", values_to = "poblacion") |> # formato largo
  ggplot(aes(x=continent,y=poblacion,fill=edad)) + # color por grupo de edad
    geom_bar(stat = "identity") # barras apiladas
ggplotly(p) # versión interactiva

Figure 6.1: Diagrama de barras apilado usando geom_bar

6.2 Gráficos circulares interactivos

La librería plotly no solo sirve para hacer dinámicos gráficos generados con ggplot, plotly tiene sus propias funciones de visualización como plot_ly que usaremos para hacer un gráfico circular

owid_country |>  # países OWID
  group_by(continent)  |> # agrupamos por continente
  dplyr::summarise(population=sum(population)) |> # sumamos población
  plotly::plot_ly(
   labels = ~continent, # sectores = continentes
   values = ~population, # tamaño = población
   textinfo='label+percent',
   insidetextorientation='radial'
  ) |>
  add_pie() |> # gráfico circular
  hide_legend() # sin leyenda

Figure 6.2: Gráfico circular usando plot_ly

Cómo se nombran las opciones en plotly

Al pasar de ggplot2 a plot_ly() cambia la forma de declarar las opciones, y conviene entender la lógica en vez de memorizar recetas:

  • Las columnas de la tabla se referencian con una tilde delante: labels = ~continent significa “usa la columna continent”. Es el equivalente del mapeo estético de aes().
  • Las opciones de aspecto se agrupan en listas anidadas que reproducen la jerarquía de la figura. Por eso el título del eje horizontal se escribe layout(xaxis = list(title = "PIB")): layout contiene xaxis, que contiene title. Lo mismo con line = list(color = "red", width = 2) dentro de una capa.

Saber esto es lo que permite usar cualquier opción de la documentación de plotly —que está escrita como esa misma jerarquía de atributos— sin depender de encontrar un ejemplo idéntico al caso propio.

6.3 Gráficos de donuts interactivos

Vamos a hacer un gráfico de donuts con los mismos datos

owid_country |>  # países OWID
  group_by(continent)  |> # agrupamos por continente
  dplyr::summarise(population=sum(population)) |> # sumamos población
  plotly::plot_ly(
   labels = ~continent, # sectores = continentes
   values = ~population, # tamaño = población
   textinfo='label+percent',
   insidetextorientation='radial'
  ) |>
  add_pie(hole=0.4) |> # con agujero central
  hide_legend() # sin leyenda

Figure 6.3: Gráfico de donuts usando plot_ly

6.4 Diagramas de cajas interactivos

Usando ggplotly vamos a crear un gráfico interactivo con el mismo diagrama de cajas de la sección 4.8. Observamos que cuando pasamos el ratón por encima de los puntos nos aparece información adicional y si nos situamos a la derecha de una caja nos sale información estadística de la distribución de valores.

p <- owid_country |> # países OWID
  ggplot(aes(x=continent,y=gdp_per_capit,fill=continent,label=location)) + # PIB por continente
    geom_boxplot(outlier.shape = NA) + # cajas sin marcar outliers aparte
    geom_jitter(shape=16, position=position_jitter(0.2))+ # puntos con dispersión horizontal
    theme(legend.position = "none") # sin leyenda
ggplotly(p,tooltip=c("location","gdp_per_capit")) # solo estos campos en el tooltip

Figure 6.4: Diagrama de cajas interactivo usando ggplotly

Como observamos en el código anterior, con la opción tooltip podemos seleccionar los campos que aparecen en el desplegable. Además es posible formatear el contenido del desplegable. A continuación haremos el mismo gráfico cambiando el texto del desplegable (<br> es la instrucción en HTML para hacer un cambio de línea (retorno de carro)) :

p <- owid_country |> # países OWID
  ggplot(aes(x=continent,y=gdp_per_capit,fill=continent,
             text = paste('País: ', location, # texto del tooltip a medida
                          '<br>PIB per cápita: ', gdp_per_capit))
                    ) +
    geom_boxplot(outlier.shape = NA) + # cajas sin marcar outliers aparte
    geom_jitter(shape=16, position=position_jitter(0.2))+ # puntos con dispersión horizontal
    theme(legend.position = "none") # sin leyenda
ggplotly(p,tooltip=c("text")) # usamos el texto formateado

Figure 6.5: Formateo desplegable usando ggplotly

6.5 Diagramas de dispersión interactivos

Usando ggplotly vamos a crear un gráfico interactivo con el mismo diagrama de dispersión de la sección 4.9. Observamos que cuando pasamos el ratón por encima de los puntos nos aparece información adicional.

p <- owid_country |> # países OWID
  mutate(gdp_per_capit=round(gdp_per_capit)) |> # redondeamos para el tooltip
  ggplot(aes(x=gdp_per_capit,y=life_expectancy,color=continent,label=location)) + # color por continente
    geom_point() + # puntos
    scale_x_continuous(transform = 'log2')+ # eje x logarítmico
    theme(legend.position = "none") # sin leyenda
  ggplotly(p) # versión interactiva

Figure 6.6: Diagrama de puntos interactivo usando ggplotly

6.6 Combinando gráficos interactivos con subplot

La función subplot de plotly permite combinar varios gráficos interactivos en una sola visualización, de forma similar a lo que hace patchwork::wrap_plots con gráficos estáticos. Los gráficos pueden compartir ejes y tooltips:

p1 <- owid_country |>
  mutate(gdp_per_capit = round(gdp_per_capit)) |> # redondeamos para el tooltip
  ggplot(aes(x = gdp_per_capit, y = life_expectancy, color = continent, label = location)) + # PIB vs. esperanza de vida
    geom_point() +
    scale_x_continuous(transform = 'log2') +
    labs(title = "Esperanza de vida vs PIB") +
    theme(legend.position = "none")

p2 <- owid_country |>
  mutate(gdp_per_capit = round(gdp_per_capit)) |> # redondeamos para el tooltip
  ggplot(aes(x = gdp_per_capit, y = human_development_index, color = continent, label = location)) + # PIB vs. IDH
    geom_point() +
    scale_x_continuous(transform = 'log2') +
    labs(title = "IDH vs PIB") +
    theme(legend.position = "none")

subplot(ggplotly(p1), ggplotly(p2), nrows = 1, shareY = FALSE, titleX = TRUE) # los combinamos lado a lado

Figure 6.7: Combinación de dos gráficos interactivos usando subplot

La opción shareY = TRUE haría que ambos gráficos compartan el eje vertical, lo cual es útil cuando las variables son comparables. nrows controla la distribución en filas y columnas.

6.7 Gráficos interactivos con highcharter

La librería highcharter construye los gráficos con su función principal, hchart(), que sigue una lógica parecida a la de ggplot2: recibe la tabla, el tipo de gráfico ("column" para barras verticales, "bar" para horizontales, "line", "scatter", "pie"…) y, dentro de hcaes(), la correspondencia entre las columnas de la tabla y los elementos del gráfico, igual que aes(). El resto del gráfico se ajusta añadiendo funciones hc_* con el pipe: hc_title() para el título, hc_xAxis() y hc_yAxis() para los ejes, hc_tooltip() para el cuadro emergente, etc. Veamos el diagrama de barras de la población por continente:

owid_country |>  # países OWID
  group_by(continent) |> # agrupamos por continente
  summarise(population = sum(population)) |> # sumamos población
  hchart("column", hcaes(x = continent, y = population), name = "Población") |> # barras verticales
  hc_title(text = "Población por continente") |> # título
  hc_xAxis(title = list(text = "")) |> # sin título en el eje x
  hc_yAxis(title = list(text = "Habitantes")) # título del eje y

Figure 6.8: Diagrama de barras interactivo usando hchart

Con el argumento group de hcaes() se dibuja una serie por grupo, con su propio color y su entrada en la leyenda (un clic en ella oculta o muestra la serie). En el cuadro emergente podemos mostrar cualquier columna de la tabla escribiendo {point.columna}:

owid_country |>  # países OWID
  filter(!is.na(gdp_per_capit), !is.na(life_expectancy)) |> # países con los dos datos
  mutate(gdp_per_capit = round(gdp_per_capit)) |> # redondeamos para el cuadro emergente
  hchart("scatter", hcaes(x = gdp_per_capit, y = life_expectancy, group = continent)) |> # una serie por continente
  hc_title(text = "Esperanza de vida frente al PIB por habitante") |> # título
  hc_xAxis(type = "logarithmic", title = list(text = "PIB por habitante (escala logarítmica)")) |> # eje x logarítmico
  hc_yAxis(title = list(text = "Esperanza de vida (años)")) |> # título del eje y
  hc_tooltip(pointFormat = "{point.location}: {point.x} $, {point.y} años") # contenido del cuadro emergente

Figure 6.9: Diagrama de dispersión interactivo usando hchart

Además de tablas, hchart() sabe dibujar directamente muchos tipos de objetos de R, eligiendo por sí misma el gráfico adecuado: con un vector numérico dibuja su histograma, y con el resultado de un análisis de componentes principales (prcomp), su biplot, como veremos en el capítulo de análisis de atributos. También es la librería que usaremos en algunos de los cuadros de mando del capítulo siguiente.

6.8 Gráficos de línea interactivos

A partir de un informe del 2022 del Oak Ridge National Laboratory vamos a estudiar el consumo de combustible según el tipo de coche y cómo aumenta dicho consumo con la velocidad. El informe expresa tanto la velocidad como la eficiencia en unidades anglosajonas —millas por hora, y millas recorridas con un galón (3.785 litros)—, así que antes de dibujar nada las pasamos a las que usamos habitualmente: km/h para la velocidad y kilómetros por litro (km/L) para la eficiencia.

ConsumoVehiculos <- read.xlsx("https://ctim.es/AEDV/data/ConsumoVehiculos.xlsx",startRow = 6) |> # datos ORNL
  as_tibble()
str(ConsumoVehiculos) # estructura de la tabla
## tibble [4 × 8] (S3: tbl_df/tbl/data.frame)
##  $ speed               : num [1:4] 45 55 65 75
##  $ Gasoline.Midsize.Car: num [1:4] 43 45 38 32
##  $ Gasoline.Small.SUV  : num [1:4] 37 36 30 26
##  $ Gasoline.Large.SUV  : num [1:4] 35 31 29 25
##  $ Diesel.Midsize.Car  : num [1:4] 57 55 45 37
##  $ Diesel.Small.SUV    : num [1:4] 48 45 36 30
##  $ Diesel.Large.SUV    : num [1:4] 48 40 35 29
##  $ Hybrid.Midsize.Car  : num [1:4] 55 46 38 33
# 1 milla = 1.60934 km; 1 galón (US) = 3.78541 litros
p <- ConsumoVehiculos |>
  pivot_longer(Gasoline.Midsize.Car:Hybrid.Midsize.Car, names_to = "Car.Type", values_to = "Millas.Por.Galon") |> # formato largo
  mutate(`speed (km/h)` = speed * 1.60934, # a km/h
         `km/L` = Millas.Por.Galon * 1.60934 / 3.78541) |> # a km/L
  ggplot(aes(x=`speed (km/h)`,y=`km/L`,colour=Car.Type))+ # color por tipo de coche
  geom_line()+ # líneas
  geom_point()
ggplotly(p)

Figure 6.10: Gráfico de línea interactivo usando ggplotly

6.9 Mapas de calor interactivo

El gráfico de línea anterior muestra la tendencia, pero para comparar los vehículos entre sí a velocidades concretas conviene una tabla. Nos quedamos con tres de las velocidades del informe —120.7, 88.5 y 72.4 km/h (75, 55 y 45 mph)— y añadimos, entre cada par de velocidades consecutivas, la variación porcentual de los km/L al pasar de la más alta a la más baja: un valor positivo indica que el coche gana eficiencia al reducir la velocidad.

consumo_resumen <- ConsumoVehiculos |>
  pivot_longer(Gasoline.Midsize.Car:Hybrid.Midsize.Car,
               names_to = "Car.Type", values_to = "Millas.Por.Galon") |>
  mutate(`speed (km/h)` = round(speed * 1.60934, 1),
         Km.Por.Litro = round(Millas.Por.Galon * 1.60934 / 3.78541, 1)) |>
  filter(`speed (km/h)` %in% c(120.7, 88.5, 72.4)) |>
  select(Car.Type, `speed (km/h)`, Km.Por.Litro) |>
  pivot_wider(names_from = `speed (km/h)`, values_from = Km.Por.Litro,
              names_prefix = "kml_") |>
  transmute(
    # conservamos el orden del fichero original (gasolina, diésel, híbrido)
    Car.Type = factor(str_replace_all(Car.Type, "\\.", " "), levels = rev(unique(str_replace_all(Car.Type, "\\.", " ")))),
    `120.7 km/h` = kml_120.7,
    `88.5 km/h`  = kml_88.5,
    `Var. 88.5 vs 120.7 (%)` = round(100 * (kml_88.5 - kml_120.7) / kml_120.7, 1),
    `72.4 km/h`  = kml_72.4,
    `Var. 72.4 vs 88.5 (%)`  = round(100 * (kml_72.4 - kml_88.5) / kml_88.5, 1)
  )

consumo_resumen
## # A tibble: 7 × 6
##   Car.Type           `120.7 km/h` `88.5 km/h` Var. 88.5 vs 120.7 (…¹ `72.4 km/h`
##   <fct>                     <dbl>       <dbl>                  <dbl>       <dbl>
## 1 Gasoline Midsize …         13.6        19.1                   40.4        18.3
## 2 Gasoline Small SUV         11.1        15.3                   37.8        15.7
## 3 Gasoline Large SUV         10.6        13.2                   24.5        14.9
## 4 Diesel Midsize Car         15.7        23.4                   49          24.2
## 5 Diesel Small SUV           12.8        19.1                   49.2        20.4
## 6 Diesel Large SUV           12.3        17                     38.2        20.4
## 7 Hybrid Midsize Car         14          19.6                   40          23.4
## # ℹ abbreviated name: ¹​`Var. 88.5 vs 120.7 (%)`
## # ℹ 1 more variable: `Var. 72.4 vs 88.5 (%)` <dbl>

Las cinco columnas numéricas tienen escalas muy distintas —km/L frente a porcentajes—, así que normalizamos cada columna por separado entre su mínimo y su máximo antes de colorear; el cuadro emergente conserva el valor original, que es el que interesa leer:

p <- consumo_resumen |>
  pivot_longer(-Car.Type, names_to = "metrica", values_to = "valor") |>
  mutate(metrica = factor(metrica, levels = c(
    "120.7 km/h", "88.5 km/h", "Var. 88.5 vs 120.7 (%)",
    "72.4 km/h", "Var. 72.4 vs 88.5 (%)"))) |>
  group_by(metrica) |>
  mutate(valor_norm = (valor - min(valor)) / (max(valor) - min(valor))) |>
  ungroup() |>
  # las columnas "Var." son porcentajes; el resto, km/L
  mutate(unidad = if_else(str_starts(as.character(metrica), "Var"), "%", "km/L"),
         etiqueta = paste(valor, unidad)) |>
  ggplot(aes(metrica, Car.Type, fill = valor_norm)) +
    geom_tile(color = "grey80") +
    # texto blanco sobre las celdas oscuras, gris oscuro sobre las claras
    geom_text(aes(label = etiqueta, color = valor_norm > 0.5), size = 3, show.legend = FALSE) +
    scale_fill_gradient(low = "white", high = "steelblue") +
    scale_color_manual(values = c(`TRUE` = "white", `FALSE` = "grey20")) +
    labs(x = "", y = "") +
    theme(axis.text.x = element_text(angle = 30, hjust = 1), legend.position = "none")

ggplotly(p, tooltip = c("metrica", "Car.Type", "valor"))

Figure 6.11: Mapa de calor de la eficiencia (km/L) y su variación porcentual, por tipo de vehículo y velocidad

Conclusiones

  1. Los coches convencionales son significativamente más eficientes que los SUV en el consumo de combustible.

  2. En términos de consumo, los motores diésel son más eficientes que los de gasolina, aunque cuando se aumenta la velocidad su consumo tiende a aumentar más que en los de gasolina.

  3. Reducir la velocidad, en general, aumenta mucho la distancia que recorremos con la misma cantidad de combustible, con el consecuente ahorro de energía. Al pasar de 120,7 km/h a 88,5 km/h aumentamos la distancia recorrida entre un 24,5 % y un 49,2 % (lo cual es muy significativo). Al pasar de 88,5 km/h a 72,4 km/h la variación es menor, del −4,2 % al 20 %: algún modelo incluso pierde algo de eficiencia, y los coches grandes y los híbridos son los que más ganan.

  4. Aunque no se deduce de estos datos, que se midieron sin viento, conviene tener en cuenta que el viento puede afectar mucho al consumo, puesto que el rozamiento con el aire es lo que más contribuye a su aumento con la velocidad: lo que importa es la velocidad del coche respecto al aire, es decir, la suma de la velocidad del coche y la del viento en contra. Por ejemplo, con un viento en contra de unos 35 km/h, como el que es habitual en verano en algunas zonas de Gran Canaria, un automóvil a 85 km/h encuentra una resistencia del aire parecida a la que tendría a 120 km/h sin viento.

6.10 Series temporales interactivas y gráficos de área

Usando ggplotly vamos a crear un gráfico interactivo con series temporales de población como las de la sección 5.2, ahora para los seis continentes. Observamos que cuando pasamos el ratón por encima de las series temporales nos aparece información adicional

# Evolución de la población por continente
owid_population <- read_csv("https://ctim.es/AEDV/data/owid_population.csv") |>
  as_tibble() |>
  filter(Entity %in% c("North America","South America","Europe","Asia",
                      "Africa","Oceania")
)
p <- owid_population |>
  rename(Population=`Population (historical estimates)`) |> # nombre más corto
  ggplot(aes(Year,Population,colour=Entity)) + # color por continente
  geom_line() # líneas

ggplotly(p) |>
  layout(hovermode = "x unified") # un solo tooltip con todas las series del año

Figure 6.12: Gráfico interactivo de series temporales usando ggplotly

Para ver la evolución del porcentaje de población de cada continente sobre el total vamos a usar un gráfico de área. Para ello, después de calcular el porcentaje, utilizamos geom_col con barras de anchura 1, que quedan pegadas unas a otras y dan el aspecto de un área apilada. Lo natural sería usar geom_area, pero esta función no interactúa bien con ggplotly: el cuadro emergente no muestra bien los valores de cada año. Al apilar, las barras suman el 100 % de cada año, que es precisamente el caso en que un apilado tiene sentido:

p <- owid_population |>
  group_by(Year) |> # % relativo a cada año
  mutate(`Population Percentage` = 100*`Population (historical estimates)`/
           sum(`Population (historical estimates)`)) |>
  ggplot(aes(Year,`Population Percentage`,fill=Entity)) + # color por continente
  geom_col(width = 1) # barras apiladas y pegadas: aspecto de área

ggplotly(p) # versión interactiva

Figure 6.13: Gráfico interactivo de porcentajes usando ggplotly

Representación interactiva de una predicción

A continuación vamos a construir una gráfica interactiva con la predicción ARIMA de la llegada de pasajeros a los aeropuertos canarios. Primero leemos los datos:

meses_es <- c("Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio", # nombres de los meses del ISTAC,
              "Agosto","Septiembre","Octubre","Noviembre","Diciembre")  # para no depender del idioma del sistema
istac_aeropuertos <- read.px("https://ctim.es/AEDV/data/istac_aeropuertos.px") |> # datos del ISTAC
as_tibble() |>
filter(word(Periodos,2)!="TOTAL" & is.na(value)==FALSE & word(Periodos,1)!="2020") |> # sin totales, NA ni 2020
  mutate(date = make_yearmonth(year = as.integer(word(Periodos,1)), # fecha mensual: año...
                               month = match(word(Periodos,2), meses_es))) |> # ...y nº de mes
  select(date,pasajeros=value) |>
  as_tsibble(index=date) # tsibble mensual

La representación de una predicción es un caso donde conviene combinar elementos de naturaleza distinta y procedentes de tablas diferentes —la serie observada, el ajuste del modelo, la predicción y la banda del intervalo de confianza—: exactamente el tipo de gráfico para el que antes recomendábamos plot_ly() capa a capa. Pero el curso ya lo trae resuelto: las funciones aedv_dibujar_arima_dinamico() y aedv_dibujar_stl_arima_dinamico() de utilidades.R son las versiones interactivas de aedv_dibujar_arima() y aedv_dibujar_stl_arima(), que usamos en el capítulo anterior, con los mismos argumentos y el mismo aspecto. Ajustamos el modelo con aedv_prediccion_arima() para predecir dos años:

prediccion <- aedv_prediccion_arima(istac_aeropuertos, h = "2 years")

# En el tramo observado, "prediccion" es el ajuste del modelo y la banda tiene
# anchura nula; a partir del primer mes predicho, la banda se abre
prediccion |> tail(3)
## # A tsibble: 3 x 5 [1M]
##        date pasajeros prediccion   lower   upper
##       <mth>     <dbl>      <dbl>   <dbl>   <dbl>
## 1 2021 oct.        NA    412101. 338056. 486146.
## 2 2021 nov.        NA    349976. 274767. 425186.
## 3 2021 dic.        NA    393499. 317204. 469794.

El gráfico interactivo sale entonces de una sola llamada, que además dibuja el ajuste del modelo sobre el tramo observado —justo lo que un plot_ly() escrito a mano, capa a capa, es fácil olvidar—:

aedv_dibujar_arima_dinamico(prediccion, y_lab = "pasajeros")

Figure 6.14: Predicción interactiva ARIMA de la llegada de pasajeros a los aeropuertos canarios, con el ajuste del modelo y su intervalo de confianza al 95%

La interactividad aporta aquí algo que el gráfico estático no da: al pasar el cursor se leen en un mismo recuadro el valor observado, el ajuste y la predicción de cada mes, y se puede ampliar el tramo final arrastrando para ver con detalle cómo se abre la incertidumbre a medida que nos alejamos.

La otra función, aedv_dibujar_stl_arima_dinamico(), hace lo propio con el resultado de aedv_prediccion_stl_arima() y añade en naranja la tendencia de la descomposición STL, sólida en el tramo observado y discontinua en el extrapolado. No hay que pasarle nada: la tendencia viaja dentro del propio resultado (es lo que devuelve aedv_tendencia()) y la función la recoge sola, con el mismo criterio que la versión estática del capítulo anterior —en una serie sin estacionalidad no se dibuja, porque coincidiría con la predicción—:

prediccion_stl <- aedv_prediccion_stl_arima(istac_aeropuertos, h = 24) # 24 meses

aedv_dibujar_stl_arima_dinamico(prediccion_stl, y_lab = "pasajeros")

Figure 6.15: Predicción interactiva STL + ARIMA de la llegada de pasajeros a los aeropuertos canarios, con la tendencia de la descomposición en naranja

Aquí la interactividad se nota todavía más, porque hay más curvas superpuestas que leer: al situar el cursor sobre un mes observado el recuadro muestra a la vez el valor real, la reconstrucción de la descomposición y la tendencia, de modo que se puede comprobar mes a mes cuánto de la serie explica el movimiento de fondo y cuánto el ciclo estacional. Un clic en una entrada de la leyenda oculta esa curva, lo que permite quedarse solo con las dos o tres que interesan.

Predicción interactiva con intervalos no paramétricos

La lectura con el ratón resulta especialmente útil cuando la banda no es la del modelo, sino la que se estima con validación cruzada temporal, en la sección de predicción con intervalos de confianza no paramétricos: esa banda son los cuantiles empíricos del error a cada horizonte, de modo que puede ser asimétrica, y sus dos límites se comparan mucho mejor leyéndolos como números que estimándolos a ojo sobre el sombreado. Reproducimos la predicción a 12 meses del capítulo anterior, ahora en versión interactiva. Primero, la validación cruzada y la predicción entrenada con el último pliegue:

# validación cruzada temporal: pliegues de 120 meses y horizonte de 12 meses
cv_stl <- aedv_cv_stl_arima(istac_aeropuertos, h = 12, tamano_pliegue = 120)

# la predicción se entrena solo con el último pliegue, para ser coherente con la
# validación cruzada: periodos_atras = tamaño del pliegue
stl_12 <- aedv_prediccion_stl_arima(istac_aeropuertos, h = 12, periodos_atras = 120)

# se sustituye la banda del modelo por los cuantiles empíricos del error
stl_cv <- aedv_aplicar_bandas_cv(stl_12, cv_stl$bandas)

Y ahora el gráfico, con la misma llamada de antes:

aedv_dibujar_stl_arima_dinamico(stl_cv,y_lab  = "pasajeros")

Figure 6.16: Predicción interactiva STL + ARIMA de la llegada de pasajeros a 12 meses (entrenada con el último pliegue), con intervalo de confianza no paramétrico

Conviene fijarse en dos detalles que el gráfico interactivo deja comprobar de un vistazo. El primero es la asimetría de la banda: al pasar el cursor por los meses predichos se ve que los dos límites no están a la misma distancia de la predicción, algo que un intervalo paramétrico normal nunca produciría. El segundo es el efecto de periodos_atras: como el modelo solo ha visto los últimos 120 meses, el histórico anterior aparece en gris sin descomposición ni tendencia superpuestas, y su recuadro muestra únicamente el valor observado; sigue ahí como contexto, pero ningún modelo lo ha utilizado.

Rendimiento de los gráficos interactivos

Un gráfico interactivo guarda todos los datos dentro del fichero HTML, de modo que su tamaño crece con el número de elementos representados. Con muchos miles de puntos la página puede volverse pesada y lenta de abrir. En esos casos conviene:

  • Reducir el número de elementos representados, agregando los datos o tomando una muestra (slice_sample()).
  • Dibujar con la tarjeta gráfica: toWebGL() sobre el gráfico ya convertido, o directamente plot_ly(type = "scattergl"), que admite conjuntos mucho mayores que el tipo scatter estándar. La conversión con ggplotly() de un gráfico con muchos elementos (varios miles de puntos, un mapa de calor con muchas celdas) es justamente el caso en que esto se nota.
  • Reservar la interactividad para los gráficos donde de verdad aporte, y dejar los demás como gráficos estáticos.

Los mapas son, con diferencia, lo más pesado: cada polígono guarda todas sus coordenadas, así que un mapa con varios miles de municipios puede ocupar por sí solo varios megabytes. Conviene filtrar la zona de interés en lugar de dibujar el país entero, y recordar que este libro contiene decenas de gráficos interactivos: es la razón de que su fichero HTML sea grande.

6.11 Mapas interactivos con leaflet

Con frecuencia, los datos que manejamos están vinculados a localizaciones geográficas (países, municipios, etc.) o coordenadas GPS (latitud y longitud) y en la exploración de estos datos, resulta de gran interés posicionar los datos en un gráfico, de acuerdo con su localización geográfica.

¿Cómo se representa la información geográfica?

Usaremos dos formas principales (que se pueden combinar) de representar información geográfica:

  • Ficheros geojson: para leer estos ficheros usaremos la librería sf (simple features), el estándar actual en R para datos geoespaciales. Al leer un fichero geojson con sf se obtiene un objeto que es, esencialmente, un tibble con una columna especial (geometry) que contiene la geometría de cada región (polígono que delinea su contorno); por tanto se puede manipular directamente con las funciones de dplyr (filter, mutate, left_join, …) que ya conocemos. El flujo de trabajo habitual con este tipo de ficheros es el siguiente:
    • Previamente disponemos de los datos que queremos representar en el mapa.
    • Leemos la tabla de datos geoespaciales (fichero geojson) de las áreas geográficas asociadas a nuestros datos.
    • Combinamos, con un left_join, la tabla de datos geoespaciales con nuestros datos originales.
    • Creamos un vector de etiquetas HTML con la información que queremos que aparezca al pasar el ratón, en el mapa, por encima de un área geográfica.
    • Con toda esta información creamos el mapa.
  • Mapas OpenStreetMap: son mapas tipo Google Maps que se renderizan en tiempo real (hace falta conexión a internet) donde podemos situar etiquetas con la información que queramos usando sus coordenadas GPS. En este caso no hay que leer ningún fichero de datos geoespaciales, lo que sí necesitamos son las coordenadas GPS de las posiciones geográficas donde queramos poner las etiquetas. Es decir, si los datos originales son, por ciudades, necesitamos disponer de las coordenadas GPS de las ciudades.

Como los ficheros geojson del curso están en un servidor remoto, usamos la función auxiliar aedv_leer_geojson() del fichero utilidades.R, que primero descarga el fichero a un temporal y después lo lee con st_read de sf. La descarga previa es necesaria porque, en algunos sistemas Windows, st_read no puede acceder directamente a URLs https. Además, la función normaliza las coordenadas al sistema WGS84 (longitud/latitud), que es el que espera leaflet.

A continuación veremos algunos ejemplos de manejo de ficheros geojson para diferentes niveles de áreas geográficas:

Países del mundo

geoj <- aedv_leer_geojson("https://ctim.es/AEDV/data/geo_countries.geojson") # geometría de los países
geoj |> as_tibble()
## # A tibble: 255 × 3
##    ADMIN                ISO_A3                                          geometry
##    <chr>                <chr>                                 <MULTIPOLYGON [°]>
##  1 Aruba                ABW    (((-69.99694 12.57758, -70.05809 12.53718, -69.8…
##  2 Afghanistan          AFG    (((71.0498 38.40866, 70.97404 38.47367, 70.76109…
##  3 Angola               AGO    (((23.96746 -10.87231, 23.83372 -11.02847, 23.49…
##  4 Anguilla             AIA    (((-63.03767 18.21296, -62.97289 18.26927, -63.0…
##  5 Albania              ALB    (((20.06496 42.54676, 19.8196 42.46648, 19.60504…
##  6 Aland                ALA    (((19.91847 60.37018, 19.74757 60.10139, 20.0063…
##  7 Andorra              AND    (((1.707006 42.50278, 1.721993 42.60986, 1.46681…
##  8 United Arab Emirates ARE    (((56.27906 25.62745, 56.14513 25.67084, 56.1834…
##  9 Argentina            ARG    (((-64.34057 -54.716, -64.75866 -54.83229, -64.6…
## 10 Armenia              ARM    (((45.0024 41.29045, 44.61286 41.22312, 44.55152…
## # ℹ 245 more rows

Al pasar el ratón por encima de cada país vamos a mostrar su nombre y su código ISO de tres letras, construyendo para cada uno una pequeña etiqueta HTML (<b> pone el nombre en negrita y <br> hace un salto de línea):

etiquetas <- paste0("<b>", geoj$ADMIN, "</b><br>Código ISO: ", geoj$ISO_A3) |> # nombre y código ISO
  lapply(htmltools::HTML) # cada etiqueta se interpreta como HTML
geoj |>
  leaflet() |> # mapa base
  addPolygons(label = etiquetas,weight = 0.5) # un polígono por país

Figure 6.17: Visualización interactiva de los países del mundo usando leaflet

Datos de Canarias

geoj <- aedv_leer_geojson("https://ctim.es/AEDV/data/geo_canarias_municipios.geojson") # geometría de municipios
geoj |> as_tibble()|>
  head() |>
  knitr::kable()
geocode geopadre etiqueta notas granularidad gcd_provincia gcd_isla gcd_grancomarca gcd_comarca ign_sup ign_perim utm_x utm_y longitud latitud utm_x_capi utm_y_capi long_capi lati_capi geometry
35001 ES705A22 Agaete Ayuntamiento de Agaete MUNICIPIOS ES701 ES705 ES705A2 ES705A22 4451.16 41038 432237.3 3105496 -15.68965 28.07314 431360.3 3108388 -15.69874 28.09919 MULTIPOLYGON (((-15.74604 2…
35002 ES705A32 Agüimes Ayuntamiento de Agüimes MUNICIPIOS ES701 ES705 ES705A3 ES705A32 7889.72 55508 455417.3 3085974 -15.45300 27.89789 456117.6 3086640 -15.44591 27.90393 MULTIPOLYGON (((-15.3869 27…
35003 ES704A01 Antigua Ayuntamiento De Antigua MUNICIPIOS ES701 ES704 ES704A0 ES704A01 25024.80 79292 603336.9 3136733 -13.94558 28.35277 596564.7 3144503 -14.01402 28.42341 MULTIPOLYGON (((-13.91951 2…
35004 ES708A01 Arrecife Ayuntamiento De Arrecife MUNICIPIOS ES701 ES708 ES708A0 ES708A01 2427.54 36641 641157.8 3206510 -13.55112 28.97888 641613.1 3204339 -13.54672 28.95923 MULTIPOLYGON (((-13.58359 2…
35005 ES705A23 Artenara Ayuntamiento de Artenara MUNICIPIOS ES701 ES705 ES705A2 ES705A23 6641.90 50791 432859.4 3099393 -15.68297 28.01807 436452.0 3099620 -15.64644 28.02031 MULTIPOLYGON (((-15.74959 2…
35006 ES705A10 Arucas Ayuntamiento de Arucas MUNICIPIOS ES701 ES705 ES705A1 ES705A10 3283.87 38491 448820.8 3110505 -15.52109 28.11910 448399.3 3110440 -15.52538 28.11849 MULTIPOLYGON (((-15.55943 2…


Para cada municipio vamos a mostrar su nombre, su geocode, su latitud y su longitud:

etiquetas <-paste("<strong> Municipio: ",geoj$etiqueta ,"</strong><br>GEOCODE: ",geoj$geocode, # etiqueta HTML
                  "<br>longitud: ",geoj$longitud,
                  "<br>latitud: ",geoj$latitud) |>
  lapply(htmltools::HTML)
geoj |>
  leaflet() |> # mapa base
  addPolygons(label = etiquetas,weight = 0.5) # un polígono por municipio

Figure 6.18: Visualización interactiva de los municipios de Canarias usando leaflet

Datos de España

Los datos geográficos de España se han obtenido a través de GADM

Los ejemplos anteriores (países del mundo, municipios de Canarias) repiten el mismo patrón: leer la geometría, crear las etiquetas con htmltools::HTML y dibujar los polígonos con leaflet + addPolygons. Para no repetir ese código en los siguientes mapas, lo encapsulamos en una pequeña función auxiliar que reutilizaremos:

# Dibuja los polígonos de un objeto sf con sus etiquetas
mapa_poligonos <- function(geoj, etiquetas, weight = 0.5) {
  geoj |>
    leaflet() |>
    addPolygons(label = lapply(etiquetas, htmltools::HTML), weight = weight)
}

Autonomías

geoj <- aedv_leer_geojson("https://ctim.es/AEDV/data/ccaa.geojson") # geometría de las autonomías
geoj |> as_tibble()|>
  knitr::kable()
codauto iso2.ccaa.code nuts1.code nuts2.code ine.ccaa.name iso2.ccaa.name.es iso2.ccaa.name.ca iso2.ccaa.name.gl iso2.ccaa.name.eu nuts2.name cldr.ccaa.name.en cldr.ccaa.name.es cldr.ccaa.name.ca cldr.ccaa.name.ga cldr.ccaa.name.eu ccaa.shortname.en ccaa.shortname.es ccaa.shortname.ca ccaa.shortname.ga ccaa.shortname.eu nuts1.name geometry
01 ES-AN ES6 ES61 Andalucía Andalucía NA NA NA Andalucía Andalusia Andalucía Andalusia Andalucía Andaluzia Andalusia Andalucía Andalusia Andalucía Andaluzia SUR MULTIPOLYGON (((-4.275964 3…
02 ES-AR ES2 ES24 Aragón Aragón NA NA NA Aragón Aragon Aragón Aragó Aragón Aragoi Aragon Aragón Aragó Aragón Aragoi NORESTE MULTIPOLYGON (((-0.74093 42…
03 ES-AS ES1 ES12 Asturias, Principado de Asturias, Principado de NA NA NA Principado de Asturias Asturias Principado de Asturias Astúries Principado de Asturias Asturiesko Printzerria Asturias Asturias Astúries Asturias Asturias NOROESTE MULTIPOLYGON (((-4.536122 4…
04 ES-IB ES5 ES53 Balears, Illes Islas Baleares Illes Balears NA NA Illes Balears Balearic Islands Islas Baleares Illes Balears Illas Baleares - Illes Balears Balear Uharteak Balearic Islands Baleares Illes Balears Illas Baleares Balear Uharteak ESTE MULTIPOLYGON (((4.095952 40…
05 ES-CN ES7 ES70 Canarias Canarias NA NA NA Canarias Canary Islands NA NA NA NA Canary Islands Canarias Canàries Canarias Kanariak CANARIAS MULTIPOLYGON (((-8.504825 3…
06 ES-CB ES1 ES13 Cantabria Cantabria NA NA NA Cantabria Cantabria Cantabria Cantàbria Cantabria Kantabria Cantabria Cantabria Cantàbria Cantabria Kantabria NOROESTE MULTIPOLYGON (((-3.16291 43…
07 ES-CL ES4 ES41 Castilla y León Castilla y León NA NA NA Castilla y León Castile and León Castilla y León Castella i Lleó Castela e León Gaztela eta Leon Castile and León Castilla y León Castella i Lleó Castela e León Gaztela eta Leon CENTRO (ES) MULTIPOLYGON (((-4.743011 4…
08 ES-CM ES4 ES42 Castilla - La Mancha Castilla-La Mancha NA NA NA Castilla-La Mancha Castile-La Mancha Castilla-La Mancha Castella - la Manxa Castela-A Mancha Gaztela-Mantxa Castile-La Mancha Castilla-La Mancha Castella - la Manxa Castela-A Mancha Gaztela-Mantxa CENTRO (ES) MULTIPOLYGON (((-2.050606 4…
09 ES-CT ES5 ES51 Cataluña Cataluña Catalunya NA NA Cataluña Catalonia Cataluña Catalunya Cataluña Katalunia Catalonia Cataluña Catalunya Cataluña Katalunia ESTE MULTIPOLYGON (((0.844002 42…
10 ES-VC ES5 ES52 Comunitat Valenciana Valenciana, Comunidad Valenciana, Comunitat NA NA Comunidad Valenciana Valencian Community Comunidad Valenciana País Valencià Comunidade Valenciana Valentziako Erkidegoa Valencian Community Comunidad Valenciana País Valencià Comunidade Valenciana Valentziako Erkidegoa ESTE MULTIPOLYGON (((0.689104 39…
11 ES-EX ES4 ES43 Extremadura Extremadura NA NA NA Extremadura Extremadura Extremadura Extremadura Estremadura - Extremadura Extremadura Extremadura Extremadura Extremadura Extremadura Extremadura CENTRO (ES) MULTIPOLYGON (((-5.744424 4…
12 ES-GA ES1 ES11 Galicia Galicia NA Galicia NA Galicia Galicia Galicia Galícia Galicia Galizia Galicia Galicia Galícia Galicia Galizia NOROESTE MULTIPOLYGON (((-7.699081 4…
13 ES-MD ES3 ES30 Madrid, Comunidad de Madrid, Comunidad de NA NA NA Comunidad de Madrid Madrid Autonomous Community Comunidad de Madrid Comunitat de Madrid Comunidade de Madrid Madrilgo Erkidegoa Madrid Madrid Madrid Madrid Madril COMUNIDAD DE MADRID MULTIPOLYGON (((-3.076952 4…
14 ES-MC ES6 ES62 Murcia, Región de Murcia, Región de NA NA NA Región de Murcia Murcia Region Región de Murcia Regió de Múrcia Rexión de Murcia Murtziako Eskualdea Murcia Murcia Múrcia Murcia Murtzia SUR MULTIPOLYGON (((-1.04468 38…
15 ES-NC ES2 ES22 Navarra, Comunidad Foral de Navarra, Comunidad Foral de NA NA Nafarroako Foru Komunitatea Comunidad Foral de Navarra Navarra Chartered Community Navarra Navarra Navarra Nafarroako Foru Erkidegoa Navarra Navarra Navarra Navarra Nafarroa NORESTE MULTIPOLYGON (((-0.732351 4…
16 ES-PV ES2 ES21 País Vasco País Vasco NA NA Euskal Herria País Vasco Basque Country País Vasco Comunitat autònoma del País Basc País Vasco Euskal Autonomia Erkidegoa Basque Country País Vasco País Basc País Vasco Euskadi NORESTE MULTIPOLYGON (((-2.41998 43…
17 ES-RI ES2 ES23 Rioja, La La Rioja NA NA NA La Rioja La Rioja La Rioja La Rioja A Rioxa Errioxako Autonomia Erkidegoa La Rioja La Rioja La Rioja A Rioxa Errioxa NORESTE MULTIPOLYGON (((-2.864322 4…
18 ES-CE ES6 ES63 Ceuta Ceuta NA NA NA Ciudad Autónoma de Ceuta Ceuta Ceuta Ceuta Ceuta Ceuta Ceuta Ceuta Ceuta Ceuta Ceuta SUR MULTIPOLYGON (((-5.364975 3…
19 ES-ML ES6 ES64 Melilla Melilla NA NA NA Ciudad Autónoma de Melilla Melilla Melilla Melilla Melilla Melilla Melilla Melilla Melilla Melilla Melilla SUR MULTIPOLYGON (((-2.418081 3…


mapa_poligonos(geoj, geoj$ccaa.shortname.es) # nombre de la autonomía como etiqueta

Figure 6.19: Visualización interactiva de las autonomías de España usando leaflet

Provincias

Vamos a mostrar las provincias de Galicia:

geoj <- aedv_leer_geojson("https://ctim.es/AEDV/data/esp_prov.geojson") |> # geometría de las provincias
  filter(nuts2.name=="Galicia") # solo Galicia

geoj |> as_tibble()|>
  knitr::kable()
codauto cpro iso2.prov.code nuts.prov.code ine.prov.name iso2.prov.name.es iso2.prov.name.ca iso2.prov.name.ga iso2.prov.name.eu cldr.prov.name.en cldr.prov.name.es cldr.prov.name.ca cldr.prov.name.ga cldr.prov.name.eu prov.shortname.en prov.shortname.es prov.shortname.ca prov.shortname.ga prov.shortname.eu nuts2.code nuts2.name nuts1.code nuts1.name geometry
12 15 ES-C ES111 Coruña, A La Coruña NA A Coruña NA A Coruña Provincia de La Coruña Província de la Corunya Provincia da Coruña Coruñako probintzia A Coruña La Coruña La Corunya A Coruña Coruña ES11 Galicia ES1 NOROESTE MULTIPOLYGON (((-7.699081 4…
12 27 ES-LU ES112 Lugo Lugo NA Lugo NA Lugo Provincia de Lugo Província de Lugo Provincia de Lugo Lugoko probintzia Lugo Lugo Lugo Lugo Lugo ES11 Galicia ES1 NOROESTE MULTIPOLYGON (((-7.035698 4…
12 32 ES-OR ES113 Ourense Orense NA Ourense NA Ourense Provincia de Orense Província d’Ourense Provincia de Ourense Ourenseko probintzia Ourense Orense Ourense Ourense Ourense ES11 Galicia ES1 NOROESTE MULTIPOLYGON (((-7.944449 4…
12 36 ES-PO ES114 Pontevedra Pontevedra NA Pontevedra NA Pontevedra Provincia de Pontevedra Província de Pontevedra Provincia de Pontevedra Pontevedrako probintzia Pontevedra Pontevedra Pontevedra Pontevedra Pontevedra ES11 Galicia ES1 NOROESTE MULTIPOLYGON (((-8.012904 4…


mapa_poligonos(geoj, geoj$prov.shortname.es) # nombre de la provincia como etiqueta

Figure 6.20: Visualización interactiva de las provincias de Galicia usando leaflet

En este caso, se ha filtrado la tabla usando la función filter de dplyr para que solo se dibujen las provincias de Galicia. Nótese que, al ser el objeto sf una tabla, podemos usar sobre él las funciones de dplyr directamente.

Municipios

Vamos ahora a mostrar los municipios de Galicia leyendo todos los municipios de España y filtrando:

geoj <- aedv_leer_geojson("https://ctim.es/AEDV/data/esp_mun.geojson") |> # geometría de todos los municipios
  filter(ine.ccaa.name=="Galicia") # solo Galicia

geoj |> as_tibble()|>
  head() |> 
  knitr::kable()
codauto ine.ccaa.name cpro ine.prov.name cmun name LAU_CODE geometry
12 Galicia 15 Coruña, A 001 Abegondo 15001 MULTIPOLYGON (((-8.28432 43…
12 Galicia 15 Coruña, A 002 Ames 15002 MULTIPOLYGON (((-8.69223 42…
12 Galicia 15 Coruña, A 003 Aranga 15003 MULTIPOLYGON (((-8.09551 43…
12 Galicia 15 Coruña, A 004 Ares 15004 MULTIPOLYGON (((-8.19586 43…
12 Galicia 15 Coruña, A 005 Arteixo 15005 MULTIPOLYGON (((-8.46901 43…
12 Galicia 15 Coruña, A 006 Arzúa 15006 MULTIPOLYGON (((-8.21491 42…


mapa_poligonos(geoj, geoj$name) # nombre del municipio como etiqueta

Figure 6.21: Visualización interactiva de los municipios de Galicia usando leaflet

6.12 Mapas con OpenStreetMap

Leaflet utiliza, por defecto, los mapas diseñados por OpenStreetMap para crear mapas. En primer lugar vamos a crear un mapa con las Islas Canarias donde aparezca una etiqueta en la posición donde se encuentran los municipios. Para ello vamos a leer los datos geográficos de los municipios canarios:

geoj <- aedv_leer_geojson("https://ctim.es/AEDV/data/geo_canarias_municipios.geojson") # geometría de municipios
geoj_ti <- geoj |>
  as_tibble() |> # quitamos la geometría, solo nos interesan las coordenadas
  rename(nombre=etiqueta) |>
  select(nombre,geocode,latitud,longitud)
geoj_ti |>
  print(n=3)
## # A tibble: 88 × 4
##   nombre  geocode latitud longitud
##   <chr>   <chr>     <dbl>    <dbl>
## 1 Agaete  35001      28.1    -15.7
## 2 Agüimes 35002      27.9    -15.5
## 3 Antigua 35003      28.4    -13.9
## # ℹ 85 more rows

En el tibble geoj_ti hemos seleccionado el nombre, el código del municipio, la longitud y la latitud de los municipios que usaremos ahora para crear el mapa:

etiquetas <- paste("<strong>",geoj_ti$nombre,"</strong>") |>
  lapply(htmltools::HTML)
geoj |>
  leaflet() |>
  addTiles() |> # mapa base OpenStreetMap
  addMarkers(
    geoj_ti$longitud,
    geoj_ti$latitud,
    label = etiquetas,
    clusterOptions = markerClusterOptions() # agrupa marcadores cercanos al alejar el zoom
  )

Figure 6.22: Mapa interactivo con marcadores en la localización de los municipios canarios

A continuación vamos a crear un mapa con círculos cuya área sea proporcional a la superficie de los municipios canarios. Como la superficie no está en el objeto geométrico geoj, tenemos que buscarla en otra tabla y vincularla. Ambas tablas comparten el código INE del municipio —geocode en el geojson e ine_code en la tabla del ISTAC—, así que la combinación puede hacerse por código y es exacta. Lo importante es dónde guardamos el resultado: unimos los datos al propio objeto geoj, de modo que la geometría y sus datos viajen juntos en una sola tabla.

# Leemos un fichero que contiene la superficie y el código INE de los municipios
istac_municipios <- read.xlsx("https://ctim.es/AEDV/data/istac_municipios_ine_codes.xlsx") |>
  as_tibble()
istac_municipios |>
  print(n=3)
## # A tibble: 88 × 6
##   ine_code nombre  nombre2 nombre3 isla         superficie
##   <chr>    <chr>   <chr>   <chr>   <chr>             <dbl>
## 1 38001    Adeje   Adeje   Adeje   Tenerife          106. 
## 2 35001    Agaete  Agaete  Agaete  Gran Canaria       45.5
## 3 35002    Agüimes Agüimes Agüimes Gran Canaria       79.3
## # ℹ 85 more rows
# Unimos la superficie a la GEOMETRÍA usando el código del INE, presente en las dos
# tablas. Un objeto sf es una tabla, así que left_join funciona sobre él directamente
geoj <- geoj |>
  left_join(istac_municipios |> select(ine_code, nombre3, superficie),
            by = c("geocode" = "ine_code"))

# Comprobamos que ningún municipio se ha quedado sin superficie
sum(is.na(geoj$superficie))
## [1] 0

Ahora creamos el mapa con los círculos, usando ~nombre3 —el nombre corto del municipio— en las etiquetas. El radio del círculo se calcula con la raíz cuadrada de la superficie, porque lo que el ojo compara es el área del círculo, que crece con el cuadrado del radio: sin la raíz, un municipio con el doble de superficie se dibujaría cuatro veces más grande.

etiquetas <- paste("<strong>",geoj$nombre3,"</strong><br/>",geoj$superficie," km²") |>
  lapply(htmltools::HTML)
geoj |>
  leaflet() |>
  addTiles() |> # mapa base
  addCircles(
    lng = ~longitud,
    lat = ~latitud,
    weight = 1,
    radius = ~sqrt(superficie) * 300, # área proporcional a la superficie
    label= etiquetas
  )

Figure 6.23: Mapa interactivo con círculos proporcionales a la superficie de los municipios canarios

Ahora creamos un mapa, centrado en Gran Canaria, donde aparecen los límites de cada municipio combinado con el mapa. Añadimos addMiniMap() para que el usuario pueda orientarse geográficamente cuando hace zoom en una zona concreta:

etiquetas <- paste("<strong>",geoj$nombre3,"</strong><br/>",geoj$superficie," km²") |>
  lapply(htmltools::HTML)
geoj |>
  leaflet() |>
  setView(lng = -15.6, lat = 27.95, zoom = 10) |> # centrado en Gran Canaria
  addTiles() |> # mapa base
  addPolygons(
    color = "blue", weight = 2, dashArray = "3", fillOpacity = 0., # solo el borde, sin relleno
    highlightOptions = highlightOptions(
      color = "black", weight = 4, dashArray = "", fillOpacity = 0., bringToFront = TRUE # resalte al pasar el ratón
    ),
    label = etiquetas
  ) |>
  addMiniMap(position = "bottomright", width = 120, height = 120) # mapa de referencia

Figure 6.24: Visualización interactiva del mapa con los límites dibujados de los municipios de Canarias usando leaflet

Control de capas en mapas

Cuando un mapa tiene múltiples capas de información (polígonos, marcadores, círculos), la función addLayersControl permite al usuario activar o desactivar capas interactivamente:

etiquetas_poligonos <- paste("<strong>",geoj$nombre3,"</strong>") |>
  lapply(htmltools::HTML)
etiquetas_marcadores <- paste("<strong>",geoj_ti$nombre,"</strong>") |>
  lapply(htmltools::HTML)

geoj |>
  leaflet() |>
  addTiles() |>
  addPolygons( # capa de límites municipales
    group = "Límites municipales",
    color = "blue", weight = 1, fillOpacity = 0.1,
    label = etiquetas_poligonos
  ) |>
  addMarkers( # capa de marcadores
    geoj_ti$longitud, geoj_ti$latitud,
    group = "Marcadores",
    label = etiquetas_marcadores,
    clusterOptions = markerClusterOptions()
  ) |>
  addLayersControl( # control para activar/desactivar cada capa
    overlayGroups = c("Límites municipales", "Marcadores"),
    options = layersControlOptions(collapsed = FALSE)
  )

Figure 6.25: Mapa con control de capas usando addLayersControl

6.13 Mapas coropléticos

Un mapa coroplético (choropleth map) es un gráfico dividido en zonas (por ejemplo áreas geográficas) en el que cada zona se colorea de forma distinta en función del valor de una variable estadística. Para ilustrar este concepto dibujaremos un gráfico con los países del mundo, utilizando como variable estadística su PIB por habitante. En primer lugar, leemos la información geográfica:

geoj <- aedv_leer_geojson("https://ctim.es/AEDV/data/geo_countries.geojson") # geometría de los países
geoj |> as_tibble()
## # A tibble: 255 × 3
##    ADMIN                ISO_A3                                          geometry
##    <chr>                <chr>                                 <MULTIPOLYGON [°]>
##  1 Aruba                ABW    (((-69.99694 12.57758, -70.05809 12.53718, -69.8…
##  2 Afghanistan          AFG    (((71.0498 38.40866, 70.97404 38.47367, 70.76109…
##  3 Angola               AGO    (((23.96746 -10.87231, 23.83372 -11.02847, 23.49…
##  4 Anguilla             AIA    (((-63.03767 18.21296, -62.97289 18.26927, -63.0…
##  5 Albania              ALB    (((20.06496 42.54676, 19.8196 42.46648, 19.60504…
##  6 Aland                ALA    (((19.91847 60.37018, 19.74757 60.10139, 20.0063…
##  7 Andorra              AND    (((1.707006 42.50278, 1.721993 42.60986, 1.46681…
##  8 United Arab Emirates ARE    (((56.27906 25.62745, 56.14513 25.67084, 56.1834…
##  9 Argentina            ARG    (((-64.34057 -54.716, -64.75866 -54.83229, -64.6…
## 10 Armenia              ARM    (((45.0024 41.29045, 44.61286 41.22312, 44.55152…
## # ℹ 245 more rows

A continuación, como el dato de PIB por habitante no está en esta tabla, lo tomaremos de owid_country y lo uniremos a la geometría con un left_join usando como campo común el código ISO del país (que tiene diferente nombre de campo en las dos tablas):

geoj <- geoj |>
  left_join(owid_country |> select(iso_code, gdp_per_capit), # unimos el PIB per cápita
            by = c("ISO_A3" = "iso_code"))

sum(is.na(geoj$gdp_per_capit)) # países sin dato de PIB
## [1] 61

A continuación generamos el gráfico interactivo y después explicaremos los elementos que aparecen nuevos:

etiquetas <-paste("<strong> ",geoj$ADMIN ,"</strong><br>PIB/CAPITA: ",round(geoj$gdp_per_capit)) |>
  lapply(htmltools::HTML)
pal <- colorQuantile("YlOrRd", geoj$gdp_per_capit, n = 9) # paleta por percentiles
geoj |>
  leaflet() |>
  setView(lng = 5, lat = 22, zoom = 2) |> # vista inicial del mapa
  addPolygons(
    fillColor = ~pal(gdp_per_capit), # color según el PIB
    weight = 2, opacity = 1, color = "white", dashArray = "3", fillOpacity = 0.7,
    highlightOptions = highlightOptions(
      weight = 2, color = rgb(0.2,0.2,0.2), dashArray = "", fillOpacity = 0.7, bringToFront = TRUE # resalte
    ),
    label = etiquetas
  ) |>
  addLegend("bottomleft", pal = pal, values = ~gdp_per_capit, # leyenda de colores
    title = "PIB/habit.",
    labFormat = function(type, cuts, p) { # rango de valores de cada tramo
          n = length(cuts)
          x = format(round(cuts), big.mark = ".", trim = TRUE) # miles separados con punto
          paste(x[-n], "–", x[-1])
        },
    opacity = 1
  )
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' y 'decimal.mark' son ambos '.', lo cual puede ser confuso

Figure 6.26: Visualización interactiva del PIB por habitante en los países del mundo

La instrucción pal <- colorQuantile("YlOrRd", geoj$gdp_per_capit, n = 9) genera una tabla de colores asociando los percentiles de los valores del PIB por habitante con la tabla de colores “YlOrRd”. La instrucción fillColor = ~pal(gdp_per_capit) utiliza esa tabla de colores para asignar un color a cada país: al escribirse como fórmula, leaflet busca la columna en la propia tabla del mapa, que es lo que garantiza que cada país reciba su valor. La instrucción highlightOptions = ... produce que cuando el ratón se sitúa por encima de un país, se destaca el país para que se vea más claro el contorno del país seleccionado. La instrucción addLegend(...) posiciona una leyenda en la parte inferior izquierda del gráfico con los colores de la paleta y el rango de valores del PIB por habitante que corresponde a cada color (con labFormat escribimos el rango, en lugar del percentil que leaflet muestra por defecto en una paleta de cuantiles).

Estas piezas —escala por cuantiles, resalte, etiquetas emergentes y leyenda— son siempre las mismas, cambie el indicador que cambie. Por eso están empaquetadas en aedv_mapa_coropletico, del fichero utilidades.R, que produce el mapa anterior en una sola instrucción:

aedv_mapa_coropletico(geoj, geoj$gdp_per_capit, etiquetas, "PIB/habit.", # mismo mapa en una instrucción
                      lng = 5, lat = 22, zoom = 2)

Figure 6.27: El mismo mapa coroplético, construido con aedv_mapa_coropletico

Recibe el objeto geográfico, los valores que dan el color, las etiquetas y el título de la leyenda; los tres últimos argumentos fijan el encuadre inicial del mapa. Los valores se le pasan como vector, y por eso conviene tomarlos de la columna del propio objeto (geoj$gdp_per_capit): así se cumple la condición del bloque anterior sin tener que pensarlo. Es la función que usaremos en los cuadros de mando del capítulo siguiente, donde el indicador que se colorea lo elige el usuario. El atajo no exime de entender el código anterior: en cuanto haya que salirse de la escala por cuantiles —como en el mapa siguiente— hay que volver al patrón completo.

Mapa coroplético de España por comunidades autónomas

Para completar los ejemplos de mapas coropléticos, a continuación creamos uno de España coloreado por el salario medio bruto mensual obtenido del INE:

Aquí el INE nos hace un favor: antepone al nombre de la comunidad su código oficial (“01 Andalucía”), que es exactamente el que el geojson guarda en la columna codauto. Separando ese código con substr podemos unir las dos tablas de forma exacta, sin recurrir a los nombres:

# Leemos datos del INE de salario medio por CCAA. El nombre viene precedido de su
# código oficial ("01 Andalucía"): separamos el código y el nombre en dos columnas
salarios_ccaa <- read.px('https://ctim.es/AEDV/data/INE.SUELDOS.COMUNIDADES.px') |>
  as_tibble() |>
  filter(Tipo.de.jornada == "Total", Decil == "Total decil", Periodo == 2022) |>
  mutate(codigo = substr(Comunidades.y.Ciudades.Autonómas, 1, 2),
         ccaa   = substr(Comunidades.y.Ciudades.Autonómas, 4, 100)) |>
  select(codigo, ccaa, salario = value)

salarios_ccaa |>
  print(n = 3)
## # A tibble: 20 × 3
##   codigo ccaa        salario
##   <chr>  <chr>         <dbl>
## 1 To     al Nacional   2128.
## 2 01     Andalucía     1915.
## 3 02     Aragón        2030.
## # ℹ 17 more rows
# Leemos el geojson de las CCAA y le unimos el salario por el código oficial
geoj_ccaa <- aedv_leer_geojson("https://ctim.es/AEDV/data/ccaa.geojson") |>
  left_join(salarios_ccaa |> select(codigo, salario),
            by = c("codauto" = "codigo"))

# Comunidades sin salario asignado
sum(is.na(geoj_ccaa$salario))
## [1] 0
etiquetas <- paste("<strong>", geoj_ccaa$ccaa.shortname.es,
                   "</strong><br>Salario medio: ", round(geoj_ccaa$salario), "€") |>
  lapply(htmltools::HTML)

pal_ccaa <- colorNumeric("YlOrRd", geoj_ccaa$salario) # escala lineal, no por cuantiles

geoj_ccaa |>
  leaflet() |>
  setView(lng = -3.7, lat = 40.0, zoom = 5) |> # vista inicial centrada en España
  addPolygons(
    fillColor = ~pal_ccaa(salario), # color según el salario
    weight = 1, color = "grey50", fillOpacity = 0.7,
    highlightOptions = highlightOptions(
      weight = 3, color = "black", fillOpacity = 0.85, bringToFront = TRUE # resalte
    ),
    label = etiquetas
  ) |>
  addLegend("bottomright", pal = pal_ccaa, values = ~salario, # leyenda de colores
            title = "Salario medio (€)", opacity = 0.8)

Figure 6.28: Mapa coroplético del salario medio bruto mensual por comunidades autónomas en España (2022)

Nótese que aquí la escala de color es lineal (colorNumeric) y no por cuantiles: los salarios medios de las comunidades se mueven en un rango estrecho, sin valores extremos que aplasten la escala. La elección entre una escala lineal y una por cuantiles depende, por tanto, de cómo se repartan los datos —conviene mirar antes su histograma—: el PIB por habitante del mapa anterior está tan concentrado en los valores bajos que una escala lineal dejaría casi todos los países del mismo tono.

6.14 Gráficos animados

Los gráficos animados son una herramienta muy potente de visualización que permite ver cómo evolucionan los datos en un gráfico. La librería plotly permite hacer animaciones interactivas. Veremos a continuación un diagrama de barras animado sobre la población, a lo largo de los años, por sexos y nacionalidad en las comunidades autónomas en España usando datos del INE:

INE.poblacion <- readRDS(url("http://ctim.es/AEDV/data/INE.poblacion.rds")) |> # datos del INE
  as_tibble()
INE.poblacion |>
  filter(Sexo!="Total",Nacionalidad!="Total",Comunidad!="Total Nacional") |> # sin totales
  mutate(`Nacionalidad/Sexo`=paste0(Nacionalidad,"/",Sexo))  |> # atributo combinado
  mutate(Comunidad=factor(Comunidad,levels=unique(Comunidad))) |> # orden fijo
  plot_ly(
    x = ~value,
    y = ~Comunidad,
    color = ~`Nacionalidad/Sexo`,
    frame = ~Periodo, # variable de animación
    type = "bar",
    # con 19 comunidades x 4 barras agrupadas por cada una, la altura por
    # omisión (480 px) deja cada barra en unos pocos píxeles: se amplía aquí,
    # en plot_ly() y no en layout(), porque solo así crece también el
    # contenedor HTML del widget -si no, con layout(height=...) el eje x y
    # los controles de la animación quedaban recortados por debajo de los
    # 480 px del contenedor, que no había cambiado de tamaño-
    height = 900
  ) |>
  layout(barmode = "group", bargap = 0.15) # barras agrupadas, no apiladas

Figure 6.29: Gráfico de barras animado con la evolución de la población en las CCAA

Hay librerías específicas como gganimate para hacer gráficos animados. Veremos cómo usarla para ver cómo evoluciona el porcentaje que representa la población extranjera sobre el total de la población en España a lo largo de los años por sexos. Esta librería genera una imagen animada en formato GIF que inserta en el documento. Tiene la ventaja de que resulta muy sencillo de usar a partir de ggplot, pero no puede hacer cosas más complejas como añadir un slider para interactuar con la animación.

p <-INE.poblacion |>
  filter(Comunidad=="Total Nacional",Nacionalidad!="Total") |> # total nacional, sin fila "Total"
  group_by(Periodo,Sexo) |> # % relativo a cada año y sexo
  mutate(Porcentaje.Población.Extranjera =100*value/sum(value)) |>
  ungroup() |>
  filter(Nacionalidad=="Extranjera") |> # nos quedamos con el % de extranjeros
  ggplot(aes(x=Periodo,y=Porcentaje.Población.Extranjera,colour=Sexo)) + # color por sexo
  geom_line() + # línea
  scale_colour_brewer(palette = "Set1") + # paleta cualitativa
  labs(x="") +
  transition_reveal(Periodo) # anima revelando año a año
animate(p,fps = 10,end_pause = 40)
Gráfico de líneas animado con la evolución del porcentaje de población extranjera en España por sexos

Figure 6.30: Gráfico de líneas animado con la evolución del porcentaje de población extranjera en España por sexos

Otras transiciones de gganimate

Además de transition_reveal (que revela la serie progresivamente), gganimate ofrece otras transiciones útiles:

  • transition_states(variable): transiciona entre diferentes valores de una variable categórica, útil para comparar distribuciones por categoría.
  • shadow_mark(): deja un rastro semitransparente de los estados anteriores, lo que permite ver la evolución acumulada.

Veamos un ejemplo con transition_states y shadow_mark donde comparamos la distribución del PIB per cápita por continentes:

p <- owid_country |>
  filter(!is.na(gdp_per_capit)) |> # sin NA
  ggplot(aes(x = gdp_per_capit, fill = continent)) + # color por continente
  geom_histogram(bins = 20, alpha = 0.7, show.legend = FALSE) + # histograma
  scale_x_continuous(transform = 'log2') + # eje x logarítmico
  labs(title = "Continente: {closest_state}", x = "PIB per cápita (log)", y = "Nº países") + # título dinámico
  transition_states(continent, transition_length = 2, state_length = 3) + # un estado por continente
  shadow_mark(alpha = 0.15) # rastro de los estados anteriores

animate(p, fps = 10, end_pause = 30) # generamos el GIF
Animación con `transition_states` y `shadow_mark` mostrando la distribución del PIB per cápita por continentes

Figure 6.31: Animación con transition_states y shadow_mark mostrando la distribución del PIB per cápita por continentes

A continuación, volviendo a plotly, presentamos el uso más conocido de la animación: un diagrama de dispersión donde cada punto se desplaza a lo largo de los años. Comparamos las emisiones de CO2 por habitante debidas al carbón y al petróleo en los países más poblados del mundo, con el tamaño del círculo proporcional a su población. Lo construimos con plot_ly nativo —indicando la columna de los fotogramas en frame—, en lugar de convertir un ggplot: frame no forma parte de la gramática de ggplot2, así que pasarlo dentro de aes() obliga a convivir con un warning de estética desconocida.

# Orden FIJO de los países (el mismo del filtro de abajo, por población): si
# "country" se deja como texto, plot_ly asigna el color por orden alfabético, que
# no es más arbitrario pero tampoco aporta nada. Fijarlo como factor deja claro
# de dónde sale el orden y asegura que cada país conserva su color aunque cambie
# el filtro.
paises_poblados <- c("China", "India", "United States", "Indonesia", "Pakistan",
                     "Nigeria", "Brazil", "Bangladesh", "Russia")

# Paleta de 9 colores muy separados entre sí en tono, luminosidad y saturación,
# elegida a propósito frente a la paleta cualitativa por defecto de plotly (o
# ColorBrewer "Set1"), que con 9 niveles deja colores casi idénticos. Con 9
# países en un diagrama de DISPERSIÓN -sin el orden fijo de una barra o una
# línea, cualquier punto puede acabar junto a cualquier otro en la pantalla- no
# existe ninguna combinación de 9 colores que sea distinguible para una persona
# con daltonismo en los 36 pares posibles: es un límite geométrico del círculo
# de tonos, no un fallo de esta paleta en concreto. Por eso el país de cada
# punto se apoya también en la leyenda y en el cuadro emergente (`text`), que lo
# identifican por su nombre y no solo por el color.
colores_paises <- c("#ff0081", "#bb0000", "#e57600", "#316000", "#00c560",
                    "#006d97", "#0097ff", "#5200e4", "#e42ef6")

read.xlsx("https://ctim.es/AEDV/data/owid-co2-data-resumen.xlsx") |> # emisiones de CO2
  as_tibble() |>
  filter(iso_code %in% c("CHN","IND","USA","IDN","PAK","NGA","BRA","BGD","RUS"), # países más poblados
         year >= 1950) |>
  # las emisiones vienen en millones de toneladas: las pasamos a toneladas por habitante
  mutate(coal_co2_per_capita = coal_co2 * 1e6 / population,
         oil_co2_per_capita  = oil_co2  * 1e6 / population,
         country = factor(country, levels = paises_poblados)) |> # color fijo por país
  select(country,year,population,coal_co2_per_capita,oil_co2_per_capita) |>
  arrange(year, country) |> # orden temporal para la animación
  plot_ly(
    x = ~coal_co2_per_capita,
    y = ~oil_co2_per_capita,
    color = ~country,
    colors = colores_paises,
    size = ~population, # tamaño = población
    sizes = c(20, 800),
    frame = ~year, # variable de animación
    text = ~country,
    hoverinfo = "text",
    type = "scatter",
    mode = "markers"
  ) |>
  layout(xaxis = list(title = "CO2 por habitante debido al carbón (t)"), # títulos de los ejes
         yaxis = list(title = "CO2 por habitante debido al petróleo (t)")) |>
  animation_opts(frame = 200) # velocidad de la animación

Figure 6.32: Gráfico de dispersión con las emisiones de CO2 per cápita debidas al carbón y al petróleo en los países más poblados del mundo

La animación cuenta una historia que una tabla no dejaría ver igual de rápido. En 1950 solo dos países se separan del origen, Estados Unidos y Rusia; Estados Unidos sube después en petróleo hasta los años setenta y a partir de ahí retrocede en carbón; China permanece pegada al origen durante décadas y desde el año 2000 asciende casi en vertical hasta adelantarlo en emisiones de carbón por habitante.

6.15 Visualización dinámica de tablas

Usando, por ejemplo, la librería DT, es posible visualizar tablas (que no sean demasiado grandes) de forma dinámica con múltiples opciones de configuración. A continuación presentamos un ejemplo con un cuadro de búsqueda, sliders para movernos por la tabla, podemos ordenar por cada columna y dejamos la primera columna fija como referencia.

owid_country |>
  relocate(location) |> # location en primer lugar
  datatable(
    extensions = 'FixedColumns', # columnas fijas al hacer scroll
    options = list(
      scrollX = TRUE,
      scrollY = "400px",
      paging = FALSE,
      fixedColumns = list(leftColumns = 2) # fijamos las 2 primeras columnas
  )
)

Formato condicional en tablas

La función formatStyle de DT permite colorear celdas en función de su valor, lo cual es muy útil en cuadros de mandos para resaltar indicadores que superan un umbral:

owid_country |>
  select(location, continent, life_expectancy, gdp_per_capit, human_development_index) |> # columnas de interés
  filter(!is.na(human_development_index)) |> # sin NA
  datatable(options = list(pageLength = 10, scrollX = TRUE)) |>
  formatStyle( # color de fondo por rango
    'life_expectancy',
    backgroundColor = styleInterval(c(65, 75), c('#FF6B6B', '#FFE66D', '#4ECDC4'))
  ) |>
  formatStyle( # barra de progreso proporcional al valor
    'human_development_index',
    background = styleColorBar(range(owid_country$human_development_index, na.rm = TRUE), '#6C5CE7'),
    backgroundSize = '95% 80%',
    backgroundRepeat = 'no-repeat',
    backgroundPosition = 'center'
  ) |>
  formatRound(columns = c('life_expectancy', 'gdp_per_capit', 'human_development_index'), digits = 2) # redondeo

En este ejemplo, la columna life_expectancy se colorea en rojo (<65), amarillo (65-75) o verde (>75), y la columna human_development_index muestra una barra de progreso proporcional al valor.

Referencias

[Ca] Cartography Vectors. Spain with regions (geometría de las comunidades autónomas; en octubre de 2026 el sitio no respondía).

[Hi21] Asha Hill. 9 Useful R Data Visualization Packages for Data Visualization, 2021.

[Ka20] Rob Kabacoff. Data Visualization with R, 2020.

[Leaflet] Leaflet for R.

[Open] OpenDataSoft

[Rc] R CODER. R CHARTS.

[Sthda] STHDA Statistical tools for high-throughput data analysis.

[Ho] Yan Holtz. The R Graph Gallery.

[DataNovia] Highchart Lessons. Datanovia.