Setup Menus in Admin Panel

School.Dataninja.it

Visualizzazioni dati con d3js: associare i dati agli elementi della pagina

Associare i dati agli elementi della pagina è il primo passo per poi usarli nel modificarne l'aspetto grafico in base ai dati stessi.

Hai visto come manipolare le pagine web con d3js, prima selezionando gli elementi, poi modificandone gli attributi. Hai visto anche come ottenere dei dati che siano disponibili alla tua applicazione sotto forma di array e oggetti javascript, quello che manca è collegare questi dati agli elementi della pagina, così da impostarli e modificarli con un approccio realmente data-driven.

Il join tra strutture dati

Immagina di avere due liste affiancate. A sinistra un elenco di elementi della pagina, per esempio paragrafi, a destra un elenco di dati, per esempio numeri interi.

Elemento Dato

La visualizzazione a tabella già mostra un’associazione tra elemento (a sinistra) e dato (a destra). Le due liste sono indipendenti (gli elementi span sono nella pagina, i dati vengono da una fonte esterna), sei tu che decidi come associarle tra di loro. A quale elemento associare i tuoi dati, se a una serie di span, di p o di circle. In questo caso probabilmente vuoi ottenere una sorta di word cloud, in cui ogni parola a sinistra abbia una dimensione in px uguale al numero a destra. Viene quindi prima l’associazione tra elemento e dato, poi la modifica degli attributi degli elementi in base ai dati associati. Ed ecco il risultato: .

Join per posizione

Il giochino che hai appena visto si basa su un join per posizione. La parola join significa semplicemente associare o collegare, in questo caso una parola con un numero intero. Per posizione invece vuol dire che si associano a due a due gli elementi di due liste in base alla loro posizione nella lista: la prima parola con il primo numero, la seconda parola con il secondo numero, ecc.

Questo significa che se le due liste vengono ordinate in maniera differente (stessi elementi, ma ordine diverso), il join fornisce coppie parola – numero diverse. In altre parole, l’ordine è importante.

Se inoltre il numero di elementi nelle due lista è diverso (dieci parole e otto numeri, per esempio, o viceversa), saranno sempre gli ultimi, da una parte o dall’altra, a rimanere soli, non associati. In questo caso, nella gestione degli elementi della pagina successiva al join, devi prevedere queste situazioni: cosa accade alle parole senza un numero associato? Le cancelli o associ loro un valore di default, per esempio quello del testo normale? E se hai più numeri che parole? Ignori quelli di troppo?

Join per chiave

Considera ora il classico caso della ricerca di una serie di persone nell’elenco telefonico (anche se suona ormai un po’ anacronistico nell’era delle rubriche degli smartphone in cloud). Hai una lista di soli nomi da una parte e l’elenco telefonico dall’altra, che altro non è che una lista di coppie nome – numero. Di fatto vuoi fare un join tra le due liste, ma non ti interessa la posizione dei nomi! Non vuoi certo associare il primo nome della tua lista con il primo numero che trovi nell’elenco telefonico… In realtà vuoi cercare i nomi della tua lista all’interno dell’elenco telefonico e quando trovi una corrispondenza (stesso nome da una parte e dall’altra) vuoi prendere il corrispondente numero di telefono. Il nome della persona cercata è la chiave della ricerca. Vuoi quindi effettuare un join per chiave.

Lasciando stare persone e numeri di telefono, torna a considerare l’esempio di prima, questa volta leggermente modificato.

Elemento  Dato

Puoi verificare facilmente che le coppie di elementi non sono più correttamente associate, in generale il contenuto dell’elemento span a sinistra non corrisponde con l’attributo word dell’oggetto a destra. Ma è proprio questa la chiave da usare per un corretto join. Se rifacessi la word cloud esattamente come prima, sfruttando un join per posizione, le dimensioni delle varie parole sarebbero sbagliate.

Un join per chiave, invece, permette di ottenere il risultato corretto, indipendentemente dall’ordine delle due liste: .

Il metodo data()

