Skip to content

Instantly share code, notes, and snippets.

@riccardoscalco
Last active August 29, 2015 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save riccardoscalco/2163ada1b4681c49d24c to your computer and use it in GitHub Desktop.
Save riccardoscalco/2163ada1b4681c49d24c to your computer and use it in GitHub Desktop.
mappa
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Prerequisiti Software Suggeriti

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)

Mappa disastri

#1 Template iniziale

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>

Elemento SVG

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.


Aggiustamenti finali

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 containerper 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.

Il viewbox

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?


#2 Disegnare la mappa

Il file topojson

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...

Importare il file

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.

Visualizzare la mappa

Al fine di visualizzare la mappa è necessario specificare

  • la topologia che si vuole usare, se land o countries
  • 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.

Aggiustare colori e posizione.

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);

#3 Data Join

Importazione dei dati

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).

Definizione delle scale

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]);
    ...

Visualizzazione dei dati

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;
      }

Il data join

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 rimanenti E-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)).


**#4 Transizioni **

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");

#5 Continuate voi

  • modificare lo stile
  • inserire dei tooltip
  • provare altre proiezioni
  • ...

Link utili

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment