Setup Menus in Admin Panel

School.Dataninja.it

Visualizzazioni dati con d3js: creare, aggiornare, distruggere

Non basta modificare gli elementi di una pagina, puoi anche crearli o rimuoverli quando e come vuoi in base ai dati dinamici che guidano il tuo documento.

Finora hai imparato a manipolare in tutti i modi gli elementi di una pagina, ma con un limite piuttosto grosso: tutti gli elementi dovevano già esistere nella pagina prima di potervi associare i dati e quindi modificarli in base a essi. Un documento realmente data-driven, però, non può partire già popolato di tutti gli elementi che servono prima che siano a disposizione i dati. Nell’unità su scale e proporzioni, però, hai visto le word cloud costruirsi in base alle parole presenti nella pagina, una classica situazione che è impossibile prevedere a priori.

Aggiungere e rimuovere elementi

D3js permette di manipolare agevolmente il DOM di una pagina web, selezionando qualsiasi elemento grazie ai selettori CSS, intervenendo sui suoi attributi e stili, impostando il suo contenuto testuale. Non ti deve stupire quindi che offra anche metodi per aggiungere e togliere elementi dalla pagina.

Si tratta sempre di metodi delle selezioni: il metodo append([tag]) inserisce all’interno della selezione corrente (in fondo se ci sono già altri elementi) l’elemento il cui tag è passato come parametro, mentre il metodo remove() elimina gli elementi presenti nella selezione corrente. Il metodo append() ritorna naturalmente una selezione, per cui è possibile concatenare a valle altri metodi, che però agiscono sull’elemento appena creato. Per esempio d3.select("p#span-container").append("span").text("Io sono uno span")  scrive il testo dentro allo span appena creato, non dentro l’elemento p selezionato all’inizio.

Esiste anche un metodo insert() che prende due parametri: il tag da inserire e il selettore del nodo prima del quale vuoi che sia inserito. Per esempio d3.select(“p”).insert(“span”,”:first-child”) aggiunge uno span in cima e non in fondo al paragrafo.  

Il metodo append() può di per sé aggiungere solo un elemento alla volta, ma fai attenzione alle selezioni annidate, di cui abbiamo già parlato. L’istruzione d3.selectAll("p").append("span")  aggiunge un solo span a tutti gli elementi p selezionati, quindi crea tanti span quanti sono gli elementi p. Come creare però dei nuovi elementi in base ai dati?

Elementi e dati fantasma

Torna ora all’associazione dei dati agli elementi della pagina. Nell’unità dedicata hai imparato a usare il metodo data(): d3.select("p#span-container").selectAll("span").data([1,2,3,4]) . Ora tutti gli elementi span dentro il contenitore p hanno un dato associato per posizione: 1 al primo span, 2 al secondo, 3 al terzo e 4 al quarto. Ma se il numero di span non corrisponde al numero di dati?

  1. Nessuno span – La selezione tornata da selectAll() è vuota e i dati non vengono associati
  2. Pochi span rispetto ai dati – I primi dati vengono associati agli span presenti, quelli di troppo si perdono
  3. Perfetta corrispondenza tra span e dati – Un dato per ogni span
  4. Troppi span rispetto ai dati – Tutti i dati vengono associati ai primi span, quelli di troppo rimangono senza dati

L’obiettivo è costruire un documento data-driven, in cui sono i dati che guidano il documento, non il contrario. Nei primi due casi, quindi, vorresti poter creare nuovi span per consumare tutti i dati a disposizione, mentre nell’ultimo caso vorresti poter rimuovere gli span in eccesso per riflettere la quantità di dati a disposizione. A valle del metodo data(), quindi, ci vorrebbero dei metodi che permettano di agire sugli elementi mancanti (magari con un append()) o su quelli di troppo (magari con un remove()).

Il metodo enter(), ovvero la creazione

Sei nel primo caso, un contenitore vuoto e dei dati in ingresso. Vuoi creare nuovi span per accomodare i tuoi dati: d3.select("p#span-container").selectAll("span").data([1,2,3,4]).enter().append("span") . Il risultato è proprio quattro nuovi span all’interno del contenitore p, ognuno associato al suo dato.

Se non sei rimasto confuso, allora ti consiglio di guardare meglio e confonderti… 🙂 Se gli span non esistono al momento della selezione, il selectAll() torna una seleziona vuota. Come può agire il metodo data(), che effettua un join tra elementi e dati, su una selezione vuota? In realtà una selezione vuota è una selezione a tutti gli effetti, che ha tutto il diritto di esistere al pari delle altre. E un join con una lista vuota ritorna semplicemente un insieme vuoto, anch’esso perfettamente valido.