Una volta che hai effettuato una selezione di elementi della pagina (un array di span, per esempio, usando d3.selectAll("span") ) e hai a disposizione un array di dati che vi vuoi associare (un array di numeri interi, per esempio), d3js permette di effettuare il join, sia per posizione che per chiave, mediante il metodo data(). Nel primo caso è sufficiente passare al metodo un array di dati (primo elemento della selezione con il primo elemento dei dati, secondo elemento della selezione con il secondo elemento dei dati, ecc.), nel secondo devi specificare con una funzione di callback quale chiave usare per il join per chiave.

data([array])

Puoi effettuare un semplice join per posizione mediante il metodo data() passandogli un array di dati: d3.selectAll("span").data([1,2,3,4,5,6]) . A valle ritorna la selezione di span, ma con associati gli elementi dell’array di dati: il numero 1 al primo span, il numero 2 al secondo span, ecc. Ecco come esempio il codice che ha generato la prima word cloud che hai visto in questa unità:

L’ultima istruzione è particolarmente importante: nell’unità sui metodi text() e html() (ma anche in quella su stili e attributi) hai visto come modificare il contenuto degli elementi della pagina passando come valori dei dati statici: stringhe, per lo più. Ma nell’esempio hai visto anche che è possibile passare una funzione che calcoli al volo e ritorni quei valori. In quel caso non c’erano dati associati agli elementi da modificare, così la funzione veniva chiamata senza parametri: function() { return ...; } . In realtà d3js chiama quelle funzioni di callback passando ben due parametri: i dati associati e la posizione nella lista della selezione, accessibili solo se li nomini esplicitamente usando function(d,i) { return ...; } .

Nell’esempio qui sopra, quindi, a valle di data() modifico lo stile “font-size” con una funzione che viene eseguita per ogni span della selezione. A ogni esecuzione il parametro d è diverso, perché a ogni span è associato un numero intero diverso: per il primo span d è uguale a 12, per il secondo è uguale a 34, ecc. Il valore associato alla regola “font-size” sarà quindi diverso per ogni span: per il primo è “12px”, per il secondo è “34px”, ecc.

Prova il codice nella console del tuo browser, modificando i numeri all’interno dell’array di dati. Te lo riporto qui per comodità su un’unica riga e senza commenti, così puoi copiarlo e incollarlo facilmente: d3.select("#join-per-posizione-your-wordcloud").selectAll("em").data([12,34,21]).style("font-size", function(d) { return d+"px"; }); . Vedrai il risultato qui di seguito: sole cuore amore.

Ecco finalmente un documento data-driven! Benché in questo caso gli span con le varie parole già esistano nel documento, l’associazione dei dati (potenzialmente provenienti da tutt’altra fonte) permette di impostarne la dimensione del font in base ai dati. Puoi non sapere a priori quali siano quei numeri, il tuo documento mostrerà sempre le dimensioni giuste delle parole.

data([array], function(el) { … })

Nel join per chiave devi invece esplicitare la chiave che colleghi opportunamente gli elementi della selezione con i dati, indipendentemente dall’ordine delle due liste. In questo caso l’esempio sopra va modificato leggermente perché il confronto tra le chiavi avviene tra i dati già associati agli elementi della selezione e nuovi dati in arrivo. Agisce quindi nel caso di un aggiornamento dei dati associati alla selezione, non alla prima associazione (nell’unità su creazione, aggiornamento e distruzione capirai bene perché questo non è un limite).

Nota che i due array di dati sono diversi (gli oggetti sono diversi, nel primo caso con il solo attributo word, nel secondo con gli attributi word e size) e anche il loro ordine, relativamente all’attributo word, è diverso (sole, cuore, amore nel primo caso, cuore, amore, sole nel secondo). L’associazione dei nuovi dati è però corretta indipendentemente dal loro ordine rispetto ai primi dati associati alla selezione, proprio perché avviene per chiave (in questo caso word) e non per posizione.

