install.packages(c("gganimate", "gifski", "av"))Grafici animati con gganimate
Prepariamo l’ambiente di lavoro
Il package gganimate estende ggplot2: permette di trasformare un grafico statico in un’animazione aggiungendo nuovi strati con il +, esattamente come si aggiungono geometrie, facet o temi. Per produrre l’animazione, gganimate disegna una serie di fotogrammi (frame) e li unisce in un unico file: per questo passaggio ha bisogno di un renderer, cioè di un package che sappia creare il file finale. I più usati sono gifski, per le gif, e av, per i video in formato mp4.
# carico le librerie di lavoro --------------------------------------------
library(tidyverse) # per ggplot2 e per le funzioni di dplyr
library(janitor) # per la funzione clean_names
library(gapminder) # per la tabella dati utilizzata
library(gganimate) # per animare i grafici di ggplot2
# NOTA: per creare le animazioni gganimate ha bisogno di un "renderer":
# - il package gifski (consigliato) per salvare in formato gif
# - il package av per salvare in formato video (mp4)
# se non sono installati:
# install.packages(c("gganimate", "gifski", "av"))Calcolare un’animazione richiede qualche secondo, e questo tutorial ne contiene più di venti. Per questo il codice delle animazioni è mostrato ma non eseguito durante la compilazione del documento: le animazioni che vedete sono state create in anticipo, eseguendo lo stesso codice dalla console, e salvate come file gif. Eseguendo il codice in RStudio o in Positron otterrete le stesse animazioni.
La tabella dati gapminder
Come nei tutorial su ggplot2 e sulla grafica di base, uso la tabella gapminder, che riporta per 142 paesi e per gli anni dal 1952 al 2007 (ogni 5 anni) il continente, la speranza di vita alla nascita, la popolazione e il PIL pro capite. È la tabella resa famosa dalla presentazione di Hans Rosling: “The best stats you’ve ever seen”, in cui un diagramma a bolle animato mostra come sono cambiate ricchezza e salute delle nazioni nel corso di mezzo secolo. Ricostruire quel grafico è l’obiettivo dell’ultima parte del tutorial.
Oltre alla tabella completa gap_data (con i nomi delle variabili ripuliti da clean_names) e alla tabella gap_2007, preparo la tabella gap_continenti con le medie per continente e anno, utile per gli esempi più semplici.
gap_data <- gapminder |>
clean_names() |>
rename(gdp_per_cap = gdp_percap)
gap_2007 <- gap_data |>
filter(year == 2007)
# medie per continente e anno, utili per alcuni esempi
gap_continenti <- gap_data |>
group_by(continent, year) |>
summarise(life_exp = mean(life_exp),
gdp_per_cap = mean(gdp_per_cap),
.groups = "drop")
gap_continenti# A tibble: 60 × 4
continent year life_exp gdp_per_cap
<fct> <int> <dbl> <dbl>
1 Africa 1952 39.1 1253.
2 Africa 1957 41.3 1385.
3 Africa 1962 43.3 1598.
4 Africa 1967 45.3 2050.
5 Africa 1972 47.5 2340.
6 Africa 1977 49.6 2586.
7 Africa 1982 51.6 2482.
8 Africa 1987 53.3 2283.
9 Africa 1992 53.6 2282.
10 Africa 1997 53.6 2379.
# ℹ 50 more rows
La logica di gganimate
Un grafico ggplot2 rappresenta i dati in un’unica immagine. Quando nei dati c’è una dimensione temporale, o più in generale una variabile che distingue “momenti” diversi, possiamo rappresentarla in tre modi: con il colore o la forma degli elementi, con un pannello per ciascun momento (i facet), oppure con il tempo stesso, mostrando un momento dopo l’altro. È quest’ultima la strada di gganimate.
Il grafico seguente, con tutti gli anni insieme, contiene troppi punti per essere leggibile:
# gganimate estende la grammatica di ggplot2: si parte da un grafico
# statico e si AGGIUNGONO (con il +) nuovi "strati" che descrivono come
# il grafico cambia nel tempo. Le famiglie di funzioni sono:
# transition_*() quale variabile guida l'animazione (obbligatoria)
# enter_*()/exit_*() come compaiono e scompaiono gli elementi
# ease_aes() con quale "andatura" ci si muove da un fotogramma all'altro
# view_*() come cambiano gli assi durante l'animazione
# shadow_*() quale traccia lasciano i fotogrammi passati
# partiamo da un grafico statico: tutti gli anni insieme (troppi punti!)
ggplot(gap_data, aes(x = gdp_per_cap, y = life_exp)) +
geom_point(alpha = 0.3) +
scale_x_log10()
Aggiungendo una sola riga, transition_time(year), otteniamo un fotogramma per ciascun anno, più i fotogrammi intermedi che rendono fluido il passaggio da un anno all’altro:
# aggiungendo transition_time(year) otteniamo un fotogramma per ogni anno
# (e i fotogrammi intermedi, interpolati)
p_idea <- ggplot(gap_data, aes(x = gdp_per_cap, y = life_exp)) +
geom_point(alpha = 0.5) +
scale_x_log10() +
transition_time(year)
class(p_idea) # un oggetto "gganim": un ggplot che sa muoversi
p_idea # stampandolo, R calcola e mostra l'animazione
# NOTA: il calcolo richiede qualche secondo (per default 100 fotogrammi);
# in RStudio/Positron l'animazione compare nel pannello Viewer
L’oggetto prodotto ha classe gganim: è un grafico ggplot2 che “sa muoversi”. Come un grafico ggplot2, viene disegnato solo quando lo si stampa; a differenza di un grafico statico, la stampa richiede qualche secondo, perché R deve disegnare tutti i fotogrammi (100 per default) e unirli. In RStudio e in Positron l’animazione compare nel pannello Viewer.
Le funzioni di gganimate si dividono in famiglie, ognuna con un compito preciso:
| Famiglia | Che cosa stabilisce | Esempi |
|---|---|---|
transition_*() |
quale variabile guida l’animazione (è l’unica obbligatoria) | transition_states(), transition_time(), transition_reveal(), transition_layers() |
enter_*(), exit_*() |
come compaiono e scompaiono gli elementi | enter_fade(), enter_grow(), exit_shrink(), exit_fly() |
ease_aes() |
l’andatura del movimento tra due fotogrammi | ease_aes("linear"), ease_aes("cubic-in-out") |
view_*() |
come cambiano gli assi durante l’animazione | view_follow(), view_step(), view_zoom() |
shadow_*() |
quale traccia lasciano i fotogrammi passati | shadow_wake(), shadow_trail(), shadow_mark() |
transition_states(): una variabile qualitativa
transition_states() passa da uno stato all’altro, dove gli stati sono le modalità di una variabile qualitativa (o di una variabile numerica con pochi valori). Nell’esempio, l’aspettativa di vita e il PIL pro capite dei paesi nel 2007, un continente alla volta:
# transition_states() passa da uno "stato" all'altro: gli stati sono le
# modalità di una variabile qualitativa (o di un numero intero con pochi
# valori). Qui: l'aspettativa di vita del 2007, un continente alla volta
p_states <- ggplot(gap_2007, aes(x = gdp_per_cap, y = life_exp)) +
geom_point(aes(color = continent), size = 3) +
scale_x_log10() +
transition_states(continent)
p_states
# NOTA: guardate bene il passaggio da un continente all'altro: i punti
# "viaggiano" e cambiano colore. gganimate abbina le righe di uno
# stato a quelle dello stato successivo e le sposta: qui l'effetto
# non ha senso (un paese africano non "diventa" americano).
# Lo sistemiamo nella sezione 3
Gli argomenti principali di transition_states() regolano i tempi dell’animazione:
transition_length: la durata (relativa) del passaggio da uno stato al successivostate_length: la durata (relativa) della pausa su ciascuno statowrap: seTRUE(il default) dopo l’ultimo stato l’animazione torna al primo
Le durate sono relative: con transition_length = 1 e state_length = 3 le pause durano il triplo dei passaggi, qualunque sia la durata complessiva dell’animazione.
# gli argomenti principali di transition_states():
# transition_length durata (relativa) del passaggio tra due stati
# state_length durata (relativa) della pausa su ciascuno stato
# wrap TRUE (default): dall'ultimo stato torna al primo
# nel titolo, {closest_state} viene sostituito dallo stato corrente
p_states <- ggplot(gap_2007, aes(x = gdp_per_cap, y = life_exp)) +
geom_point(aes(color = continent), size = 3, show.legend = FALSE) +
scale_x_log10() +
labs(title = "Continente: {closest_state}") +
transition_states(continent, transition_length = 1, state_length = 3)
p_states
Nei testi del grafico (titolo, sottotitolo, didascalia, …) si possono inserire tra parentesi graffe alcune variabili calcolate da gganimate per ogni fotogramma. Quelle disponibili dipendono dalla funzione transition_*() usata:
| Funzione | Variabili |
|---|---|
transition_states() |
{closest_state}, {previous_state}, {next_state}, {transitioning} |
transition_time() |
{frame_time} |
transition_reveal() |
{frame_along} |
| tutte | {frame} (numero del fotogramma), {nframes} (numero totale), {progress} (avanzamento, da 0 a 1) |
Tra le graffe si può scrivere anche un’espressione R, ad esempio {round(frame_time)}.
Un esempio classico di transition_states() sono le barre che crescono nel tempo. Qui l’anno ha solo 12 valori e lo possiamo trattare come uno stato:
# un esempio classico: barre che crescono anno dopo anno.
# Qui year ha 12 valori: lo trattiamo come stato
p_barre <- ggplot(gap_continenti, aes(x = continent, y = life_exp,
fill = continent)) +
geom_col(show.legend = FALSE) +
labs(title = "Aspettativa di vita media nel {closest_state}",
x = NULL, y = "Anni") +
transition_states(year, transition_length = 2, state_length = 1)
p_barre
Entrare, uscire, muoversi
Gli oggetti dell’animazione e l’estetica group
Per costruire i fotogrammi intermedi, gganimate deve sapere quali righe di uno stato corrispondono a quali righe dello stato successivo: le righe abbinate vengono spostate gradualmente dalla vecchia alla nuova posizione. Se non diciamo nulla, le righe vengono abbinate in base ai gruppi definiti dalle variabili qualitative del grafico e all’ordine in cui compaiono nella tabella. Nell’esempio dei continenti questo produce un effetto privo di senso (i punti africani “diventano” punti americani), che si corregge con l’estetica group: con group = country ogni paese è un oggetto a sé, che non ha corrispondenti negli altri stati.
Gli oggetti senza corrispondenti entrano nell’animazione quando compaiono ed escono quando scompaiono. Le funzioni enter_*() ed exit_*() stabiliscono come:
| Entrata | Uscita | Effetto |
|---|---|---|
enter_appear() |
exit_disappear() |
compare/scompare di colpo (default) |
enter_fade() |
exit_fade() |
dissolvenza |
enter_grow() |
exit_shrink() |
crescita/riduzione della dimensione |
enter_fly() |
exit_fly() |
arrivo/uscita da un punto indicato con x_loc e y_loc |
enter_drift() |
exit_drift() |
scivolamento laterale |
# per evitare che i punti "viaggino" da un continente all'altro bisogna
# dire a gganimate quali righe sono lo stesso oggetto: l'estetica group.
# Con group = country ogni paese è un oggetto a sé, presente in un solo
# stato: entra quando arriva il suo continente ed esce quando se ne va.
# enter_*() ed exit_*() definiscono COME entra ed esce:
# enter_fade() / exit_fade() dissolvenza
# enter_grow() / exit_shrink() crescita / riduzione della dimensione
# enter_fly(x_loc, y_loc) / exit_fly() arrivo / uscita da un punto
# enter_drift() / exit_drift() scivolamento laterale
p_states_paesi <- ggplot(gap_2007, aes(x = gdp_per_cap, y = life_exp,
group = country)) +
geom_point(aes(color = continent), size = 3, show.legend = FALSE) +
scale_x_log10() +
labs(title = "Continente: {closest_state}") +
transition_states(continent, transition_length = 1, state_length = 3)
p_states_paesi # senza enter/exit: compaiono e spariscono
p_states_paesi +
enter_fade() +
exit_shrink()

