D3.js – Usando Mapas

En esta entrada de blog veremos como hacer mapas en D3.js y como agregar figuras usando la geolocalizaci贸n que tengan. Partiremos introduciendo para que sirven los c贸digos y luego iremos mostrando los resultados esperados.

Promesas

Primero tenemos que partir hablando de las “promesas” en javascript. Viendo la definici贸n en MDN, tenemos que una promesa es un “objeto Promise (Promesa) que es usado para computaciones as铆ncronas. Una promesa representa un valor que puede estar disponible ahora, en el futuro, o nunca”. En simple, es otra forma de usar callbacks, es decir, sirve para asegurarnos que una acci贸n ocurra despu茅s que otra haya terminado.

El siguiente c贸digo sirve para poder hacer varias promesas con las funciones de D3.js que leen archivos, tales como d3.json o d3.csv .

var PromiseWrapper = (xhr, d) = > new Promise(resolve = > xhr(d, p => resolve(p)));

//Nos sirve para poder cargar los archivos y luego ejecutar createMap
     Promise
        .all([
            PromiseWrapper(d3.json, "world.geojson"),
            PromiseWrapper(d3.csv, "cities.csv")
        ])
        .then(resolve => {
            createMap1(resolve[0], resolve[1]);
        });

Lo que est谩 haciendo, es cargar los 2 archivos solicitados, y una vez que tenga los 2, se resuelve retornando el resultado del wrapper en su orden respectivo, es decir, resolve[0] tiene los datos del archivo world.geojson y resolve[1] tiene los datos de cities.csv. Si se dan cuenta, usamos inmediatamente los resolve como par谩metro de la funci贸n createMap1. Esta funci贸n la crean ustedes.

驴Por qu茅 usamos las promesas aqu铆? Como dijimos previamente, tienen un rol como el de los callbacks, por lo que estamos buscando hacer que el c贸digo se ejecute una vez que contemos con toda la data que necesitamos para trabajar.

GeoJSON

Los archivos GeoJSON, son b谩sicamente json, pero que representan a una geograf铆a. Si en alg煤n momento necesitan el mapa de alguna ciudad o pa铆s, tendr铆an que buscar algo como “chile geojson”, y si tienen suerte, alguien ya hizo el GeoJSON del lugar que buscan. Tengan en mente que si el mapa tiene mucho detalle, ser谩 mas pesado, y por lo tanto, ser谩 m谩s lenta la visualizaci贸n que hagan.

Haciendo el Mapa

Cuando se hace un mapa en D3.js, lo primero que tienen que decidir, es que tipo de proyecci贸n usar谩n. Hay que tener en mente que la Tierra es una geoesfera, por lo que hay varias formas de llevarla a un plano. En la documentaci贸n pueden encontrar las projections que m谩s les gusten, pero se suele usar geoMercator.

En cuanto al c贸digo, lo que hace una proyecci贸n es llevar coordenadas de latitud y longitud terrestre, a los puntos en pixeles para poder visualizarlo.

El geoPath contendr谩 finalmente la figura a dibujar, es decir, el mapa que veremos despu茅s.

let ancho=1000;
let projection = d3.geoMercator()
        //que tan cercano
            .scale(200)
            //igual que en los casos pasados
            .translate([ancho / 2, 250])
            //desde el centro, lo podemos mover
            .center([0, 0]);
let geoPath = d3.geoPath().projection(projection);

