- Web browser Chrome o Firefox
- editor testuale Sublimetext
- Python2 o Python3
Se necessario, attivare un server locale mediante il seguente comando Python (2 o 3) internamente alla cartella di lavoro
$ python2 -m SimpleHTTPServer 8008
$ python3 -m http.server
oppure lanciando chrome con la flag
$ open -a Google\ Chrome --args --disable-web-security (MAC)
$ google-chrome --disable-web-security (LINUX)
$ chrome.exe --disable-web-security (WINDOWS)
Creiamo il file index.html
con il seguente contenuto:
<!DOCTYPE html>
<!-- Declaring that the document contains HTML5 mark-up with the HTML5 doctype -->
<html>
<head>
<meta charset="utf-8"> <!-- Declaring the character set -->
<!-- defines the ratio between the device width and the viewport size -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type='text/css'></style>
</head>
<body></body>
<script></script>
</html>
Osserviamo la presenza di tre distinte sezioni
<style type='text/css'></style>
per il codice CSS<body></body>
per il codice HTML<script></script>
per il codice Javascript
Richiamiamo le librerie javascript all'interno del tag head
, queste si trovano nella sotto-cartella ./js
<head>
...
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js">
</script>
<style type='text/css'></style>
</head>
Creiamo un div
con id="container"
entro il tag <body>
:
<body>
<div id="container"></div>
</body>
ed osserviamo la presenza el nuovo elemento all'interno del DOM per mezzo della console.
Cos'è il Document Object Model (DOM)? Dalla Mozilla Developer Network: The Document Object Model (DOM) is an API for manipulating HTML and XML documents. It provides a structural representation of the document, enabling you to modify its content and visual presentation by using a scripting language such as JavaScript. Provare a rappresentare mediante una grafo ad albero il seguente documento html:
<body>
<section id="s1">
<div>
<h1>
Titolo
</h1>
<p>
Paragrafo
</p>
</div>
</section>
<section id="s2">
<ul>
<li>
<a href="http://www.tuxtax.ch">
tuxtax
</a>
</li>
item
<li>
</li>
item
</ul>
</section>
</body>
Appendiamo un elemento SVG al div
appena creato:
<script>
var width = 960;
var height = 500;
var svg = d3.select("#container").append("svg")
.attr("id", "chart")
.attr("width", width)
.attr("height", height);
</script>
ed aggiungiamo del codice CSS al solo scopo di visualizzare gli elementi sinora inseriti:
<style type='text/css'>
#container {
border: 1px solid #000;
}
svg {
border: 1px solid red;
}
</style>
Ora possiamo visualizzare il file index.html
al browser.
Al fine di centrare il grafico e renderlo responsive definiamo gli attributi viewBox
e preserveAspectRatio
per l'elemento SVG precedentemente creato:
var svg = d3.select("#container").append("svg")
.attr("id", "chart")
.attr("viewBox", "0 0 960 500") // make it
.attr("preserveAspectRatio", "xMidYMid") // responsive
.attr("width", width)
.attr("height", height);
e aggiungamo il seguente codice jQuery in coda allo script:
<script>
...
// Responsive
var chart = $("#chart"),
aspect = chart.width() / chart.height(),
container = chart.parent();
var resize = function() {
var targetWidth = container.width();
chart.attr("width", targetWidth);
chart.attr("height", Math.round(targetWidth / aspect));
};
$(window).on("resize", resize).trigger("resize");
$(window).on("ready", resize).trigger("resize");
</script>
Qual è il significato del precedente codice?
Infine aggiungiamo due righe CSS all'elemento container
per definire larghezza e margini:
#container {
border: 1px solid #000;
width: 90%;
margin: auto;
}
Proviamo ora a ridimensionare la finestra del browser per vedere come cambia il box.
Qual è il significato dell'attributo viewBox
?
Il seguente codice appende un cerchio a ciascun angolo del box:
svg.append("circle")
.attr({"cx":"0", "cy":"0", "r":"10"})
.style("fill","red");
svg.append("circle")
.attr({"cx":"960", "cy":"0", "r":"10"})
.style("fill","blue");
svg.append("circle")
.attr({"cx":"960", "cy":"500", "r":"10"})
.style("fill","green");
svg.append("circle")
.attr({"cx":"0", "cy":"500", "r":"10"})
.style("fill","yellow");
viewBox
definisce l'intervallo entro il quale i punti (x,y)
devono stare affinché vengano visualizzati.
E se nei nostri dati abbiamo valori che cadono esternamente all'intervallo?
Il file world.json
contiene in formato topojson la mappa del mondo
{
"type": "Topology",
"transform": {
"scale": [0.03600360036003601, 0.017366249624962495],
"translate": [-180, -90]
},
"objects": {
"land": {
"type": "MultiPolygon",
"arcs": [
[
[0]
],
[
[1]
],
...
],
[
[296]
]
]
},
"countries": {
"type": "GeometryCollection",
"geometries": [{
"type": "Polygon",
"id": 4,
"arcs": [
[297, 298, 299, 300, 301, 302]
]
}, {
"type": "MultiPolygon",
"id": 24,
"arcs": [
[
[303, 304, 211, 305]
...
E' importante notare che sono presenti due oggetti, land
e countries
, i quali definiscono due topologie.
Come vedremo, land
definisce unicamente i bordi della terra emersa, mentre countries
definisce i bordi degli stati.
Come si fa a creare un file topojson? Qui la spiegazione...
Facciamo riferimento alle API di D3.js per importare il file json:
d3.json(url[, callback])
Creates a request for the JSON file at the specified url with the mime type "application/json". If a callback is specified, the request is immediately issued with the GET method, and the callback will be invoked asynchronously when the file is loaded or the request fails; the callback is invoked with two arguments: the error, if any, and the parsed JSON. The parsed JSON is undefined if an error occurs.
Inseriamo il codice per la richiesta del file json sotto la variabile svg
:
var svg = d3.select("#container").append("svg")
.attr("id", "chart")
.attr("viewBox", "0 0 960 500") // make it
.attr("preserveAspectRatio", "xMidYMid") // responsive
.attr("width", width)
.attr("height", height);
d3.json("./map/world.json", function(error, topology) {
console.log(topology);
});
...
Nella console di chrome dovrebbe apparire il contenuto di
topology
.
Al fine di visualizzare la mappa è necessario specificare
- la topologia che si vuole usare, se
land
ocountries
- la proiezione geografica, di cui si ha una vasta scelta qui
Il codice seguente visualizza la mappa:
var map = svg.append("g")
.attr("class","map");
var projection = d3.geo.mercator(); // projection
var path = d3.geo.path()
.projection(projection); // the path SVG object created by the projection function
d3.json("./map/world.json", function(error, topology) {
console.log(topology);
var data = topojson.feature(topology, topology.objects.land); // topology
map.append("path")
.datum(data)
.attr("d", path);
});
La mappa risulterà essere un elemento path
che appenderemo ad una variabile map
.
La variabile map
è un elemento gruppo (g
), questa scelta è utile per raggruppare assieme la mappa con gli elementi SVG, raffiguranti i dati, che saranno creati a breve.
Cominciamo col definire lo style della mappa, aggiungiamo le seguenti linee al codice CSS:
.map {
fill: #636363;
stroke: #fff;
stroke-width: 0.5px;
}
Ora che visualizziamo il corpo ed i contorni della mappa con colori differenti, è efficace osservare con cambia la visualizzazione cambiando la topologia, scegliamo countries
anziché land
:
var data = topojson.feature(topology, topology.objects.countries);
Rimane ora da rimpicciolire e centrate la mappa.
Centriamo la mappa sulle coordinate di longitudine e latitudine di Milano ([9.1815,45.4773]
) e scegliamo la scal più opportuna:
var projection = d3.geo.mercator()
.center([9.1815,45.4773]) // long, lat of Milan
.scale(100);
Aiutarsi con le funzioni translate
e rotate
per personalizzare posizione ed orientazione della mappa:
var projection = d3.geo.mercator()
.center([9.1815,45.4773]) // long, lat of Milan
.rotate([0,0,0])
.translate([480,250])
.scale(100);
I dati che andremo a visualizzare nella mappa sono contenuti nel file data.json
, si tratta di una lista ([ ... ]
) di oggetti ({ ... }
), ciascuno dei quali ha chiavi lat
, lng
, country
, code
e displaced
ed associati valori.
[{
"lat": 18.25,
"country": "Jamaica",
"lng": -77.5,
"code": "JM",
"displaced": 2000
}, {
"lat": 13.0,
"country": "Nicaragua",
"lng": -85.0,
"code": "NI",
"displaced": 3000
}, {
"lat": -12.5,
"country": "Angola",
"lng": 18.5,
"code": "AO",
"displaced": 6361
}, {
...
}]
Ancora una volta importiamo i dati mediante la funzione d3.json
:
d3.json("./data/data.json", function(error, data) {
console.log(data);
});
Scegliamo di visualizzare il valore delle chiave displaced
, relativa a ciascun stato, mediante un cerchio di area proporzionale al dato e centrato sullo stato in questione (dunque utilizzeremo anche le chiavi lat
e lng
).
La definizione di opportune scale di conversione ci permette di mappare i valori numerici dei dati (domain
) nelle dimensioni del viewBox
(range
), questo ci assicura che tutti i dati saranno visualizzati a schermo.
Nel nostro caso definiamo una relazione di proporzionalità tra il raggio del cerchio e la radice quatrata del valore di displaced
(essendo l'area proporzionale a quadrato del raggio):
d3.json("./data/data.json", function(error, data) {
console.log(data);
var rScale = d3.scale.sqrt()
.domain([d3.min(data, function(d) { return d.displaced; }),
d3.max(data, function(d) { return d.displaced; })
])
.range([10, 70]);
...
Appendiamo un elemento gruppo (g
) alla variabile map
, perché vogliamo raggruppare tra loro i cerchi.
Sebbere quest'ultima operazione non sia necessaria, essa può esser utile qualora volessimo applicare la medesima trasformazione ad ogni cerchio, basterà infatti applicarla al gruppo.
Selezioniamo i cerchi presenti e applichiamo il data join
(vedi oltre):
var circles = map.append("g")
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx",function(d) { return projection([d.lng,d.lat])[0]; })
.attr("cy",function(d) { return projection([d.lng,d.lat])[1]; })
.attr("r",function(d) { return rScale(d.displaced)});
Notate l'uso della funzione projection
, essa mappa le coordinate geografiche in punti entro il viewBox
.
La funzione
projection
viene chiamate due volte per ogni cerchio, questo è poco efficiente, come possiamo migliorare il codice?
Notate inoltre come la scala precedentemente definita venga chiamata con ingresso valori legati al dato d
e restituisca il valore del raggio.
Infine, apportiamo delle righe di stile ai cerchi
circle {
fill: #d1d1d1;
opacity: 0.5;
stroke: #131313;
}
L'operazione di data join è contenuta nel frammento di codice
.selectAll("circle")
.data(data)
.enter()
.append("circle")
Il data join è una delle novità più importanti introdotte dalla libreria d3.js, a proposito Mike Bostock ha scritto:
Instead of telling D3 how to do something, tell D3 what you want. You want the circle elements to correspond to data. You want one circle per datum. Instead of instructing D3 to create circles, then, tell D3 that the selection "circle" should correspond to data. This concept is called the data join. Data points joined to existing elements produce the update selection. Leftover unbound data produce the enter selection, which represents missing elements. Likewise, any remaining unbound elements produce the exit selection, which represents elements to be removed.
In altre parole, il data join è un esempio di programmazione dichiarativa, grazie alla quale è sufficiente definire la logica della computazione, anziché descrivere l'intero flusso di lavoro come accade nella programmazione imperativa.
Nel caso del data join è sufficiente dichiarare a quale selezione di elementi presenti nel DOM associare i dati. C'è però una complicazione, va da sè infatti che il numero di elementi selezionati (E)
possa essere minore, maggiore o uguale al numero di dati (D)
, distinguaimo allora i seguenti casi:
E = D
: alla selezione viene fatto un update con i nuovi dati (si parla di update selection)E > D
: solo ad una parte della selezione viene fatto un update, quella fino ad esaurimento dei dati. I rimanentiE-D
elementi diventano la exit selection e generalmente vengono eliminati.D > E
: alla selezione viene fatto un update, ma avanzano dati. A quest'ultimi vengono associati nuovi elementi, che vanno a formare la enter selection.
Nel nostro caso, selezioniamo tutti gli elementi cerchio (selectAll("circle")
) ed associamo i dati (.data(data)
).
Non essendoci elementi cerchio il quel ramo del DOM, solo la enter selection è popolata, perciò ha senso solo agire su di essa (.enter()
) ed appendere dei cerchi (.append(circle)
).
Immaginiamo di volere far comparire i cerchi un po' alla volta, prima i più piccoli e solo successivamente i più grandi.
Cominciamo con cambiare la opacità iniziale dei cerchi:
circle {
fill: #d1d1d1;
opacity: 0;
stroke: #131313;
}
Definiamo poi una seconda scala, simile alla prima, in cui però il range
sarà usato per calcorale dopo quanti secondi il cerchio dovrà apparire (così i cerchi più grossi appariranno dopo)
var rScale = d3.scale.sqrt()
.domain([d3.min(data, function(d) { return d.displaced; }),
d3.max(data, function(d) { return d.displaced; })
])
.range([10, 70]);
var timeScale = d3.scale.sqrt()
.domain([d3.min(data, function(d) { return d.displaced; }),
d3.max(data, function(d) { return d.displaced; })
])
.range([10, 10000]); // 10 ms to 10 s
Notate che l'ultimo cerchio, il più grande di tutti apparirà solo dopo 10 secondi.
Infine applichiamo le transizioni:
circles.transition()
.duration(1000) // 1 sec
.delay(function(d) { return timeScale(d.displaced); })
.style("opacity","0.5");
- modificare lo stile
- inserire dei tooltip
- provare altre proiezioni
- ...