L’andatura del movimento
La funzione ease_aes() stabilisce l’andatura (in inglese easing) con cui gli elementi si spostano tra due fotogrammi. Il default è "linear", cioè una velocità costante. Le altre andature combinano una curva (quadratic, cubic, quartic, quintic, sine, circular, exponential, elastic, back, bounce) con un suffisso che indica dove agisce l’accelerazione: -in (all’inizio), -out (alla fine) o -in-out (all’inizio e alla fine).
# ease_aes() stabilisce l'andatura del movimento tra due fotogrammi:
# "linear" velocità costante (default)
# "cubic-in-out" parte piano, accelera, rallenta all'arrivo
# "elastic-out" arriva "rimbalzando"
# "bounce-out" arriva "saltellando"
# provate a cambiare il valore e a osservare le barre
p_barre +
ease_aes("cubic-in-out")
p_barre +
ease_aes("elastic-out")

transition_time(): una variabile temporale
transition_time() usa una variabile numerica che rappresenta il tempo. A differenza di transition_states(), non ci sono pause sugli stati e la durata di ciascun passaggio è proporzionale alla distanza nel tempo: se gli anni non fossero equidistanti, il passaggio tra due anni lontani durerebbe di più. Nel titolo si usa la variabile {frame_time}:
# transition_time() usa una variabile numerica che rappresenta il tempo:
# a differenza di transition_states() non ci sono pause, e la durata dei
# passaggi è proporzionale alla distanza nel tempo
# nel titolo si usa {frame_time}: il tempo del fotogramma corrente
p_time <- ggplot(gap_continenti, aes(x = gdp_per_cap, y = life_exp,
color = continent)) +
geom_point(size = 4) +
scale_x_log10() +
labs(title = "Anno: {frame_time}") +
transition_time(year)
p_time
# il titolo mostra anni con i decimali (es. 1953.27): sono i fotogrammi
# intermedi. Si arrotonda con una funzione R dentro le graffe:
p_time +
labs(title = "Anno: {round(frame_time)}")

