La presentazione dei dati ha un ruolo rilevante in molte applicazioni, non solo per una questione estetica ma soprattutto perché le informazioni vengano colte in maniera più intuitiva. Ad esempio, un grafico delle temperature in una data località rende meglio l'idea dell'andamento del clima rispetto ad una semplice tabella di valori.
D3js, o semplicemente D3, è una libreria JavaScript che ci permette di creare grafici d'impatto (o infografiche) a partire dai dati (D3 è l'acronimo di Data-Driven Documents). In altre parole questa libreria fornisce funzionalità per la generazione di documenti HTML il cui contenuto è dinamicamente determinato dai dati. In questo articolo ne esamineremo le funzionalità di base per comprendere le potenzialità offerte manipolando DOM e CSS tramite JavaScript.
Manipolare il DOM
L'aggancio della libreria in un documento HTML5 è molto semplice:
<!doctype html>
<html>
<head>
<script src="js/d3.v2.min.js"></script>
</head>
In alternativa al mantenimento di una copia locale della libreria è possibile agganciare la sua versione remota:
<script src="http://d3js.org/d3.v2.js"></script>
Dopo aver reso disponibile la libreria all'interno del nostro documento, la prima cosa che dobbiamo esplorare è il meccanismo di selezione di nodi del DOM e la loro manipolazione. Vediamo, ad esempio, come selezionare tutti i paragrafi di un documento:
d3.selectAll("p")
Evidenziamo innanzitutto che i metodi forniti dalla libreria fanno capo all'oggetto globale d3. Il metodo selectAll()
di questo oggetto consente di selezionare tutti gli elementi del documento corrente identificati dalla stringa passata come parametro, nel nostro caso tutti gli elementi di tipo paragrafo.
Il valore che può assumere il parametro segue le specifiche dei selettori stabilite dal W3C quindi, ad esempio, per selezionare un solo elemento con id uguale a test possiamo scrivere:
d3.select("#test")
In questo caso abbiamo utilizzato il metodo select() anzichè selectAll(). Questi concetti dovrebbero suonare familiari a chi usa jQuery e tool analoghi. In effetti il meccanismo per la selezione degli elementi del DOM è molto simile.
Analogo è anche l'utilizzo della chaining syntax per concatenare metodi in un'unica istruzione. Infatti, il risultato di una selezione è un oggetto che consente di accedere alle proprietà del nodo o dei nodi sottostanti tramite opportuni metodi. Ad esempio, se vogliamo impostare le dimensioni del font di tutti i paragrafi possiamo scrivere:
d3.selectAll("p")
.style("font-size", "30px")
NOTA: Prima di proseguire è opportuno evidenziare che per utilizzare la libreria occorre assicurarsi che il DOM sia stato completamente caricato, ad esempio eseguendo il codice in corrispondenza dell'evento onload del body.
Il data binding
Fin qui le funzionalità offerte da D3 non hanno nulla di particolarmente eccezionale. In effetti la parte realmente interessante ed innovativa riguarda la mappatura dei dati sui nodi del DOM. Analizziamo ad esempio il seguente codice:
d3.selectAll("p")
.data([20, 30, 40, 50, 60])
.style("font-size", function(d) { return d + "px"})
Con esso selezionamo tutti i paragrafi ed associamo a ciascuno di essi, tramite il metodo data(), uno dei valori contenuti nell'array. In pratica, se abbiamo cinque paragrafi nel documento, al primo associamo il valore 20, al secondo il valore 30 e così via. Quindi impostiamo le dimensioni del font basandoci su una funzione che per ciascun paragrafo riceve come parametro il valore associato e restituisce la stringa che rappresenta le dimensioni in pixel.
Il risultato sarà una sequenza di paragrafi con le dimensioni corrispondenti agli elementi dell'array.
Nell'esempio appena visto, un requisito fondamentale per il funzionamento del codice è che i paragrafi per cui abbiamo impostato le dimensioni del font siano pre-esistenti nel DOM. Possiamo fare in modo che D3 si occupi di generare dinamicamente gli elementi del DOM modificando il codice come mostrato di seguito:
d3.select("body")
.selectAll("p")
.data([20, 30, 40, 50, 60])
.enter()
.append("p")
.text("Veni vidi vici")
.style("font-size", function(d) { return d + "px"})
Abbiamo aggiunto alcune istruzioni rispetto all'esempio precedente: la selezione del bodysi rende necessaria perché, in assenza di paragrafi definiti staticamente nel documento, la selezione di tutti i paragrafi restituirebbe una selezione vuota e questo porterebbe a non poter determinare il genitore dei paragrafi che andremo a generare nell'albero del DOM.
Vediamo inoltre la presenza del metodo enter() il cui compito consiste nella creazione di un nodo segnaposto per ciascun elemento dell'array non trovato nella selezione corrente. Nel nostro caso quindi verranno creati cinque nodi generici che rappresentano i segnaposto per i paragrafi aggiunti con la chiamata append("p")
.
All'interno di ciascuno di questi paragrafi viene inserito il testo specificato tramite il metodo text() ed infine viene applicata l'impostazione delle dimensioni del font.
Il risultato sarà identico al precedente, ma in questo caso potremo variare il numero di paragrafi e le relative dimensioni del font semplicemente modificando i dati presenti nell'array.
Creare tabelle
Una volta fatto nostro il meccanismo di data binding di D3 possiamo sfruttarlo per qualsiasi tipo di rappresentazione. Vediamo ad esempio come generare una tabella partendo da una matrice di dati (naturalmente inventati) che rappresenta il numero di articoli pubblicati su HTML.it per ciascun linguaggio o tecnologia. Supponiamo quindi di avere la seguente dichiarazione:
var data = [["JavaScript", 1329],
["PHP", 1164],
["Java", 871],
["Ruby", 339],
["ASP.NET", 847],
["XML", 325],
["HTML", 2483],
["CSS", 1639]
];
Per generare la tabella utilizzeremo il seguente codice JavaScript:
var rows = d3.select("body")
.append("table")
.selectAll("tr")
.data(data)
.enter()
.append("tr")
rows.selectAll("td")
.data(function(d) {return d})
.enter()
.append("td")
.text(function(d) {return d})
Abbiamo spezzato la catena di chiamate in due parti esclusivamente per rendere più chiara l'operazione. La prima catena genera la struttura della tabella con il numero di righe corrispondenti al numero di elementi contenuti nell'array data e lo assegna ad una variabile rows. A ciascuna riga, dunque, viene associato il corrispondente array di due elementi.
La seconda catena di chiamate genera le celle della tabella inserendo in ciascuna di esse il contenuto degli array associati a ciascuna riga.
Il risultato è mostrato di seguito:
Come abbiamo visto, la fonte dati a partire dalla quale generiamo il DOM può essere di qualsiasi tipo. Nei primi esempi abbiamo utilizzato un semplice array di valori numerici, in questo esempio abbiamo utilizzato un array di array con stringhe ed interi. Possiamo anche strutturare i dati come oggetti, come nel seguente esempio:
var data = [{topic:"JavaScript", count:1329},
{topic:"PHP", count:1164},
{topic:"Java", count:871},
{topic:"Ruby", count:339},
{topic:"ASP.NET", count:847},
{topic:"XML", count:325},
{topic:"HTML", count:2483},
{topic:"CSS", count:1639}
];
In ogni caso, la responsabilità di interpretare correttamente la struttura dati spetta al codice che genera i nodi del DOM. Quindi la seconda catena di chiamate del nostro codice andrebbe trasformata nel seguente modo:
rows.selectAll("td")
.data(function(d) {return new Array(d.topic, d.count)})
.enter()
.append("td")
.text(function(d) {return d})
In questo caso, a partire dal singolo oggetto contenuto nell'array dei dati, generiamo un nuovo array con i valori estratti dalle proprietà che ci interessano.
Caricare dati esterni (CSV, JSON, XML)
Negli esempi di questo articolo facciamo riferimento ad un array definito staticamente, ma in applicazioni reali i dati potrebbero essere esterni alla nostra applicazione. Per semplificare le problematiche di accesso ai dati, D3 mette a disposizione metodi per il loro caricamento e gestione nei formati più comuni come file di puro testo, file contenenti oggetti JSON, file XML, HTML o CSV.
Ad esempio, la seguente istruzione carica un file CSV eseguendo una chiamata HTTP asincrona e passa il contenuto, trasformato in array, alla funzione di callback specificata:
d3.csv('data/mydata.csv', function(data) {...});
Creare visualizzazioni grafiche
Ora che abbiamo visto come creare dinamicamente una tabella a partire da un array di dati, proviamo a spingerci un po' più in là nella creazione di una rappresentazione grafica più intuitiva. Prendiamo in considerazione sempre l'array di oggetti che rappresentano i dati statistici sugli articoli pubblicati e rappresentiamolo sotto forma di tag cloud. Il codice necessario è di seguito mostrato:
d3.select("body")
.append("p")
.selectAll("span")
.data(data)
.enter()
.append("span")
.text(function(d) {return d.topic})
.style("margin", "2px")
.style("font-size", function(d) {return d.count/30 + "px"})
.attr("title", function(d) {return "Articoli pubblicati: " + d.count});
A questo punto dovremmo aver acquisito una certa familiarità con i metodi di D3, quindi non dovremmo avere molta difficoltà a comprendere il codice. Sostanzialmente abbiamo creato un paragrafo contenente tanti <span>
quanti sono gli elementi dell'array. Ciascun elemento span visualizza l'argomento degli articoli mentre le dimensioni del relativo font sono proporzionali al numero di articoli pubblicati. Inoltre, a ciascun argomento è associato un tooltip che visualizza il relativo numero di articoli pubblicati.
Il risultato visivo sarà analogo a quello mostrato di seguito:
Se invece da questi dati vogliamo creare un grafico a barre possiamo farlo con il seguente codice:
d3.select("body")
.append("div")
.selectAll("div")
.data(data)
.enter()
.append("div")
.attr("class", "bar")
.style("width", function(d) {return d.count/10 + "px"})
.text(function(d) {return d.topic})
.attr("title", function(d) {return "Articoli pubblicati: " + d.count});
Creiamo un div ed all'interno inseriamo tanti div quanti sono gli elementi dell'array. Regoliamo la lunghezza di ciascun div in proporzione al numero di articoli pubblicati per argomento ed impostiamo un po' di attributi per facilitare la visualizzazione grafica.
In particolare impostiamo la classe di ciascun div in modo da poter gestire l'aspetto grafico tramite CSS. Un possibile risultato è mostrato nella seguente figura:
Nell'esempio abbiamo utilizzato dei semplici div per creare le barre del grafico. In realtà possiamo utilizzare approcci più sofisticati come ad esempio la creazione dinamica di grafica SVG con la possibilità quindi di disegnare linee e forme geometriche. Questo può essere fatto generando a runtime i tag SVG dell'immagine che vogliamo creare, come abbiamo fatto per i div, oppure sfruttando le funzionalità messe a disposizione da D3 tramite i metodi dell'oggetto d3.svg
.
La disponibilità dell'oggetto d3.svg
semplifica la creazione di figure geometriche delegando a D3 il compito di generare dinamicamente le eventuali informazioni intermedie mancanti, come ad esempio nel caso di creazione di percorsi o di aree.
Animazioni e transizioni
Oltre alla visualizzazione statica dei dati, D3 consente di realizzare semplici effetti di animazione che possono rendere più interessante la presentazione dei dati. Supponiamo di voler animare la generazione del nostro grafico a barre facendo in modo che le barre crescano fino a raggiungere le dimensioni finali corrispondenti al valore assegnato.
Possiamo ottenere questo effetto tramite la seguente variante del codice che abbiamo visto nel paragrafo precedente:
d3.select("body")
.append("div")
.selectAll("div")
.data(data)
.enter()
.append("div")
.attr("class", "bar")
.style("width", "0px")
.transition()
.duration(2000)
.style("width", function(d) {return d.count/10 + "px"})
.text(function(d) {return d.topic})
.attr("title", function(d) {return "Articoli pubblicati: " + d.count});
Abbiamo evidenziato in grassetto le differenze rispetto al codice precedente. In esse vediamo una chiamata al metodo style() che imposta le dimensioni iniziali di ciascun div a zero; segue una chiamata al metodo transition(), l'artefice vero dell'effetto di animazione che consente di passare da una rappresentazione grafica ad un'altra in maniera progressiva invece che istantanea; infine tramite duration() indichiamo il numero di millisecondi entro cui deve essere effettuata la transizione.
Se vogliamo arricchire ancora di più l'effetto dinamico della generazione del grafico possiamo aggiungere una chiamata al metodo ease() specificando uno degli effetti disponibili. Ad esempio, ease("elastic") infonde alla generazione del grafico un effetto elastico, come mostrato nel seguente esempio:
Applicazioni avanzate e conclusioni
Le funzionalità che abbiamo esplorato in questo articolo rappresentano soltanto un piccolo assaggio delle possibilità offerte dalla libreria D3. Oltre a poter generare grafica in SVG, la libreria prevede il supporto per diversi tipi di elaborazione e visualizzazione: dalla generazione di assi cartesiani a rappresentazioni in scala, da rappresentazioni per interpolazione a visualizzazioni sotto forma di grafi. Sul sito del progetto è presente una galleria di applicazioni che rende bene l'idea dei risultati interessanti che si possono ottenere con D3.
Tra l'altro questa libreria rappresenta la base per toolkit come Polychart.js, Rickshaw o Graphene. L'estensibilità di D3 permette inoltre la creazione di plugin e componenti specializzati che semplificano o arricchiscono le possibilità di rappresentazione dei dati. Ad esempio, NVD3 consente di creare facilmente i classici grafici lineari, a barre, a torta, ecc. mentre Cubism.js è un plugin specializzato nella rappresentazione grafica di serie temporali dinamiche. Altri plugin sono disponibili in un'apposita sezione del repository GitHub del progetto.
Il successo che sta vivendo questa libreria è segno della maturità raggiunta dalle tecnologie Web in questi ultimi anni, cosa che consente di essere indipendenti da componenti esterni e tecnologie proprietarie.
Link utili