Visualización Geoespacial
La importancia de los mapas en visualización de datos
Los mapas son herramientas fundamentales para representar datos que tienen una componente geográfica o espacial. Permiten revelar patrones, tendencias y relaciones que serían difíciles de detectar en tablas o gráficos tradicionales.
Observable Plot facilita la creación de mapas mediante el uso de coordenadas geográficas (latitud y longitud) que ya están presentes en nuestro dataset.
En esta lección aprenderemos a:
- Crear mapas de puntos usando coordenadas geográficas
- Codificar variables mediante tamaño y color
- Usar proyecciones geográficas apropiadas
- Cargar datos geográficos desde fuentes estándar
- Agregar contexto geográfico con líneas de costa
- Crear mapas coropléticos coloreando países completos
- Combinar múltiples capas de información espacial
Mapa base con puntos
Comenzaremos creando un mapa simple que muestra la ubicación de cada país usando sus coordenadas:
Ver código Plot.plot mapa base
Plot.plot({
width: 900,
height: 500,
projection: "equirectangular",
marks: [
Plot.dot(datos, {
x: "longitude",
y: "latitude",
fill: "steelblue",
r: 3,
}),
],
})
¿Qué hace cada parte?
- projection: "equirectangular": define el sistema de proyección del mapa (más sobre esto adelante)
- x: "longitude": coordenada horizontal (longitud geográfica, -180 a 180)
- y: "latitude": coordenada vertical (latitud geográfica, -90 a 90)
- r: 3: radio fijo de 3 píxeles para todos los puntos
Cada punto representa la ubicación aproximada de un país en el mapa mundial.
Entendiendo las proyecciones geográficas
La Tierra es esférica, pero los mapas son planos. Las proyecciones cartográficas son métodos matemáticos para representar la superficie curva de la Tierra en un plano bidimensional. Cada proyección tiene ventajas y desventajas (Snyder, 1987).
Proyección Equirectangular (Plate Carrée)
Es la proyección más simple: convierte directamente latitud y longitud en coordenadas X e Y.
Ventajas:
- Muy simple de entender
- Fácil de implementar
- Buena para mostrar ubicaciones generales
Desventajas:
- Distorsiona severamente las áreas cerca de los polos
- Groenlandia aparece del mismo tamaño que África (en realidad es 14 veces más pequeña)
Proyección de Mercator
Preserva los ángulos y formas locales, popular en navegación marítima:
Ver código Plot.plot proyección Mercator
Plot.plot({
width: 900,
height: 500,
projection: "mercator",
marks: [
Plot.dot(datos, {
x: "longitude",
y: "latitude",
fill: "steelblue",
r: 3,
}),
],
})
Ventajas:
- Preserva formas locales
- Útil para navegación
- Líneas rectas representan rumbos constantes
Desventajas:
- Distorsiona severamente las áreas
- Regiones polares aparecen enormes
- No se puede mostrar los polos
Proyección Equal Earth
Una proyección moderna diseñada específicamente para minimizar distorsiones visuales (Šavrič et al., 2019):
Ver código Plot.plot proyección Equal Earth
Plot.plot({
width: 900,
height: 500,
projection: "equal-earth",
marks: [
Plot.dot(datos, {
x: "longitude",
y: "latitude",
fill: "steelblue",
r: 3,
}),
],
})
Ventajas:
- Balance óptimo entre distorsión de área y forma
- Apariencia estéticamente agradable
- Apropiada para mapas temáticos generales
Recomendación: Para la mayoría de visualizaciones de datos globales, Equal Earth es la mejor opción.
Codificando población mediante tamaño
Podemos hacer que el tamaño de cada punto represente la población del país:
Ver código Plot.plot con tamaño por población
Plot.plot({
width: 900,
height: 500,
projection: "equal-earth",
r: {
range: [2, 20],
},
marks: [
Plot.dot(datos, {
x: "longitude",
y: "latitude",
r: "population",
fill: "steelblue",
fillOpacity: 0.6,
stroke: "white",
strokeWidth: 0.5,
}),
],
})
¿Qué cambió?
- r: "population": el radio ahora representa la población
- r: { range: [2, 20] }: controla el rango de tamaños (mínimo 2px, máximo 20px)
- fillOpacity: 0.6: semitransparencia para ver puntos superpuestos
- stroke: "white": borde blanco que mejora la visibilidad
Ahora podemos ver claramente dónde están los países más poblados: China, India, Estados Unidos, Indonesia, etc.
Codificando continentes mediante color
Usemos la columna continent para colorear los países por región geográfica:
Ver código Plot.plot con color por continente
Plot.plot({
width: 900,
height: 500,
projection: "equal-earth",
r: {
range: [2, 20],
},
color: {
legend: true,
scheme: "Observable10",
},
marks: [
Plot.dot(datos, {
x: "longitude",
y: "latitude",
r: "population",
fill: "continent",
fillOpacity: 0.7,
stroke: "white",
strokeWidth: 0.5,
}),
],
})
¿Qué cambió?
- fill: "continent": el color ahora varía según el continente
- color: { legend: true }: muestra una leyenda automática
- scheme: "Observable10": paleta de 10 colores distintivos
Ahora podemos identificar visualmente las regiones geográficas del mundo.
Cargando datos geográficos
Hasta ahora, nuestros mapas solo muestran puntos sobre un fondo vacío. Para agregar contexto geográfico (líneas de costa, fronteras, territorios), necesitamos cargar geometrías que describan las formas de los continentes y países.
Formatos geográficos: TopoJSON y GeoJSON
Los datos geográficos se almacenan en formatos especializados:
- GeoJSON: formato estándar que describe geometrías (polígonos, líneas, puntos) con coordenadas. Es el formato que
Plot.geo()entiende directamente. - TopoJSON: versión comprimida de GeoJSON que reduce significativamente el tamaño del archivo eliminando coordenadas redundantes. Es el formato en que se distribuyen la mayoría de los datasets geográficos.
En la práctica, descargamos TopoJSON (más ligero) y lo convertimos a GeoJSON (más compatible) antes de usarlo.
World Atlas: un dataset geográfico estándar
El paquete world-atlas proporciona geometrías del mundo en diferentes niveles de detalle:
- land-110m.json: siluetas de los continentes (sin fronteras entre países)
- countries-110m.json: polígonos individuales de cada país con su código ISO
El sufijo 110m indica la resolución: una simplificación a escala 1:110 millones, suficiente para mapas mundiales y ligera de descargar.
Cargando las geometrías
Para descargar y convertir los datos geográficos usamos fetch con await:
Ver código de carga de geometrías
const land = await fetch(
"https://cdn.jsdelivr.net/npm/world-atlas@2/land-110m.json"
)
.then((r) => r.json())
.then((world) => topojson.feature(world, world.objects.land))
¿Qué hace cada parte?
- await fetch(url): descarga el archivo desde el CDN de jsdelivr.
awaitindica que debemos esperar a que la descarga termine antes de continuar - .then(r => r.json()): convierte la respuesta HTTP a un objeto JavaScript (el TopoJSON crudo)
- .then(world => topojson.feature(world, world.objects.land)): extrae la geometría
landdel TopoJSON y la convierte a GeoJSON - const land: almacena el resultado como variable reutilizable en todos los gráficos siguientes
El resultado land es un objeto GeoJSON con los polígonos de todos los continentes, listo para usarse con Plot.geo().
La biblioteca topojson está disponible globalmente en Observable Framework, por lo que no es necesario importarla.
Agregando líneas de costa (contexto geográfico)
Ahora que tenemos las geometrías cargadas en la variable land, podemos agregarlas como fondo de nuestros mapas:
Ver código Plot.plot con líneas de costa
Plot.plot({
width: 900,
height: 500,
projection: "equal-earth",
r: {
range: [2, 20],
},
color: {
legend: true,
scheme: "Observable10",
},
marks: [
Plot.geo(land, {
fill: "#f0f0f0",
stroke: "#ccc",
strokeWidth: 0.5,
}),
Plot.dot(datos, {
x: "longitude",
y: "latitude",
r: "population",
fill: "continent",
fillOpacity: 0.8,
stroke: "white",
strokeWidth: 0.5,
}),
],
})
¿Qué hace cada parte?
- Plot.geo(land, ...): dibuja los polígonos geográficos almacenados en
land - fill: "#f0f0f0": color gris claro para los continentes
- stroke: "#ccc": líneas grises para las costas
Las dos marcas se combinan en orden: primero se dibuja el fondo geográfico, luego los puntos encima. El orden dentro del array marks determina qué capa queda arriba de cuál.
Mapa temático: Expectativa de vida
Creemos un mapa que muestre la expectativa de vida usando color continuo:
Ver código Plot.plot expectativa de vida
Plot.plot({
width: 900,
height: 500,
projection: "equal-earth",
color: {
type: "linear",
scheme: "RdYlGn",
legend: true,
label: "Expectativa de vida (años)",
},
marks: [
Plot.geo(land, {
fill: "#f8f8f8",
stroke: "#ddd",
strokeWidth: 0.5,
}),
Plot.dot(datos, {
x: "longitude",
y: "latitude",
r: 5,
fill: "life_expectancy",
stroke: "white",
strokeWidth: 0.5,
}),
],
})
¿Qué cambió?
- fill: "life_expectancy": color según expectativa de vida
- scheme: "RdYlGn": escala divergente rojo-amarillo-verde
- r: 5: tamaño fijo (ya no representa población)
- color: { type: "linear" }: escala continua de colores
Los países con mayor expectativa de vida aparecen en verde, los de menor en rojo.
Mapa con título y contexto completo
Agreguemos todos los elementos de contexto profesional:
Ver código Plot.plot mapa completo con contexto
Plot.plot({
width: 900,
height: 550,
marginTop: 50,
projection: "equal-earth",
title: "Expectativa de vida global por país",
subtitle:
"Datos de 195 países (2023) • Color representa años de vida esperados",
caption: "Fuente: World Data 2023 (Kaggle)",
color: {
type: "linear",
scheme: "RdYlGn",
legend: true,
label: "Expectativa de vida (años)",
domain: [50, 85],
},
marks: [
Plot.geo(land, {
fill: "#f8f8f8",
stroke: "#ddd",
strokeWidth: 0.5,
}),
Plot.dot(datos, {
x: "longitude",
y: "latitude",
r: 5,
fill: "life_expectancy",
stroke: "white",
strokeWidth: 1,
}),
],
})
Este mapa tiene todos los elementos profesionales: título informativo, subtítulo contextual, fuente citada, leyenda clara y fondo geográfico.
Mapa multivariable: PIB y Población
Combinemos múltiples variables: color para PIB, tamaño para población:
Ver código Plot.plot mapa multivariable
Plot.plot({
width: 900,
height: 550,
marginTop: 50,
projection: "equal-earth",
title: "Desarrollo económico y demográfico mundial",
subtitle: "Tamaño = población, Color = PIB",
caption: "Fuente: World Data 2023 (Kaggle)",
r: {
range: [2, 25],
},
color: {
type: "log",
scheme: "YlGnBu",
legend: true,
label: "PIB (escala log)",
},
marks: [
Plot.geo(land, {
fill: "#fafafa",
stroke: "#ccc",
strokeWidth: 0.5,
}),
Plot.dot(datos, {
x: "longitude",
y: "latitude",
r: "population",
fill: "gdp_usd",
fillOpacity: 0.7,
stroke: "white",
strokeWidth: 0.5,
}),
],
})
Este mapa visualiza tres dimensiones simultáneamente:
- Ubicación geográfica (X, Y)
- Población (tamaño del punto)
- PIB (color del punto)
Guía de proyecciones
| Proyección | Mejor para | Distorsión |
|---|---|---|
| equirectangular | Ubicaciones generales, datos simples | Alta en áreas |
| mercator | Navegación, zonas ecuatoriales | Muy alta en polos |
| equal-earth | Mapas temáticos generales | Balanceada |
| albers-usa | Datos de Estados Unidos | Ninguna (solo USA) |
| orthographic | Vista de globo terráqueo | Mitad del mundo invisible |
| natural-earth | Alternativa a Equal Earth | Similar a Equal Earth |
Recomendación general: Usa "equal-earth" para mapas mundiales temáticos.
Mapas coropléticos: coloreando países completos
Hasta ahora hemos usado puntos para representar países. Los mapas coropléticos colorean todo el territorio del país según una variable, lo que facilita ver patrones regionales de forma más clara (Slocum et al., 2022).
Para construir un mapa coroplético necesitamos:
- Geometrías por país: polígonos individuales (no solo la silueta de los continentes)
- Datos: las variables que queremos visualizar (expectativa de vida, PIB, etc.)
- Un identificador común: una clave que permita unir cada polígono con sus datos
Cargando geometrías de países
A diferencia de land-110m.json (que tiene los continentes como una sola pieza), el archivo countries-110m.json contiene un polígono separado por cada país, cada uno con su código ISO como identificador:
Ver preparación de geometrías de países
const world = await fetch(
"https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"
).then((r) => r.json())
const countries = topojson.feature(world, world.objects.countries)
¿Qué cambió respecto a la carga anterior?
- countries-110m.json en vez de land-110m.json: contiene polígonos individuales por país
- world.objects.countries en vez de world.objects.land: extrae los países, no los continentes
- Se guarda el paso intermedio
worldporque lo usaremos para acceder a los metadatos
El resultado countries es un GeoJSON con un feature por cada país. Cada feature tiene un id con el código ISO numérico del país (ej: "484" para México, "392" para Japón).
El identificador común: códigos ISO numéricos
Para colorear cada país, necesitamos combinar las geometrías con nuestros datos. El archivo World Atlas identifica a los países con códigos ISO numéricos (por ejemplo, México es "484", Japón es "392"). Nuestro CSV incluye esta columna como iso_numeric, que fue generada en el data loader a partir de los códigos alpha-2 de cada país.
Si no tuviéramos este identificador compartido, el join entre geometrías y datos fallaría silenciosamente: todos los países quedarían grises porque ningún código coincidiría. Es un problema muy común al trabajar con datos geoespaciales, donde las diferentes fuentes no siempre comparten el mismo sistema de identificadores.
Haciendo el join entre geometrías y datos
Ahora podemos combinar las geometrías con nuestros datos usando el código ISO numérico:
Ver código del join
const countriesWithData = countries.features.map((feature) => ({
...feature,
properties: {
...feature.properties,
data: datos.find((d) => String(d.iso_numeric) === feature.id),
},
}))
¿Qué hace cada parte?
- countries.features: array con todos los polígonos de países
- feature.id: código ISO numérico del país en el GeoJSON (ej:
"484"para México) - d.iso_numeric: código ISO numérico en nuestro CSV (ej:
484para México) - String(d.iso_numeric): convierte el número a texto para comparar con
feature.id - datos.find(): busca el país correspondiente en nuestros datos
- El resultado es cada país con su geometría + sus datos estadísticos
Los países que no tienen correspondencia quedarán con data: undefined, lo que los mostrará en gris en el mapa.
Mapa coroplético: Expectativa de vida
Ahora podemos colorear cada país según su expectativa de vida:
Ver código Plot.plot mapa coroplético básico
Plot.plot({
width: 900,
height: 500,
projection: "equal-earth",
color: {
type: "linear",
scheme: "RdYlGn",
legend: true,
label: "Expectativa de vida (años)",
tickFormat: ".0f",
domain: [50, 80],
},
marks: [
Plot.geo(countriesWithData, {
fill: (d) => d.properties.data?.life_expectancy,
stroke: "black",
strokeWidth: 0.5,
title: (d) =>
d.properties.data
? `${d.properties.data.country}: ${d.properties.data.life_expectancy} años`
: "Sin datos",
}),
],
})
¿Qué hace cada parte?
- Plot.geo(countriesWithData, ...): dibuja polígonos de países
- fill: d => d.properties.data?.life_expectancy: color según expectativa de vida
- d?.: operador opcional que maneja países sin datos (los deja en gris)
- title: d => ...: muestra tooltip con nombre del país y valor al pasar el mouse
- color: { type: "quantile" }: divide los datos en grupos (cuantiles) para mejor distribución visual
- tickFormat: ".0f": muestra los valores de la leyenda como números enteros, evitando que los decimales se encimen
Ahora cada país está completamente coloreado según su expectativa de vida, no solo un punto.
Mapa coroplético: PIB
Visualicemos el PIB usando una escala logarítmica apropiada:
Ver código Plot.plot mapa coroplético PIB
Plot.plot({
width: 900,
height: 500,
projection: "equal-earth",
color: {
type: "log",
scheme: "YlGnBu",
legend: true,
label: "PIB (USD, escala log)",
domain: [1000000000, 100000000000000],
},
marks: [
Plot.geo(countriesWithData, {
fill: (d) => d.properties.data?.gdp_usd,
stroke: "black",
strokeWidth: 0.5,
title: (d) =>
d.properties.data
? `${d.properties.data.country}: $${(
d.properties.data.gdp_usd / 1e9
).toFixed(1)}B`
: "Sin datos",
}),
],
})
type: "log" es esencial para PIB porque los valores varían enormemente (desde millones hasta trillones). La escala logarítmica permite ver diferencias tanto entre países pequeños como entre grandes potencias.
Mapa coroplético: Población
Para población también usamos escala logarítmica:
Ver código Plot.plot mapa coroplético población
Plot.plot({
width: 900,
height: 500,
projection: "equal-earth",
color: {
type: "log",
scheme: "Reds",
legend: true,
label: "Población (escala log)",
domain: [1000000, 1000000000],
},
marks: [
Plot.geo(countriesWithData, {
fill: (d) => d.properties.data?.population,
stroke: "black",
strokeWidth: 0.5,
title: (d) =>
d.properties.data
? `${d.properties.data.country}: ${(
d.properties.data.population / 1e6
).toFixed(1)}M`
: "Sin datos",
}),
],
})
Mapa coroplético con contexto completo
Agreguemos título, subtítulo y caption para un mapa profesional:
Ver código Plot.plot mapa coroplético completo
Plot.plot({
width: 900,
height: 550,
marginTop: 50,
projection: "equal-earth",
title: "Expectativa de vida global",
subtitle: "Años de vida esperados al nacer por país (2023)",
caption: "Fuente: World Data 2023 (Kaggle) • Países en gris: sin datos",
color: {
type: "linear",
scheme: "RdYlGn",
legend: true,
label: "Expectativa de vida (años)",
tickFormat: ".0f",
domain: [50, 80],
},
marks: [
Plot.geo(countriesWithData, {
fill: (d) => d.properties.data?.life_expectancy,
stroke: "black",
strokeWidth: 0.5,
title: (d) =>
d.properties.data
? `${d.properties.data.country}: ${d.properties.data.life_expectancy} años`
: "Sin datos",
}),
],
})
Combinando coroplético + puntos
Podemos superponer puntos sobre el mapa coroplético para mostrar dos variables simultáneamente:
Ver código Plot.plot mapa combinado
Plot.plot({
width: 900,
height: 550,
marginTop: 50,
projection: "equal-earth",
title: "Expectativa de vida y densidad poblacional",
subtitle:
"Color del territorio = esperanza de vida, Tamaño del punto = población",
caption: "Fuente: World Data 2023 (Kaggle)",
color: {
type: "linear",
scheme: "RdYlGn",
legend: true,
label: "Expectativa de vida",
tickFormat: ".0f",
domain: [50, 80],
},
r: {
range: [2, 20],
},
marks: [
Plot.geo(countriesWithData, {
fill: (d) => d.properties.data?.life_expectancy,
stroke: "black",
strokeWidth: 0.5,
}),
Plot.dot(datos, {
x: "longitude",
y: "latitude",
r: "population",
fill: "black",
fillOpacity: 0.3,
stroke: "white",
strokeWidth: 0.5,
}),
],
})
Este mapa muestra tres variables simultáneamente:
- Color del país: expectativa de vida
- Tamaño del punto: población
- Ubicación: geografía
Mapas de puntos vs Mapas coropléticos
| Aspecto | Mapas de puntos | Mapas coropléticos |
|---|---|---|
| Mejor para | Ubicaciones precisas, ciudades | Países completos, regiones |
| Ventaja | Muestra ubicación exacta | Muestra cobertura territorial |
| Desventaja | Difícil ver países pequeños | Sesgo visual por tamaño de país |
| Datos necesarios | Latitud y longitud | Polígonos geográficos + join por código |
| Cuándo usar | Datos de ciudades o sitios | Datos agregados por país/región |
Recomendación:
- Usa puntos cuando la ubicación precisa importa o cuando no tienes polígonos disponibles
- Usa coroplético cuando trabajas con datos agregados por país o región
- Combínalos cuando quieras mostrar múltiples dimensiones
Cuándo usar mapas
Usar mapas cuando:
- Los datos tienen componente espacial o geográfica
- Las ubicaciones son importantes para la interpretación
- Se busca revelar patrones espaciales o regionales
- La audiencia está familiarizada con geografía
Evitar mapas cuando:
- La ubicación no es relevante para el análisis
- Los datos no tienen coordenadas geográficas
- Un gráfico de barras o scatter plot sería más claro
- La precisión numérica es más importante que la distribución espacial
Mejores prácticas para mapas
Proyección apropiada (Bostock et al., 2011)
- Elegir según el área geográfica y el propósito
- Equal Earth para mapas temáticos mundiales
- Proyecciones cónicas para países individuales
Escalas de color
- Divergentes (RdYlGn) para datos con punto medio significativo
- Secuenciales (YlGnBu) para magnitudes crecientes
- Categóricas (Observable10) para regiones o grupos
Contexto geográfico
- Agregar líneas de costa cuando sea posible
- Incluir fronteras si son relevantes
- No saturar con demasiados detalles
Legibilidad
- Controlar tamaños para evitar superposición excesiva
- Usar bordes blancos para separar puntos
- Considerar opacidad cuando hay muchos datos
Resumen de propiedades para mapas
| Propiedad | Función |
|---|---|
| projection | Sistema de proyección cartográfica |
| x: "longitude" | Coordenada horizontal geográfica |
| y: "latitude" | Coordenada vertical geográfica |
| Plot.geo() | Marca para geometrías geográficas (polígonos, líneas) |
| Plot.dot() | Marca para puntos geográficos con coordenadas |
| fetch() + await | Descarga archivos geográficos desde un CDN |
| topojson.feature() | Convierte TopoJSON a GeoJSON |
| iso_numeric | Código ISO numérico para unir datos con geometrías |
Conclusiones de la lección
En esta lección se ha explorado la creación de mapas temáticos usando Observable Plot y datos geográficos.
Conceptos clave
Proyecciones cartográficas
- Transforman coordenadas esféricas en planas
- Cada proyección tiene ventajas y limitaciones
- Equal Earth es ideal para mapas temáticos mundiales
Datos geográficos
- TopoJSON es el formato comprimido de distribución; GeoJSON es el formato que Plot entiende
fetch+awaitdescarga los archivos;topojson.feature()los convierte- World Atlas ofrece dos niveles:
land(continentes) ycountries(países individuales)
Mapas de puntos
- Usan directamente latitud y longitud
- Tamaño y color agregan dimensiones adicionales
- Ideales cuando se tienen coordenadas y no se requieren polígonos
Mapas coropléticos
- Colorean el territorio completo de cada país
- Requieren polígonos geográficos (GeoJSON/TopoJSON)
- Necesitan un join entre geometrías y datos mediante un identificador común (
iso_numeric) - Los identificadores deben coincidir entre fuentes; el data loader se encarga de esta preparación
Elementos de contexto
- Plot.geo() para líneas de costa y fronteras
- World Atlas proporciona geometrías estándar
- Capas múltiples permiten combinaciones complejas
Reflexión final
Los mapas son herramientas poderosas cuando los datos tienen componente espacial, pero no son apropiados para todos los casos. La clave está en:
- Elegir la proyección correcta para minimizar distorsiones
- Usar color y tamaño apropiadamente para codificar variables adicionales
- Agregar contexto geográfico suficiente sin saturar
- Mantener legibilidad controlando densidad y superposición
- Preparar los datos correctamente para que los identificadores coincidan entre fuentes
Un buen mapa temático revela patrones geográficos que serían invisibles en otros tipos de gráficos, pero un mal mapa puede confundir más que aclarar. Al trabajar con datos geoespaciales, la preparación de los datos (incluyendo la correspondencia de identificadores) es un paso esencial que conviene resolver en el data loader, antes de llegar a la visualización.
Próximo paso: En las siguientes lecciones exploraremos como implementar D3 para aumentar la personalización de visualizaciones, revisa SVG: el lenguaje visual de D3