In realtà il metodo data() tiene memoria delle associazioni tra elementi e dati, ma anche degli elementi non associati e dei dati non associati. Normalmente ritorna la lista delle associazioni, ma il metodo enter() permette invece di selezionare l’altra lista, quella dei dati non associati. Ricorda che si tratta in fondo sempre di array e di metodi che agiscono ciclicamente sugli elementi di questi array. A valle di enter() i metodi successivi agiranno quindi tante volte quanti sono i dati non associati. Se per prima cosa fai un append(), quindi, avrai creato uno span per ogni dato non associato in precedenza, per cui tanti span quanti te ne servono per associare tutti i dati disponibili. Proprio quello di cui avevi bisogno, sia per il caso 1) che per il caso 2) visti prima.

Il metodo remove(), ovvero la distruzione

Poniti ora nel caso 4), in cui hai più elementi che dati. Vuoi fare esattamente il contrario, selezionare a valle di data() la lista degli elementi non più associati ad alcun dato. Ci pensa il metodo exit(): d3.select("p#span-container").selectAll("span").data([1,2,3]).exit().remove() . Se prima gli span erano quattro, ora hai eliminato l’ultimo e sono rimasti in tre. Naturalmente puoi fare quello che vuoi a valle di exit(), non solo rimuovere elementi.

Dati freschi

Rimane il caso 3), quello in cui ci sono già tutti gli elementi che servono per i dati, quello che in realtà hai visto fin qui. Per gestirlo infatti non serve alcun metodo aggiuntivo: il metodo data() già ritorna la lista di elementi associati con i dati, quindi i metodi a valle già agiscono sugli elementi con i dati aggiornati. In questo caso, infatti, i metodi enter() e exit() tornano liste vuote, per cui i metodi successivi non hanno alcun effetto.

Crea, aggiorna, distruggi

A questo punto la strategia per gestire qualsiasi tipo di dato dinamico, di cui non conosci a priore né la quantità né l’informazione contenuta, è piuttosto semplice e schematica: devi prevedere sempre le tre fasi di aggiornamento, creazione e distruzione, sapendo che se non dovessero essere necessarie, comunque non avrebbero effetto. Ecco un esempio completo, sulla scia delle word cloud che hai già visto nelle precedenti unità.

Ed ecco qui il risultato, aggiornato opportunamente ogni qualche secondo.

Word cloud…

Il join per chiave ti permette di aggiungere parole nuove quando serve, di aggiornare quelle che hanno nuovi dati, di barrare quelle che ci sono già ma non hanno più dati, di far tornare in chiaro quelle che hanno di nuovo dati. Tutti i casi possibili, insomma. In tre sole mosse.

Dati multi-dimensionali

A questo punto l’utilità delle selezioni annidate che hai visto in una precedente unità è evidente. Se al metodo data() passi un array semplice, puoi costruire una struttura lineare di elementi. Se però si tratta di dati annidati, per esempio un array di array, le selezioni annidate ti permettono facilmente di costruire strutture bidimensionali, come le tabelle. E anche oltre, se i dati sono particolarmente complessi. Ecco un esempio di tabella che mostra il rapporto tra uomini e donne nella popolazione di due regioni italiane per tre differenti anni (fonte: ISTAT).

Ed ecco il codice.

Hai appena visto una struttura tridimensionale costruita sfruttando le selezioni annidate. Non è semplicissimo seguire il processo passo passo, devi prendere confidenza con il fatto che associ dati sempre diversi agli elementi padri, ai figli e ai nipoti. I padri (le righe, elementi tr) hanno gli oggetti che rappresentano gli anni, i figli (le celle, elementi td) hanno gli oggetti che rappresentano le regioni. Hai preso questi ultimi direttamente dai dati associati ai padri, grazie al fatto che data() ammette anche una funzione che ha come parametro il dato associato all’elemento corrente e che ritorna un’array. I dati dei nipoti (i quadratini, elementi span), invece, volendo mostrare la popolazione invece che semplicemente scrivere un numero, li hai un po’ inventati, costruendo un array fittizio a partire dal numero della popolazione totale. In generale, se vuoi creare N elementi (per esempio paragrafi): d3.select("#container").selectAll("p").data(d3.range(N)).enter().append("p") .

Letture: 250