Prima di cominciare con questo articolo, vi consiglio di leggere l’articolo Hexagonal Binning, in cui viene spiegata e mostrata questa metodica di aggregazione. Nell’articolo vengono messi a confronto gli scatterplot generati da due diversi insiemi di dati. Viene così evidenziato che per particolari insiemi di dati, soprattutto quelli che si presentano come una distribuzione “sparsa” sul piano XY, può risultare difficile rilevare eventuali clusters o andamenti lineari. A tale scopo viene mostrata la tecnica del binning, sia quella più semplice, il rectangular binning, che quella più complessa, l’hexagonal binning.
Ecco i due dataset salvati come file CSV:
scatterplot01 : è il dataset che genera lo scatterplot “sparso”
scatterplot02 : è il dataset che genera lo scatterplot con andamento lineare
Nell’articolo vengono mostrate alcune figure che illustrano un insieme di dati “sparso” rappresentato prima come scatterplot, e poi come rectangular binning e come hexagonal binning. In questo articolo mostreremo come realizzare il rectangular binning utilizzando la libreria JavaScript D3.
Generalmente ci riferiamo con rectangular binning alla metodica utilizzata, mentre per quanto riguarda il grafico che viene generato da tale tecnica, si preferisce chiamarlo istogramma 2D che abbinato ad una particolare rampa di colori che ne specifica il peso di ciascun bin genera un heatmap..
Qui, impareremo a sviluppare un istogramma 2D con la libreria D3. Faremo uso quindi del linguaggio JavaScript.
Per chi non avesse dimestichezza con la realizzazione di grafici utilizzando librerie JavaScript consiglio vivamente di leggere il libro Beginning JavaScript Charts with jqPlot, D3 and Highcharts. In questo libro sono contenuti numerosissimi esempi (oltre 250 esempi) in cui passo per passo vengono spiegati come si realizzano le tipologie più comuni di grafici su Web utilizzando diverse librerie JavaScript.
Nel framework di D3, abbiamo un plugin specializzato per l’hexagonal binning (d3.hexbin.js) che ci fornisce il layout d3.hexbin, uno strumento essenziale per gestire sia la suddivisione del piano XY in tasselli esagonali, sia la conta dei vari campioni all’interno di ciascun bin esagonale. Strano a dirsi non c’è alcun layout che gestisce il binning rettangolare, anche se teoricamente molto più semplice.
In effetti dando un occhiata in internet, si possono trovare svariati esempi di istogrammi 2D, ma questi utilizzano dati che sono stati già raggruppati in bin rettangolari con le conte già inserite. Quindi spetterebbe all’utente implementare la metodica sottostante. Quindi per prima cosa vedremo come implementare un plugin che effettui lo stesso lavoro effettuato da d3.hexbin. Lo chiameremo d3.bin.
Per la realizzazione sono partito dal codice di d3.hexbin e l’ho modificato per effettuare il rectangular binning. Scaricate il file d3.bin.zip contenente il plugin d3.bin.js. Una volta scompattato copiatelo nella stessa directory della pagina web che ne fa uso (altrimenti correggete il path nella pagina web).
Per capire come utilizzare il plugin e capire come funziona il rectangular binning, seguiamo insieme, passo passo, questo piccolo tutorial. Per esempio, consideriamo un piano XY di dimensione 100×100. All’interno di esso vogliamo rappresentare un insieme di dati. Per semplicità prendiamo solo tre campioni.
var points = [[10,10],[0,0],[30,30]]
Adesso, vogliamo applicare il metodo del rectangula binning su questo piano XY. Vogliamo, per esempio, suddividerlo in quadrati 20×20 (usando la funzione side()). Inoltre vogliamo applicare il metodo all’intera area 100×100 (usando la funzione size()). Quindi definiamo:
var binning = d3.bin()
.size([100,100])
.side(20);
Adesso che abbiamo configurato i parametri del rectangular binning, dobbiamo applicarlo all’insieme dei dati. Per fare ciò, passiamo l’array points come argomento della funzione binning.
var bins = binning(points);
La Fig.4 riporta i risultati del rectangular binning. Abbiamo la creazione di 16 bin 20×20 indicizzate tramite i e j, in cui solo due bin risultano occupate. La bin (i=0,,j=0) ha il valore count = 2, mentre la bin (i=1,j=1) ha count=1. Se adesso analizziamo il contenuto della variabile bins troviamo infatti la struttura dati corrispondente:
[ [[0,0],[10,10]] i=0, j=0, x=0, y=0,
[[30,30]] i=1, j=1, x=20, y=20 ]
Quindi abbiamo due soli bin (i bin con count=0 non vengono considerati). Ciascun bin ha un array con i punti contenuti in esso, gli indici i e j, e i valori x e y che sono le coordinate del vertice inferiore sinistro del quadrato.
Quindi con questo plugin è possibile sottoporre qualsiasi insieme di dati al metodo di rectangular binning, per ottenere come risultato una struttura dati da utilizzare per la rappresentazione grafica tramite la libreria D3.
Adesso che abbiamo visto come utilizzare il plugin, scriviamo il codice JavaScript che legge i dati contenuti in un qualsiasi file CSV per generare un istogramma 2D. In particolare ci riferiremo al dataset contenuto nel file scatterplot01.csv. Questo genera il seguente istogramma.
Da come si può ben vedere in Fig.5, contrariamente allo scatterplot, l’andamento lineare è ben evidente. Ciò è infatti dovuto al fatto che uno scatterplot non tiene conto dei punti sovrapposti, o meglio della loro densità. Lo scatterplot è sì un modo veloce per visualizzare come un insieme di dati si distribuisce nello spazio, ma come abbiamo appena sperimentato non è certamente il modo più corretto per visualizzare le densità dei punti nello spazio XY. Ma adesso passiamo al codice della pagina Web che genera l’istogramma 2D della Fig.2. Poi passeremo ad analizzarne alcune sue parti.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="d3.bin.js"></script>
<style>
body {
font: 12px sans-serif;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.square {
stroke: #fff;
stroke-width: .5px;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 40, right: 40, bottom: 40, left: 40},
w = 380 - margin.left - margin.right,
h = 380 - margin.top - margin.bottom;
var color = d3.scale.linear()
.domain([0, 3])
.range(["yellow", "darkred"])
.interpolate(d3.interpolateLab);
var x = d3.scale.linear()
.domain([0, 100])
.range([0, w]);
var y = d3.scale.linear()
.domain([0, 100])
.range([h, 0]);
var yinv = d3.scale.linear()
.domain([0, 100])
.range([0, h]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var side = 10;
var bins = d3.bin()
.size([w, h])
.side(side);
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")");
var points = [];
d3.csv("scatterplot01.csv", function(error, data) {
data.forEach(function(d) {
d.time = +d.time;
d.intensity = +d.intensity;
points.push([d.time, d.intensity]);
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.selectAll(".square")
.data(bins(points))
.enter().append("rect")
.attr("class", "square")
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y(d.y)-yinv(side); })
.attr("width", x(side))
.attr("height", yinv(side))
.style("fill", function(d) { return color(d.length); }); }); </script>
</html>
Un punto importante da tenere presente è la dimensione dei quadrati con cui effettuare il binning. Infatti a seconda della distribuzione dei dati e del dataset che stiamo analizzando sarà necessario regolare la dimensione di ciascuna bin. Dato che i dati che stiamo analizzando coprono un valore da 0 a 100 sia per l’asse X che per l’asse Y, ed inoltre l’insieme non contiene poi così tanti elementi ho scelto dei quadrati di lato 10 (10 è il valore numerico sulla scala X o Y, non i pixel!!).
var side = 10;
var bins = d3.bin()
.size([w, h])
.side(side);
Nella Fig.6 vediamo come cambia il grafico al variare delle dimensioni dei quadrati.
Un altro parametro da regolare è la gradazione di colore da applicare a seconda dei conteggi per ciascuna bin, cioè a seconda dei campioni del dataset contenuti in ciascuna bin. Per questo esempio ho utilizzato come estremi due colori: il giallo per i valori più bassi e il rosso scuro per i valori più alti. Nel definire la scala dei colori, è possibile regolare il gradiente dei colori definendo un intervallo all’interno della funzione domain(). In questo esempio ho impostato il giallo per il conteggio di valore 1 (voglio ricordare che le bin non contenenti campioni non vengono rappresentate). Per un conteggio equivalente a 3 campioni avremo un quadrato di color rosso scuro. Se il conteggio è superiore tenderà allora ad una gradazione ancora più scura (nero).
var color = d3.scale.linear()
.domain([0, 3])
.range(["yellow", "darkred"])
.interpolate(d3.interpolateLab);
Nella Fig.7 possiamo vedere i diversi effetti che si ottengono regolando l’intervallo.
Inoltre è possibile aggiungere dei contorni alle bin modificando il colore da bianco a nero negli stili CSS.
.square {
fill: none;
stroke: #000;
//black stroke
stroke-width: .5px;
}