Construyendo un mapa. Un paso a la vez.

1. Requisitos

Este pequeño tutorial asume que D3 y su funcionamiento general ya te son familiares; de no ser así, ¡no te preocupes!; voy a esquematizar de manera muy intuitiva lo que está pasando; de cualquier manera, en la página estaré añadiendo recursos para aumentar tu acervo de conocimiento en D3. ¿Qué te parece si empezamos?

2. Plan de ataque

Como mencioné al inicio de esta serie, la cartografía es un tema bastante amplio y es necesario tomar en cuenta y hacer uso de varios objetos de D3; mas no desesperes; revisemos los conceptos y pasos que vamos a abordar.

  1. Proyecciones y el objeto projection de D3
  2. Cómo conseguir y usar la información geográfica en D3
  3. El objeto path de D3
  4. ¡Manos a la obra! Creando un mapa

Muy bien, entonces atendamos cada punto.

3. Trabajando con proyecciones

3.1 De esfera a plano…

Desde siempre, uno de los grandes problemas de la cartografía ha sido el hecho de que la tierra no es plana (lamento romper tu burbuja si eres una de esas pobres almas perdidas que creen en la teoría de la tierra plana); esto implica que no podemos pasar de una esfera (u ovoide, para ser más precisos) tridimensional a un plano bidimensional directamente. ¿Qué podemos hacer entonces?… ¡proyecciones al rescate!

Lo que una proyección intenta hacer es trasformar la geometría poligonal esférica a una geometría poligonal plana. En este contexto, un polígono es una región geográfica, por ejemplo, un país. Esta transformación poligonal se logra a través de complejos cálculos matemáticos; afortunadamente, de esto se hace cargo D3.

Para visualizar el proceso de proyección de manera más gráfica imagina una calabaza de Halloween. Cada figura tallada en la calabaza es un polígono; por ejemplo, si tallamos dos ojos, la nariz y la boca, tendríamos 4 polígonos. Si colocamos una vela dentro de la calabaza podremos apreciar cómo los polígonos tallados se proyectan en los muros. Esta vez, en lugar de una calabaza, imagina al planeta Tierra. En esta analogía, cada figura tallada en la calabaza podría ser un país, una isla, un continente, etc.

Ahora bien, la proyección de polígonos de nuestra calabaza-Tierra no es igual en los muros que en el techo o en el piso; del mismo modo, la proyección cambiaría si colocamos la calabaza-Tierra dentro de un cilindro o sobre un cono. Por este motivo es que diferentes proyecciones rinden diferentes formas de mapas; algunas proyecciones son mejores que otras para ciertas tareas; por ejemplo, las proyecciones cónicas son muy utilizadas para crear mapas de países específicos; por otro lado, las proyecciones cilíndricas como la de Mercator son bastaste útiles para mostrar mapas de todo el mundo; sin embargo, distorsionan demasiado el tamaño de las regiones geográficas conforme éstas se alejan del ecuador.

Por cierto, si quieres saber más sobre las proyecciones, aquí se ofrece una explicación muy concreta y general; y aquí el INEGI ofrece una explicación muy profunda y completa.

3.2 En concreto ¿Cómo hacerlo en D3?

Para nuestra fortuna, D3 ya ofrece una gran variedad de proyecciones que prácticamente cubren todos los casos que se te podrían ocurrir; e incluso, es posible que tú crees tus proyecciones personalizadas; aunque hacer esto sería una tarea bastante compleja.

Crear un objeto projection en D3 es muy sencillo y basta con invocar a la función adecuada; por ejemplo, para una proyección de Mercator, se invocaría la función d3.geoMercator(); para una proyección cónica se invocaría d3.geoConicConformal(). Aquí dejo un enlace por si quieres conocer todo lo que D3 tiene para ofrecerte en materia de proyecciones.

4. ¿Y, de dónde salen los datos geográficos?