Lo siguiente no es necesario para hacer un mapa, pero lo haremos para ilustrar algunas cosas que se pueden hacer. Si analizan el GeoJSON, ver谩n que tienen un array llamado “features”, esto es com煤n para todo GeoJSON que lleguen a utilizar. Por simplicidad, usaremos let featureData = countries.features; . Tenemos en cuenta que countries hace referencia al resolve que tiene world.geojson.

       //features es un array con cada pais
        var featureData = countries.features;

        //geoArea nos da el area dado un GeoJson
        //extent nos devuelve el minimo y maximo valor
        //var realFeatureSize = d3.extent(featureData, d => d3.geoArea(d));
        var realFeatureSize = d3.extent(featureData, function (d) {
            return d3.geoArea(d)
        });
        console.log("M铆nimo y M谩ximo: ");
        console.log(realFeatureSize);

        //hacemos un escala para los colores
        //veamos cual es el rango
        //https://github.com/d3/d3-scale#scaleQuantize
        var newFeatureColor = d3.scaleQuantize()
            .domain(realFeatureSize)
            .range(colorbrewer.OrRd[5]);

La variable realFeatureSize contiene el valor del 谩rea m谩s grande y m谩s chica, con esto podemos hacer una escala de colores dependiendo del 谩rea de un pa铆s.

Como siempre, para finalmente hacer el dibujo, tenemos que usar un “data.enter”.

 //Nada nuevo, aqui hacemos el dibujo
        d3.select("#geoOrthographic1").selectAll("path")
            .data(countries.features).enter()
            .append("path")
            .attr("d", geoPath)
            .attr("id", d => d.id)
            .attr("class", "countries")
            .style("fill", d => {
                //AQUI SE USA LA ESCALA DE COLORES EN BASE AL AREA DE CADA PAIS

                if (d.id != "CHL") {
                    return newFeatureColor(d3.geoArea(d))
                } else {
                    return "red";
                }
            })
            .style("stroke", d => d3.rgb(newFeatureColor(d3.geoArea(d))).darker());

En este ejemplo, ademas hemos intervenido para que cuando se identifique Chile se utilice otro color.

Por 煤ltimo, si se quieren mostrar los meridianos y paralelos lo hacemos con el siguiente c贸digo.

        let graticule = d3.geoGraticule();

        //dibujamos la grilla
        d3.select("#geoOrthographic1").insert("path", "path.countries")
            .datum(graticule)
            .attr("class", "graticule line")
            .attr("d", geoPath);

El resultado con lo anterior, deber铆a verse similar a la siguiente imagen.

Agregar Elementos al Mapa

Ahora que ya tenemos un mapa, agregar elementos es como siempre se ha hecho, s贸lo que para la posici贸n usaremos la proyecci贸n que generamos al principio. 脡sta nos entrega las coordenadas en pixeles dada una latitud y longitud.

  // dibujamos las ciudades
        d3.select("#geoOrthographic1").selectAll("circle")
            .data(cities).enter()
            .append("circle")
            .attr("class", "cities")
            .attr("r", 10)
            .attr("cx", d => projection([d.x, d.y])[0])
            .attr("cy", d => projection([d.x, d.y])[1]);

        //dibujamos los textos
        d3.select("#geoOrthographic1").selectAll("text")
            .data(cities).enter()
            .append("text")
            .attr("x", d => projection([d.x, d.y])[0])
            .attr("y", d => projection([d.x, d.y])[1])
            .style("font-size", "20px")
            .style("font-family", "sans-serif")
            .style("fill", "black")
            .text(d => {
                return d.label
            });

        //dibujamos charmanders
        d3.select("#geoOrthographic1").selectAll("images")
            .data(cities).enter()
            .append("svg:image")
            .attr("xlink:href", "pokemon_PNG154.png")
            .attr("x", d => projection([d.x, d.y])[0])
            .attr("y", d => projection([d.x, d.y])[1])
            .attr('width', 100)
            .attr('height', 100)

El resultado final con los elementos agregados deber铆a ser como la siguiente imagen.

C贸digo Fuente

Los c贸digos vistos aqu铆 son una adaptaci贸n de los hechos por Elijah Meeks. Los c贸digos adaptados pueden ser descargados aqu铆. Tambi茅n pueden ver un c贸digo algo m谩s complejo aqu铆, encontran un mapa muy similar al visto en esta entrada de blog, otro donde se incorpora una representaci贸n en 3 dimensiones, y un 煤ltimo en el que se implementa un zoom al mapa.