I fotogrammi sono 100 per default, mentre gli anni sono 12: la maggior parte dei fotogrammi rappresenta un momento intermedio tra due anni, e {frame_time} vale ad esempio 1953.27. Per mostrare un anno intero basta arrotondare con {round(frame_time)}.
Le ombre: la memoria dei fotogrammi passati
Per default ogni fotogramma mostra solo i dati del momento corrente. Le funzioni shadow_*() lasciano sul grafico una traccia dei fotogrammi precedenti, utile per seguire la traiettoria degli elementi:
shadow_wake()lascia una scia che si dissolve progressivamente;wake_lengthne stabilisce la lunghezza (come frazione dell’animazione)shadow_trail()lascia una serie di “impronte” a intervalli regolari, stabiliti dadistanceshadow_mark()lascia sul grafico tutti i fotogrammi già mostrati
# le funzioni shadow_*() lasciano una traccia dei fotogrammi precedenti
# shadow_wake() una scia che si dissolve (wake_length = lunghezza)
# shadow_trail() una serie di "impronte" a intervalli regolari
# shadow_mark() lascia sul grafico tutti i fotogrammi già mostrati
p_time_tit <- p_time +
labs(title = "Anno: {round(frame_time)}")
p_time_tit +
shadow_wake(wake_length = 0.2)
p_time_tit +
shadow_trail(distance = 0.1, alpha = 0.3)
p_time_tit +
shadow_mark(alpha = 0.3, size = 1.5)