Hasta ahora, hemos visto cómo se hace la proyección de una esfera a un plano; sin embargo, no hemos visto cómo ingresar datos concretos de latitud y longitud de regiones geográficas que nos interese representar.

En el mundo cartográfico, los datos geográficos se almacenan y comparten con diversos formatos que dependen de la institución o software que se esté empleando. En el caso de D3 hay dos formatos principales: GeoJSON y TopoJSON. Como sus nombres los indican, ambos siguen la notación de objetos en JSON. Ya que TopoJSON desprende de GeoJSON y que de cualquier manera D3 sólo trabaja con el formato GeoJSON de manera directa; me enfocaré en este último.

Ahora, ¿de dónde puedes obtener información con el formato GeoJSON? Pues bien, hay varias fuentes en internet; por ejemplo, en geojson-maps.ash.ms puedes obtener información de diversas regiones del mundo; no obstante, si deseas conseguir información específica y detallada de alguna región particular del país donde vives o trabajas, la mejor fuente de datos son los registros creados por las instituciones públicas de cada país; por ejemplo, en el caso de México sería el INEGI; en el caso de los Estados Unidos sería el USGS.

Es importante mencionar que normalmente la información pública que ofrecen las instituciones gubernamentales NO está en formato GeoJSON; por lo cual, es necesario hacer una conversión de formatos. No voy a adentrarme en cómo realizar esta conversión ya que existen muchas herramientas online que son gratuitas e incluso ofrecen opciones para simplificar los mapas cuando estos tienen más información de la que necesitas para tu proyecto. Una de mis herramientas favoritas para este propósito es mapshaper.org

5. El objeto path: dibujando la información geográfica en un lienzo

El ingrediente final para la creación de nuestro mapa es el objeto path, el cual se construye utilizando la función d3.geoPath(projection); en este caso, el parámetro projection se refiere precisamente al objeto projection del que ya hemos hablado; dependiendo del tipo de proyección que quieras emplear, será el tipo de objeto projection que le pases a esta función.

Para usar al objeto path, se le debe invocar como una función (recuerda que en JavaScript una función es también un objeto; que, a su vez, puede tener propiedades y métodos). La manera de invocarlo sería la siguiente: path(geoJsonObject); en donde el parámetro geoJsonObject es precisamente una estructura con el formato indicado por el estándar GeoJSON.

Lo que el objeto path hace es generar una cadena de texto en la cual hay instrucciones que indican cómo debe dibujarse el mapa; estas instrucciones son ingresadas en el atributo “d” de un elemento <path>. ¡Cuidado!, no confundas al objeto path con el elemento <path>; el primero es un objeto de JavaScript, mientras que el segundo es un elemento de HTML.

Por ejemplo, para crear y usar un objeto path, podríamos emplear el siguiente código:

	var path = d3.geoPath(projection);
	var pathString = path(geoJsonObject); //Esta cadena es la que puede usarse en el atributo “d" del element <path>

6. ¿Podemos crear de una buena vez el mapa?

Después de todo el contexto que he dado puede ser que ya comas ansias por crear un mapa en D3; pues bien, no te haré esperar más; de hecho, vamos a crear 3 mapas, cada vez incrementando un poco la dificultad. ¡Empecemos!

6.1. Un mapa burdo

Primero te presentaré el código necesario para crear un mapa muy sencillo.

<svg id="map-demo" height="250" width="400"></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    var geoJsonGeometry = {"type":"LineString",
            "coordinates":[
                [-109.952621, 22.873550], [-117.152752, 32.511857],
                [-106.389995, 31.729324], [-103.188676, 28.983335],
                [-101.830050, 29.821814], [-97.103269, 25.889812],
                [-97.366612, 21.168504], [-94.409246, 18.190767],
                [-90.240085, 21.096935], [-86.818797, 21.067803],
                [-92.307206, 14.541265], [-94.745877, 16.296351],
                [-96.587050, 15.666151], [-105.705476, 20.346444],
                [-114.719173, 31.663829], [-109.396981, 23.349035]]};
    var projection = d3.geoMercator();
    var path = d3.geoPath(projection);
    var pathString = path(geoJsonGeometry);
    d3.select("svg#map-demo").append("path").attr("d", pathString);   