Anche in questo caso, prova a eseguire quel codice nella console del tuo browser. Te lo riporto per comodità senza commenti: d3.select("#join-per-chiave-your-wordcloud").selectAll("em").data([{ word: "sole" },{ word: "cuore" },{ word: "amore" }]).text(function(d) { return d.word; });  e d3.select("#join-per-chiave-your-wordcloud").selectAll("em").data([{ word: "cuore", size: 12 },{ word: "amore", size: 24 },{ word: "sole", size: 16 }], function(el) { return el.word; }).style("font-size", function(d) { return d.size+"px"; }); . Vedrai il risultato qui di seguito:      . Esegui più volte il secondo blocco di codice, modificando i nuovi dati associati: cambiando i numeri in size, ma anche modificando l’ordine delle parole in word.

datum({object})

Come detto, il metodo data() effettua un join tra selezione e dati, per posizione o per chiave. A volte però è utile poter associare direttamente dei dati a uno o più elementi, soprattutto per preparare una selezione a un successivo join per chiave. È possibile farlo con il metodo datum(). Chiamato senza parametri ritorna i dati associati al primo elemento della selezione. Altrimenti associa a ogni elemento il parametro passato, che naturalmente può essere una funzione. Tornando agli esempi precedenti, nel caso in cui nella pagina ci siano già gli em con le parole scritte all’interno, sarebbe utile usare direttamente queste parole come chiave per il join successivo.

Nota la funzione di callback passata al metodo datum(). Ritorna un oggetto che viene associato all’elemento em corrente, ma il valore di word è proprio il testo contenuto nell’elemento em corrente. Come leggerlo? Quando d3js esegue la funzione di callback le passa come parametri il dato associato (d) e la posizione nell’array della selezione (i). Imposta però anche la variabile this all’elemento corrente, in questo caso un em. Si tratta della rappresentazione standard dell’elemento in javascript, per cui può essere selezionato con d3.select(this)  e poi estratto il contenuto testuale con text().

Prova nella console del tuo browser. Prima esegui il join per chiave:  d3.select("#join-diretto-your-wordcloud").selectAll("em").data([{ word: "cuore", size: 12 },{ word: "amore", size: 24 },{ word: "sole", size: 16 }], function(el) { return el.word; }).style("font-size", function(d) { return d.size+"px"; }); . Vedrai che non accade nulla, anzi, la console mostra un errore del tipo “cannot read property ‘word’ of undefined”. Prova invece a fare la prima associazione diretta:  d3.select("#join-diretto-your-wordcloud").selectAll("em").datum(function() { return { word: d3.select(this).text() }; }); . Ora i successivi join per chiave funzioneranno. Qui di seguito puoi vedere il risultato: sole cuore amore.

Questo metodo è molto utile se si usano gli attributi data-* dell’HTML5 direttamente nei tag della pagina, perché si possono far usare a d3js in maniera molto semplice: d3.selectAll("span").datum(function() { return this.dataset; }) .

Accedere ai dati associati

Negli esempi precedenti hai già visto come accedere ai dati associati agli elementi della selezione e usarli per modificarne contenuto, stili, attributi, ecc. Praticamente ogni metodo di d3js che agisce sugli elementi della pagina in scrittura accetta sia un valore statico che una funzione. Nel secondo caso la funzione di callback è sempre chiamata passando due parametri (il dato corrente e la posizione corrente nell’array della selezione) e impostando il this all’elemento corrente.

Per poter usare queste variabili all’interno della tua funzione di callback, devi dar loro un nome. Tipicamente si usa d per il dato e i per l’indice (ma anche data e index, come vuoi). La funzione può fare quello che vuole con queste variabili, l’importante è che al termine ritorni un valore coerente con il metodo che l’ha eseguita. Se per esempio stai impostando la regola di stile “font-size” i valori accettati sono [numero][unità di misura], percentuali, parole chiave come larger o small, ecc. Non puoi tornare un array, per esempio.