transition_reveal(): le serie storiche
transition_reveal() “scopre” progressivamente i dati lungo una variabile, tipicamente il tempo, lasciando visibile il tratto già percorso: è l’animazione naturale per un grafico a linee. Si noti la differenza tra i due layer: geom_line() mostra il percorso fino al momento corrente, geom_point() solo il punto corrente. Nel titolo si usa la variabile {frame_along}.
# transition_reveal() "scopre" i dati progressivamente lungo una variabile
# (tipicamente il tempo), lasciando visibile il tratto già percorso:
# è l'animazione naturale per un grafico a linee.
# nel titolo si usa {frame_along}
paesi <- c("Italy", "China", "India", "Brazil", "Nigeria")
p_reveal <- gap_data |>
filter(country %in% paesi) |>
ggplot(aes(x = year, y = life_exp, color = country)) +
geom_line(linewidth = 1) +
geom_point(size = 3) +
labs(title = "Aspettativa di vita fino al {frame_along}",
x = NULL, y = "Anni", color = NULL) +
transition_reveal(year)
p_reveal
# NOTA: geom_point mostra solo il punto corrente, geom_line l'intero
# percorso fino a quel momento
Gli assi che si adattano: view_follow()
Per default gli assi restano fissi per tutta l’animazione, e coprono l’intervallo di tutti i dati. view_follow() adatta invece gli assi ai dati del fotogramma corrente, come una telecamera che insegue i punti. Con fixed_x = TRUE o fixed_y = TRUE uno dei due assi resta fisso.
# view_follow() adatta gli assi ai dati del fotogramma corrente: gli
# assi "inseguono" i punti, come una telecamera.
# fixed_x = TRUE (o fixed_y = TRUE) lascia fisso uno dei due assi
p_time_tit +
view_follow()
p_time_tit +
view_follow(fixed_y = TRUE)

