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:


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?

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:

Desventajas:


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:

Desventajas:


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:

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ó?

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ó?

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:

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:

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?

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?

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ó?

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:

  1. Ubicación geográfica (X, Y)
  2. Población (tamaño del punto)
  3. 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:

  1. Geometrías por país: polígonos individuales (no solo la silueta de los continentes)
  2. Datos: las variables que queremos visualizar (expectativa de vida, PIB, etc.)
  3. 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?

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?

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?

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:

  1. Color del país: expectativa de vida
  2. Tamaño del punto: población
  3. 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:


Cuándo usar mapas

Usar mapas cuando:

Evitar mapas cuando:


Mejores prácticas para mapas

Proyección apropiada (Bostock et al., 2011)

Escalas de color

Contexto geográfico

Legibilidad


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

Datos geográficos

Mapas de puntos

Mapas coropléticos

Elementos de contexto


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:

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


Bibliografía

Bostock, M., Ogievetsky, V., & Heer, J. (2011). D3: Data-Driven Documents. IEEE Transactions on Visualization and Computer Graphics, 17(12), 2301–2309. https://doi.org/10.1109/TVCG.2011.185
Šavrič, B., Patterson, T., & Jenny, B. (2019). The Equal Earth map projection. International Journal of Geographical Information Science, 33(3), 454–465. https://doi.org/10.1080/13658816.2018.1504949
Slocum, T. A., McMaster, R. B., Kessler, F. C., & Howard, H. H. (2022). Thematic Cartography and Geovisualization (4a ed.). CRC Press.
Snyder, J. P. (1987). Map Projections: A Working Manual. U.S. Government Printing Office. https://doi.org/10.3133/pp1395