</script>

Si pones este código directamente en un documento HTML, podrás visualizar un mapa burdo que demarca a México; el resultado debe ser muy parecido al siguiente:

Como podrás notar, el código es bastante corto; ahora, voy a explicar lo que está pasando.

En primer lugar, estamos definiendo un elemento <svg> con id “map-demo”; este elemento será el “lienzo” donde el mapa será dibujado.

Posteriormente se requiere importar la librería de D3; la versión más reciente al momento de publicar este post es la versión 5.

El resto del código está embebido dentro del elemento <script>; por lo tanto, a partir de este punto nos dedicaremos a ejecutar instrucciones en JavaScript.

En la primera línea de código, estamos asignando a la variable geoJsonGeometry, un objeto con el formato de GeoJSON; en dicho objeto está toda la información geográfica que estaremos usando para la creación del mapa; este objeto tiene dos parámetros. El primer parámetro, llamado type, indica el tipo de objeto que estamos construyendo; en este caso, se trata de un objeto LineString. El segundo parámetro, llamado coordinates, es un arreglo de coordenadas; cada coordenada es a su vez un arreglo con dos valores: longitud y latitud (en ese orden). Para este ejemplo sencillo estamos utilizando únicamente 16 puntos que demarcan a México; al ser tan pocos puntos, el mapa es bastante tosco; sin embargo, mi interés es que puedas apreciar qué está sucediendo.

En seguida, estamos creando un objeto projection; en este caso el tipo de proyección es la de Mercator pero podría ser alguna otra.

Posteriormente estamos definiendo al objeto path, para lo cual nos auxiliaremos del objeto projection, creado en el paso anterior.

El siguiente paso es crear la variable pathString; es decir, una cadena de texto con las instrucciones necesarias para dibujar al mapa. Con este objetivo en mente, invocamos al objeto path como una función, proveyéndole como argumento al objeto GeoJSON que definimos previamente en la variable geoJsonGeometry.

La línea final de JavaScript, en realidad son varias acciones concatenadas. Primero seleccionamos al elemento <svg> identificado como “map-demo”; después, a dicho elemento le añadimos un elemento <path>; finalmente, al elemento <path> le definimos su atributo “d”, asignándole la variable pathString, misma que contiene las instrucciones para dibujar al mapa.

6.2. Un mapa simple

Muy bien, el ejemplo anterior nos enseñó el mecanismo para dibujar un mapa; sin embargo, es hora de crear un mapa con una apariencia verdaderamente profesional, para lo cual necesitamos datos geográficos con mejor calidad. He preparado un archivo de GeoJSON con los datos geográficos de américa del norte; si gustas puedes descargarlo; sería un buen ejercicio que vieras la estructura que usualmente tiene un objeto GeoJSON en la vida real.

En este caso el código luciría de la siguiente manera:

<svg id="map-demo-1" height="250" width="400"></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    var projection = d3.geoMercator();
    var path = d3.geoPath(projection);
    d3.json("http://harkthedata.com/2018/11/03/north_america.geojson").then(geoData => {
        var pathString = path(geoData);
        d3.select("svg#map-demo-1").append("path").attr("d", pathString);
    });
</script>

Y el resultado sería el siguiente:

Como podrás notar, el código es muy semejante al que ya habíamos visto; sin embargo, en este caso estamos obteniendo nuestros datos de un archivo externo; hacer esto es mucho más adecuado; las estructuras de GeoJSON pueden ser sumamente grandes y no sería práctico declarar una variable en JavaScript con todos los datos geográficos.

Si aún no te has familiarizado mucho con D3, permíteme esquematizar lo que está sucediendo:

La función d3.json(file), busca un archivo con estructura de JSON en la ubicación indicada por el parámetro file (recuerda que GeoJSON no es sino una extensión de JSON); de encontrarlo, pasará la estructura leída a la función indicada en el método “then”; es precisamente en esta función en donde estamos indicando los pasos a seguir para dibujar al mapa; como notarás, son los mismos pasos que ya habíamos seguido en el ejemplo anterior.

6.3. Un mapa colorido

Para finalizar quiero mostrarte un poco más de la gran capacidad que tiene D3 para la creación de mapas. Hasta ahora, únicamente hemos utilizado un solo elemento <path> para dibujar la representación visual de todos los datos geográficos; sin embargo, de hecho, en el archivo GeoJSON que hemos estado utilizando, existe la información de 18 países; ¿no sería agradable poder visualizar cada uno de ellos? Pues bien, nos vamos a auxiliar del Update Pattern; si no te has familiarizado con él, basta con que sepas que es una metodología de D3 en la que es posible manipular varios elementos de HTML de manera simultánea, sin la necesidad de usar bloques de bucle en el código.

El código que hará la magia es el siguiente:

<svg id="map-demo-2" height="250" width="400"></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    var projection = d3.geoMercator();
    var path = d3.geoPath(projection);
    var colorScale = d3.scaleSequential(d3.interpolateRainbow);
    d3.json("http://harkthedata.com/2018/11/03/north_america.geojson").then(geoData => {
        d3.select("svg#map-demo-2").selectAll("path").data(geoData.features).enter()
            .append("path") /*Por cada país añadiremos un elemento <path>. Al final, tendremos 
            18 de estos elementos.*/
                .attr("d", path) /*El objeto path será invocado como función por cada objeto 
                GeoJSON, es decir, cada país. La cadena de texto resultante se asignará al 
                atributo “d” de cada elemento <path>*/
                .attr("fill", (d, i) => colorScale(i / 17)); /*Por cada elemento <path>, esto es, 
                cada país, se señala el atributo fill, lo cual permite establecer el color de cada 
                uno. El parámetro “i” es el índice de cada uno de los países dentro del arreglo.*/
    });
</script>

El resultado de correr este código sería algo como esto:

Seguramente en este punto, varias secciones del código ya te lucen familiares. Entre los cambios realizados podrás observar la definición de la variable colorScale; este objeto puede ser invocado como una función a la cual; al pasarle un valor entre 0 y 1, devolverá un color; o, mejor dicho, una cadena de texto con la representación hexadecimal de un color.

A diferencia de los dos ejemplos anteriores, lo que estamos haciendo en este caso es crear un elemento <path> por cada país. El objeto JSON en el archivo north_america.geojson, contiene un parámetro llamado features; este parámetro es un arreglo que a su vez contiene 18 objetos de GeoJSON; uno por cada país, y cada uno con la información necesaria para dibujar la forma de cada país.

En el bloque de código antes señalado, he puesto comentarios en varias líneas para sepas qué es lo que están haciendo; sólo recuerda, el objeto path y el elemento <path> son cosas diferentes.

7. Conclusiones

¿Qué te pareció? Como podrás notar, ¡D3 ofrece herramientas geniales!

Espero que estés de acuerdo conmigo en que el código necesario para crear un mapa utilizando D3, es bastante corto y directo; no obstante, para poder sacar el máximo potencial a lo que esta librería tiene para ofrecernos, vale la pena adentrarnos en algunos conceptos sobre la estructura de los datos geográficos y sobre la manera en la que D3 opera. Me parece que hay dos temas en los que vale la pena profundizar con el objetivo de crear geo-visualizaciones más efectivas:

  1. Detalle de la estructura de los objetos GeoJSON
  2. Mecánica del funcionamiento del Update Pattern de D3

Ya he creado enlaces para que puedas estudiar éstos temas. ¡Anímate, vísitalos!