Con assi che cambiano a ogni fotogramma il grafico è più “dinamico”, ma il lettore perde i punti di riferimento: un punto può sembrare fermo anche se il suo valore cambia. Gli assi mobili sono utili quando i valori cambiano di ordine di grandezza nel tempo; negli altri casi conviene lasciarli fissi.
La grammatica in diretta: transition_layers()
transition_layers() mostra i layer del grafico uno alla volta, nell’ordine in cui sono stati aggiunti. Oltre a essere utile in una presentazione, è un modo per vedere all’opera la grammatica di ggplot2: il grafico finale è la sovrapposizione di strati indipendenti.
# transition_layers() mostra i layer uno dopo l'altro, nell'ordine in cui
# sono stati aggiunti: un modo per vedere la costruzione del grafico
ggplot(gap_2007, aes(x = gdp_per_cap, y = life_exp)) +
geom_point(aes(color = continent)) +
geom_smooth(color = "black", se = FALSE) +
geom_text(data = filter(gap_2007, country %in% paesi),
aes(label = country), vjust = -1) +
scale_x_log10() +
transition_layers(layer_length = 1, transition_length = 1) +
enter_grow()
# NOTA: con geom_smooth() la combinazione con enter_fade() restituisce un
# errore (Can't combine `..1$fill`...): usiamo enter_grow()
Controllare e salvare l’animazione
Stampare un oggetto gganim equivale a chiamare la funzione animate() con i valori di default. Usando direttamente animate() possiamo controllare il risultato:
| Argomento | Significato |
|---|---|
nframes |
numero di fotogrammi (default 100) |
fps |
fotogrammi al secondo (default 10): la durata è nframes / fps secondi |
duration |
in alternativa a fps, la durata in secondi |
width, height, res |
dimensioni in pixel e risoluzione |
start_pause, end_pause |
numero di fotogrammi di pausa all’inizio e alla fine |
rewind |
se TRUE, alla fine l’animazione torna indietro |
renderer |
il formato del risultato: gifski_renderer() per le gif (default), av_renderer() per i video |
Più fotogrammi rendono l’animazione più fluida, ma richiedono più tempo di calcolo e producono file più grandi.
Gli argomenti width e height di animate() indicano le dimensioni in pixel e vanno usati quando si lavora nella console o in uno script. In un documento Quarto, invece, le dimensioni dell’animazione si indicano con le opzioni del chunk (#| fig-width, #| fig-height e #| dpi), come per i grafici statici: indicarle anche nella chiamata ad animate() può bloccare la compilazione del documento.
# stampare un oggetto gganim equivale a chiamare animate() con i valori
# di default. animate() permette di controllare:
# nframes numero di fotogrammi (default 100)
# fps fotogrammi al secondo (default 10): durata = nframes / fps
# duration in alternativa: durata in secondi
# width, height, res dimensioni (in pixel) e risoluzione
# start_pause, end_pause fotogrammi di pausa all'inizio e alla fine
# rewind TRUE: alla fine l'animazione torna indietro
# renderer il formato di uscita: gifski_renderer() (gif),
# av_renderer() (video mp4)
animate(p_time_tit, nframes = 120, fps = 20,
width = 700, height = 450, end_pause = 20)
La funzione anim_save() salva in un file l’ultima animazione mostrata, oppure quella indicata con l’argomento animation. Il formato del file dipende dal renderer usato per produrre l’animazione:
# anim_save() salva l'ultima animazione mostrata (o quella indicata
# con l'argomento animation) nella cartella del progetto
anim_save("continenti.gif")
# per un video: si sceglie il renderer av_renderer() in animate()
video <- animate(p_time_tit, nframes = 120, fps = 20,
renderer = av_renderer())
anim_save("continenti.mp4", animation = video)Il grafico di Gapminder, passo passo
Mettiamo insieme quanto visto per ricostruire il diagramma a bolle della presentazione di Hans Rosling. Come in ogni grafico ggplot2 conviene procedere per piccoli passi, controllando il risultato di ciascuno.
Passo 1: il diagramma di dispersione di un anno
Partiamo dal grafico statico più semplice: il PIL pro capite e l’aspettativa di vita dei paesi nel 2007.
# PASSO 1: il grafico di dispersione di un solo anno
ggplot(gap_2007, aes(x = gdp_per_cap, y = life_exp)) +
geom_point()
Passo 2: la scala logaritmica
La distribuzione del PIL pro capite è molto asimmetrica: pochi paesi hanno valori molto alti e la maggior parte dei punti è schiacciata a sinistra. Con la scala logaritmica sull’asse x la nuvola di punti si “apre” e la relazione tra le due variabili diventa quasi lineare.
# PASSO 2: la scala logaritmica sull'asse x
# il reddito pro capite è molto asimmetrico: in scala logaritmica la
# nuvola di punti si "apre" e la relazione diventa quasi lineare
ggplot(gap_2007, aes(x = gdp_per_cap, y = life_exp)) +
geom_point() +
scale_x_log10()
Passo 3: dai punti alle bolle
Associamo la popolazione alla dimensione dei punti e il continente al colore. La funzione scale_size() regola la dimensione minima e massima delle bolle; la trasparenza (alpha) rende visibili le bolle sovrapposte.
# PASSO 3: dai punti alle bolle
# la dimensione rappresenta la popolazione, il colore il continente;
# scale_size() regola la dimensione minima e massima delle bolle,
# alpha rende visibili le bolle sovrapposte
ggplot(gap_2007, aes(x = gdp_per_cap, y = life_exp,
size = pop, color = continent)) +
geom_point(alpha = 0.7) +
scale_x_log10() +
scale_size(range = c(2, 15))
Passo 4: tutti gli anni insieme
Se usiamo la tabella completa il grafico diventa illeggibile: ogni paese compare 12 volte. Servirebbe un grafico per ogni anno…
# PASSO 4: tutti gli anni insieme. Il grafico è illeggibile:
# servirebbe un grafico per ogni anno...
ggplot(gap_data, aes(x = gdp_per_cap, y = life_exp,
size = pop, color = continent)) +
geom_point(alpha = 0.7) +
scale_x_log10() +
scale_size(range = c(2, 15))
Passo 5: l’animazione
… ed è proprio quello che fa transition_time(year): rispetto al passo 4 basta aggiungere una riga.
# PASSO 5: ... ed è proprio quello che fa transition_time(year)
# basta aggiungere una riga!
p_bolle <- ggplot(gap_data, aes(x = gdp_per_cap, y = life_exp,
size = pop, color = continent)) +
geom_point(alpha = 0.7) +
scale_x_log10() +
scale_size(range = c(2, 15)) +
transition_time(year)
p_bolle
Passo 6: l’anno nel titolo e il movimento
Aggiungiamo l’anno nel titolo e un movimento a velocità costante.
# PASSO 6: l'anno nel titolo e un movimento a velocità costante
p_bolle <- p_bolle +
labs(title = "Anno: {round(frame_time)}") +
ease_aes("linear")
p_bolle
Passo 7: l’anno sullo sfondo
Nella presentazione di Rosling l’anno compare in grande sullo sfondo. Lo aggiungiamo come un nuovo layer di testo, con tre accorgimenti:
- il layer viene aggiunto prima di
geom_point(), così il testo resta sotto le bolle - usa una tabella propria,
anni, con una sola riga per anno, e non eredita le estetiche del grafico (inherit.aes = FALSE) - l’anno è trasformato in testo con
as.character(): un numero verrebbe interpolato tra un anno e l’altro, e comparirebbero valori come 1984.5
# PASSO 7: l'anno sullo sfondo, come nella presentazione di Rosling.
# Il testo è un nuovo layer, disegnato PRIMA dei punti, quindi sotto le
# bolle; usa una tabella con una riga per anno e non eredita le
# estetiche del grafico (inherit.aes = FALSE)
# il testo deve essere di tipo character: un numero verrebbe interpolato
# tra un anno e l'altro (comparirebbe, ad esempio, 1984.5)
anni <- gap_data |>
distinct(year) |>
mutate(etichetta = as.character(year))
p_bolle <- ggplot(gap_data, aes(x = gdp_per_cap, y = life_exp,
size = pop, color = continent)) +
geom_text(data = anni, aes(x = 3000, y = 55, label = etichetta),
inherit.aes = FALSE, size = 40, color = "grey90") +
geom_point(alpha = 0.7) +
scale_x_log10() +
scale_size(range = c(2, 15)) +
transition_time(year) +
ease_aes("linear")
p_bolle
Passo 8: alcuni paesi da seguire
Per seguire alcuni paesi durante l’animazione aggiungiamo il loro nome con un altro layer di testo, che usa solo le righe dei paesi scelti. Osservate in particolare il Ruanda: nel 1992 l’aspettativa di vita crolla a 23.6 anni, per effetto della guerra civile e del genocidio dei primi anni ’90.
# PASSO 8: il nome di alcuni paesi, per seguirli durante l'animazione
# (anche questo è un layer con dati propri: solo i paesi scelti)
paesi <- c("Italy", "China", "India", "Nigeria", "Rwanda")
# osservate il Ruanda: nel 1992 l'aspettativa di vita crolla a 23.6 anni
# (guerra civile e genocidio dei primi anni '90)
p_bolle <- p_bolle +
geom_text(data = filter(gap_data, country %in% paesi),
aes(label = country), size = 4, color = "black",
vjust = -1.5, show.legend = FALSE)
p_bolle
Passo 9: le rifiniture
Sistemiamo le etichette dell’asse x (in dollari, con la funzione label_dollar() del package scales), eliminiamo la legenda della dimensione, aggiungiamo titoli in italiano e un tema più pulito. R avvisa che stiamo sostituendo le scale x e size già presenti: è proprio quello che vogliamo.
# PASSO 9: le rifiniture
# - etichette leggibili sugli assi (scales::label_dollar)
# - nessuna legenda per la dimensione (guide = "none")
# - titoli in italiano e un tema pulito
p_bolle <- p_bolle +
scale_x_log10(labels = scales::label_dollar()) +
scale_size(range = c(2, 15), guide = "none") +
labs(title = "Ricchezza e salute delle nazioni",
x = "PIL pro capite (dollari, scala logaritmica)",
y = "Aspettativa di vita (anni)",
color = "Continente",
caption = "Fonte: gapminder.org") +
theme_minimal(base_size = 14)
p_bolle
# NOTA: R avvisa che stiamo sostituendo le scale x e size già presenti
# ("Scale for x is already present"): è quello che vogliamo
Passo 10: un’animazione più fluida e il salvataggio
Infine aumentiamo il numero di fotogrammi, aggiungiamo una pausa finale e salviamo il risultato in un file gif.
# PASSO 10: animazione più fluida e salvataggio
animate(p_bolle, nframes = 240, fps = 20,
width = 900, height = 600, end_pause = 30)
anim_save("gapminder_bolle.gif")
Per chi vuole fare di più
Il package gapminder contiene anche il vettore country_colors, che assegna a ogni paese un colore, con tonalità diverse per i diversi continenti. Combinandolo con un pannello per continente otteniamo l’animazione presentata nella documentazione di gganimate:
# BONUS: un pannello per continente, con i colori "ufficiali" dei paesi
# (il vettore country_colors è contenuto nel package gapminder)
ggplot(gap_data, aes(x = gdp_per_cap, y = life_exp,
size = pop, color = country)) +
geom_point(alpha = 0.7, show.legend = FALSE) +
scale_color_manual(values = country_colors) +
scale_size(range = c(2, 12)) +
scale_x_log10() +
facet_wrap(~ continent) +
labs(title = "Anno: {round(frame_time)}",
x = "PIL pro capite", y = "Aspettativa di vita") +
transition_time(year) +
ease_aes("linear")
Un consiglio: quando animare un grafico
Un’animazione attira l’attenzione ed è molto efficace in una presentazione, quando qualcuno ne guida la lettura. Ha però dei limiti: chi la guarda non può confrontare facilmente due momenti lontani nel tempo, e su un documento stampato non funziona. Prima di animare un grafico conviene chiedersi se un grafico statico ben costruito, ad esempio con un pannello per ciascun momento (i facet), non comunichi lo stesso messaggio in modo più semplice.
Per approfondire
- Il sito del package, con la documentazione di tutte le funzioni e numerosi esempi: https://gganimate.com
- Hans Rosling, The best stats you’ve ever seen (TED, 2006)
- Il sito di Gapminder, con gli strumenti interattivi per esplorare i dati