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.

Deja un comentario 🤔

This site uses Akismet to reduce spam. Learn how your comment data is processed.