Mentre l’indice è sempre un numero intero (che parte da 0), il dato dipende dall’ultimo join che l’ha associato all’elemento corrente della selezione. Sta a te saperlo e quindi usare la variabile d coerentemente. Se si tratta di un tipo semplice (un numero o una stringa) puoi usarlo così com’è, se invece (come spesso avviene) si tratta di un oggetto, puoi accedere a tutti i suoi attributi (e anche metodi, se ce li ha).

La variabile this, invece, contiene l’oggetto javascript che rappresenta l’elemento del DOM corrente, nella sua rappresentazione nativa. Non è una selezione d3, è proprio l’elemento del DOM. Quindi puoi accedere direttamente solo agli attributi e ai metodi standard, così come fatto nell’esempio precedente con this.dataset. Puoi però trasformare questo this in una vera e propria selezione d3 semplicemente selezionandolo: d3.select(this) . A valle puoi usare tutti i metodi delle selezioni di d3. Stesso discorso se decidi di usare invece jQuery: $(this) .

Gestire dati e selezioni annidati

Finora hai visto dati monodimensionali, semplici array di oggetti che vengono associati uno ad uno a una serie di elementi della pagina. Considera però una tabella: si tratta di un oggetto bidimensionale, perché è costituita da una serie di righe, che a loro volta contengono una serie di celle (una per ogni colonna). Questa la rappresentazione in HTML di una semplice tabella 2×2 (quattro celle, due righe e due colonne).

In termini di array, la stessa struttura si può rappresentare con un array di array: [["tr1/td1","tr1/td2"],["tr2/td1","tr2/td2"]] . Si tratta di una struttura dati annidata che vuoi associare a una struttura di elementi anch’essa annidata. Puoi ora applicare quanto visto nell’unità sulle selezioni.

Forse non è facile capire cosa accade al primo sguardo, passa in rassegna il codice riga per riga. Fino al primo metodo data() è tutto già visto. Ora a ogni riga (tr) è associato un dato che è un array: ["tr1/td1","tr1/td2"]  alla prima riga e ["tr2/td1","tr2/td2"]  alla seconda riga. Quindi c’è una selezione annidata delle celle di ogni riga. Ricorda: stai selezionando tutte le celle (td) per ogni riga (tr), non tutte le celle che sono all’interno di una riga. Quindi a valle hai una selezione di selezioni, non una selezione semplice.

Uno degli aspetti notevoli delle selezioni annidate è che i figli possono accedere ai dati associati ai padri. In questo caso, dopo aver selezionato gli elementi td, puoi accedere al dato associato alla riga tr che li contiene. Lo fai direttamente nel metodo data(), che come tutti gli altri metodi accetta anche una funzione oltre che array di dati statici. Occhio che non si tratta della funzione di callback che serve a indicare la chiave nel join per chiave. Quest’ultima è passata come secondo parametro, dopo i dati, mentre la funzione che stai vedendo è passata come primo parametro, al posto dei dati. Quindi ci si aspetta che ritorni i dati da associare.

Ed è proprio così. Alla prima riga è associato l’array ["tr1/td1","tr1/td2"] , per cui alla sua prima cella viene associata la stringa “tr1/td1”, alla seconda la stringa “tr1/td2”, e via così. Alla fine il metodo text() scrive il dato associato alla cella corrente come suo contenuto testuale. Nota che nella prima funzione la variabile d contiene gli array, mentre nella seconda contiene le stringhe. In effetti si tratta di una variabile muta: è visibile solo all’interno della funzione anonima a cui viene passata, ma scompare non appena la funzione ritorna. Ecco perché puoi usare sempre gli stessi nomi di variabile senza problemi.

Non ti resta che provare. Ecco la tabella vuota, riempila eseguendo il codice nella console del tuo browser, te lo rimetto qui senza commenti per comodità: d3.select("#table-2x2 tbody").selectAll("tr").data([["tr1/td1","tr1/td2"],["tr2/td1","tr2/td2"]]).selectAll("td").data(function(d) { return d; }).text(function(d) { return d; }); .

Prima colonna Seconda colonna
(vuoto) (vuoto)
(vuoto) (vuoto)
Letture: 416