Visualización Multiple


Visualizando múltiples dimensiones simultáneamente

Hasta ahora hemos trabajado principalmente con dos dimensiones: una variable en el eje X y otra en el eje Y. Sin embargo, Observable Plot permite codificar información adicional usando otros canales visuales (Mackinlay, 1986):

En esta lección aprenderemos a:


Gráfico base

Comenzaremos con un scatter plot que relaciona PIB y expectativa de vida:

Ver código Plot.plot gráfico base
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "Relación entre PIB y expectativa de vida",
  subtitle: "Datos de 195 países (2023)",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (billones de USD)",
    grid: true,
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
    type: "log",
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  marks: [
    Plot.dot(datos, {
      x: "gdp_usd",
      y: "life_expectancy",
      fill: "steelblue",
      fillOpacity: 0.6,
      stroke: "darkblue",
      strokeWidth: 1.5,
    }),
  ],
})

Este gráfico muestra dos variables, pero cada punto es idéntico en tamaño. No podemos saber, por ejemplo, si un país tiene mucha o poca población.


Agregando una tercera variable con tamaño: r

La propiedad r (radio) permite variar el tamaño de los puntos según una tercera variable.

Ver código Plot.plot con r variable
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB, expectativa de vida y población mundial",
  subtitle: "Datos de 195 países (2023) • El tamaño representa la población",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (billones de USD)",
    grid: true,
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
    type: "log",
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  marks: [
    Plot.dot(datos, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "steelblue",
      fillOpacity: 0.6,
      stroke: "darkblue",
      strokeWidth: 1.5,
    }),
  ],
})

¿Qué cambió?

Se agregó:

Los puntos más grandes representan países con mayor población (como China e India), mientras que los pequeños representan países con poblaciones menores.


Controlando el rango de tamaños

Por defecto, Plot escala automáticamente los tamaños, pero podemos controlarlo con la configuración del canal r:

Ver código Plot.plot con rango de tamaños
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB, expectativa de vida y población mundial",
  subtitle:
    "Datos de 195 países (2023) • Tamaños ajustados para mejor visualización",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (billones en USD)",
    grid: true,
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
    type: "log",
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: {
    range: [2, 20],
  },
  marks: [
    Plot.dot(datos, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "steelblue",
      fillOpacity: 0.6,
      stroke: "darkblue",
      strokeWidth: 1.5,
    }),
  ],
})

¿Qué cambió?

Se agregó:

Esto evita que los puntos sean demasiado pequeños (invisibles) o demasiado grandes (superpuestos).


Guía para rangos de tamaño

Puntos pequeños ([1, 10]):

Puntos medianos ([2, 20]):

Puntos grandes ([5, 40]):


Agregando color por categoría

Hasta ahora todos los puntos tienen el mismo color. Podemos usar fill como variable para colorear según categorías.

Primero, necesitamos crear una categorización. Agruparemos países por nivel de desarrollo basado en PIB:

Ver preparación de datos
const datosConCategoria = datos.map((d) => ({
  ...d,
  development_level:
    d.gdp_usd < 100e9
      ? "Bajo PIB"
      : d.gdp_usd < 1000e9
      ? "PIB Medio"
      : "PIB Alto",
}))

¿Qué hace cada parte?

datos.map(d => ...)

{ ...d, ... }

"development_level": ...

d.gdp_usd < 100e9 ? "Bajo PIB" : ...

d.gdp_usd < 1000e9 ? "PIB Medio" : ...


Ver código Plot.plot con color categórico
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB, expectativa de vida y población por nivel de desarrollo",
  subtitle: "Datos de 195 países (2023) • Coloreado por nivel de PIB",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (billones en USD)",
    grid: true,
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
    type: "log",
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: {
    range: [2, 20],
  },
  color: {
    legend: true,
  },
  marks: [
    Plot.dot(datosConCategoria, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "development_level",
      fillOpacity: 0.7,
      stroke: "white",
      strokeWidth: 1,
    }),
  ],
})

¿Qué cambió?

Se agregó:

Ahora podemos ver patrones: los países de PIB Alto (azul) tienden a tener mayor expectativa de vida.


Paletas de colores para categorías

Observable Plot usa paletas predefinidas, pero podemos personalizarlas:

Ver código Plot.plot con paleta personalizada
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB, expectativa de vida y población por nivel de desarrollo",
  subtitle: "Datos de 195 países (2023) • Paleta personalizada",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (billones de USD)",
    grid: true,
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
    type: "log",
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: {
    range: [2, 20],
  },
  color: {
    domain: ["Bajo PIB", "PIB Medio", "PIB Alto"],
    range: ["#ef4444", "#f59e0b", "#10b981"],
    legend: true,
  },
  marks: [
    Plot.dot(datosConCategoria, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "development_level",
      fillOpacity: 0.7,
      stroke: "white",
      strokeWidth: 1,
    }),
  ],
})

¿Qué cambió?

Se agregó en la configuración de color:

Colores usados:


Mejores prácticas para color categórico

Número de categorías: (Brewer, 2023), (Ware, 2021)

Accesibilidad: (Okabe, 2008), (Wong, 2011)

Paletas recomendadas:

Ver código paletas recomendadas
// Paleta amigable con daltonismo
range: ["#0173B2", "#DE8F05", "#029E73", "#CC78BC"]

// Paleta neutral
range: ["#4B5563", "#9CA3AF", "#D1D5DB", "#F3F4F6"]

// Paleta cálida a fría
range: ["#DC2626", "#F59E0B", "#10B981", "#3B82F6"]

Color con variable continua

Además de categorías, podemos usar color para representar una variable numérica continua:

Ver código Plot.plot con color continuo
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB, expectativa de vida, población y emisiones de CO₂",
  subtitle: "Datos de 195 países (2023) • Color representa emisiones de CO₂",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (billones de USD)",
    grid: true,
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
    type: "log",
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: {
    range: [2, 20],
  },
  color: {
    type: "linear",
    scheme: "YlOrRd",
    legend: true,
    label: "Emisiones CO₂ en millones",
    domain: [0, 10],
  },
  marks: [
    Plot.dot(datos, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "co2_emissions_kt",
      fillOpacity: 0.8,
      stroke: "white",
      strokeWidth: 1,
    }),
  ],
})

¿Qué cambió?

Se agregó:

Ahora estamos visualizando CINCO variables simultáneamente:

  1. Eje X: PIB
  2. Eje Y: Expectativa de vida
  3. Tamaño: Población
  4. Color: Emisiones de CO₂
  5. Posición: Cada punto es un país

Esquemas de color para variables continuas

Secuenciales (un color):

Divergentes (dos colores):

Multi-hue (varios colores):

Ver código esquemas de color
// Ejemplos de uso:
color: {
  scheme: "Blues"
} // Azules secuenciales
color: {
  scheme: "RdBu"
} // Rojo-azul divergente
color: {
  scheme: "Viridis"
} // Viridis (científico)
color: {
  scheme: "YlOrRd"
} // Amarillo-rojo (calor)

Agregando etiquetas de texto

Para identificar países específicos, podemos agregar etiquetas usando Plot.text():

Ver preparación de datos
const paisesDestacados = datos.filter((d) => d.population > 100e6)
Ver código Plot.plot con etiquetas de países
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB y expectativa de vida con países más poblados etiquetados",
  subtitle:
    "Datos de 195 países (2023) • Etiquetados países con más de 100 millones de habitantes",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (billones de USD)",
    grid: true,
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
    type: "log",
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: {
    range: [2, 20],
  },
  marks: [
    // Puntos de todos los países
    Plot.dot(datos, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "steelblue",
      fillOpacity: 0.5,
      stroke: "darkblue",
      strokeWidth: 1,
    }),
    // Etiquetas solo de países destacados
    Plot.text(paisesDestacados, {
      x: "gdp_usd",
      y: "life_expectancy",
      text: "country",
      fontSize: 12,
      fontWeight: "bold",
      dy: -8,
    }),
  ],
})

¿Qué cambió?

Se agregaron dos marcas diferentes:

  1. Plot.dot(): todos los puntos
  2. Plot.text(): etiquetas solo para países con más de 100M habitantes

Propiedades de Plot.text():


Propiedades útiles para texto

Posicionamiento:

Ver código Plot.text posicionamiento
Plot.text(datos, {
  x: "variable_x",
  y: "variable_y",
  text: "nombre",
  dx: 10, // Desplazamiento horizontal
  dy: -10, // Desplazamiento vertical
  textAnchor: "start", // "start", "middle", "end"
})

Estilo:

Ver código Plot.text estilo
Plot.text(datos, {
  text: "nombre",
  fontSize: 12,
  fontWeight: "bold",
  fill: "black",
  stroke: "white", // Contorno blanco
  strokeWidth: 3,
})

Rotación:

Ver código Plot.text rotación
Plot.text(datos, {
  text: "nombre",
  rotate: 45, // Grados de rotación
})

Agregando etiquetas en barras

Las etiquetas son muy útiles en gráficos de barras para mostrar los valores exactos:

Ver preparación de datos
const top10 = datos
  .filter((d) => d["life_expectancy"] != null)
  .sort((a, b) => b["life_expectancy"] - a["life_expectancy"])
  .slice(0, 10)
Ver código Plot.plot barras con etiquetas
Plot.plot({
  width: 900,
  height: 400,
  marginLeft: 150,
  title: "Top 10 países con mayor expectativa de vida",
  subtitle: "Año 2023 • Con valores exactos",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "Expectativa de vida (años)",
    grid: true,
    domain: [75, 90],
    clamp: true,
  },
  y: {
    label: null,
  },
  marks: [
    // Barras
    Plot.barX(top10, {
      y: "country",
      x: "life_expectancy",
      fill: "forestgreen",
      fillOpacity: 0.8,
      stroke: "darkgreen",
      strokeWidth: 1,
    }),
    // Etiquetas con valores
    Plot.text(top10, {
      y: "country",
      x: "life_expectancy",
      text: (d) => d["life_expectancy"].toFixed(1),
      textAnchor: "start",
      dx: 5,
      fill: "black",
      fontSize: 11,
      fontWeight: "bold",
    }),
  ],
})

¿Qué cambió?

Se combinaron dos marcas:

  1. Plot.barX(): las barras
  2. Plot.text(): etiquetas con los valores

Nota importante: textAnchor: utiliza también "end", "center", "middle", ...


Combinando múltiples marcas: líneas de referencia

Podemos agregar líneas de referencia para mostrar promedios o umbrales:

Ver preparación de datos
const promedioVida = d3.mean(datos, (d) => d["life_expectancy"])

Obtenemos el promedio de la expectativa de vida.

Ver código Plot.plot con línea de referencia
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB vs expectativa de vida con promedio mundial",
  subtitle: `Datos de 195 países (2023) • Promedio mundial: ${promedioVida.toFixed(
    1
  )} años`,
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (billones de USD)",
    grid: true,
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
    type: "log",
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: {
    range: [2, 20],
  },
  marks: [
    // Puntos
    Plot.dot(datos, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "steelblue",
      fillOpacity: 0.6,
      stroke: "darkblue",
      strokeWidth: 1,
    }),
    // Línea horizontal del promedio
    Plot.ruleY([promedioVida], {
      stroke: "red",
      strokeWidth: 2,
      strokeDasharray: "5,5",
    }),
    // Etiqueta del promedio
    Plot.text(
      [{ y: promedioVida, label: `Promedio: ${promedioVida.toFixed(1)} años` }],
      {
        x: 2e12,
        y: "y",
        text: "label",
        textAnchor: "end",
        fill: "red",
        fontWeight: "bold",
        dy: -5,
      }
    ),
  ],
})

¿Qué cambió?

Se agregaron tres marcas:

  1. Plot.dot(): los puntos de datos
  2. Plot.ruleY(): línea horizontal en el promedio
  3. Plot.text(): etiqueta explicativa

Propiedades de Plot.ruleY():


Gráfico completo: 5 dimensiones

Veamos un gráfico que combina todo lo aprendido:

Ver preparación de datos
const datosConRegion = datos.map((d) => ({
  ...d,
  Región: d.continent || "Others",
}))

const paisesGrandes = datosConRegion.filter((d) => d.population > 200e6)
const promedioGlobal = d3.mean(datos, (d) => d["life_expectancy"])

Este código toma directamente el valor de la columna continent y crea la nueva propiedad Región. El operador || "Others" maneja el caso de que algún país no tenga definido su continente.

Ver código Plot.plot gráfico completo 5 dimensiones
Plot.plot({
  width: 1000,
  height: 600,
  marginLeft: 80,
  marginRight: 150,
  title: "Panorama global: PIB, expectativa de vida, población y región",
  subtitle:
    "Datos de 195 países (2023) • Tamaño = población, Color = región geográfica",
  caption:
    "Fuente: World Data 2023 (Kaggle) | Etiquetados países con más de 200M de habitantes",
  style: {
    fontSize: "13px",
  },
  x: {
    label: "PIB (billones de USD)",
    grid: true,
    type: "log",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
    domain: [1e8, 100e12],
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
    domain: [50, 90],
  },
  r: {
    range: [2, 30],
  },
  color: {
    domain: [
      "North America",
      "Central America",
      "South America",
      "Europe",
      "Africa",
      "Asia",
      "Oceania",
      "Others",
    ],
    range: [
      "#2563EB", // América del Norte (azul fuerte)
      "#06B6D4", // América Central (cian)
      "#10B981", // América del Sur (verde)
      "#8B5CF6", // Europa (violeta)
      "#F43F5E", // África (rojo)
      "#F59E0B", // Asia (ámbar)
      "#22C55E", // Oceanía (verde claro)
      "#9CA3AF", // Otros (gris neutro)
    ],
    legend: true,
  },
  marks: [
    // Línea de promedio
    Plot.ruleY([promedioGlobal], {
      stroke: "#EF4444",
      strokeWidth: 2,
      strokeDasharray: "4,4",
    }),
    // Puntos principales
    Plot.dot(datosConRegion, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "Región",
      fillOpacity: 0.7,
      stroke: "gray",
      strokeWidth: 1.5,
    }),
    // Etiquetas de países grandes
    Plot.text(paisesGrandes, {
      x: "gdp_usd",
      y: "life_expectancy",
      text: "country",
      fontSize: 10,
      fontWeight: "bold",
      fill: "blue",
      dy: -8,
      stroke: "white",
      strokeWidth: 2,
    }),
    // Etiqueta del promedio
    Plot.text(
      [
        {
          x: 1e11,
          y: promedioGlobal,
          label: `Promedio mundial: ${promedioGlobal.toFixed(1)} años`,
        },
      ],
      {
        x: "x",
        y: "y",
        text: "label",
        textAnchor: "start",
        fill: "#EF4444",
        fontWeight: "bold",
        fontSize: 11,
        dy: -8,
      }
    ),
  ],
})

Este gráfico muestra:


Leyendas personalizadas

Las leyendas se generan automáticamente cuando usamos fill o r con variables, pero podemos personalizarlas:

Ver código Plot.plot con leyenda personalizada
Plot.plot({
  width: 900,
  height: 550,
  title: "PIB vs expectativa de vida por nivel de desarrollo",
  x: {
    label: "PIB (billones de USD)",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
    type: "log",
  },
  y: {
    label: "Expectativa de vida (años)",
  },
  color: {
    domain: ["Bajo PIB", "PIB Medio", "PIB Alto"],
    range: ["#EF4444", "#F59E0B", "#10B981"],
    legend: true,
    label: "Clasificación económica",
  },
  marks: [
    Plot.dot(datosConCategoria, {
      x: "gdp_usd",
      y: "life_expectancy",
      fill: "development_level",
      fillOpacity: 0.7,
    }),
  ],
})

Propiedades de leyenda:


Aplicando a histogramas

Las técnicas de codificación múltiple también funcionan en otros tipos de gráficos:

Ver código Plot.plot histograma con color por categoría
Plot.plot({
  width: 900,
  height: 450,
  marginTop: 50,
  title: "Distribución de expectativa de vida por nivel de desarrollo",
  subtitle: "Datos de 195 países (2023)",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  y: {
    label: "Número de países",
    grid: true,
  },
  color: {
    domain: ["Bajo PIB", "PIB Medio", "PIB Alto"],
    range: ["#EF4444", "#F59E0B", "#10B981"],
    legend: true,
  },
  marks: [
    Plot.rectY(
      datosConCategoria,
      Plot.binX(
        { y: "count" },
        {
          x: "life_expectancy",
          fill: "development_level",
          fillOpacity: 0.7,
          stroke: "white",
          strokeWidth: 2,
        }
      )
    ),
  ],
})

Ahora el histograma está segmentado por color según el nivel de desarrollo económico.


Resumen de propiedades

Propiedad Canal visual Tipo de variable Uso principal
r Tamaño Numérica continua Magnitud, población, importancia
fill (variable categórica) Color Categórica Grupos, regiones, tipos
fill (variable continua) Color Numérica continua Intensidad, temperatura, densidad
fillOpacity Opacidad Numérica continua Densidad, confianza, énfasis
text Texto Cualquiera Identificación, valores exactos

Conclusiones de la lección

En esta lección se ha explorado cómo usar múltiples canales visuales simultáneamente para representar más dimensiones de información en un solo gráfico.


Conceptos clave

Canales visuales múltiples

Leyendas automáticas

Buenas prácticas


Reflexión final

La codificación visual múltiple es una herramienta poderosa que permite contar historias complejas en un solo gráfico (Munzner, 2015). Sin embargo, con gran poder viene gran responsabilidad:

El objetivo no es mostrar todas las variables posibles, sino comunicar insights de manera efectiva. A veces, dos gráficos simples son mejores que uno complejo.


Próximo paso: En Interactividad se explorará la interactividad básica, aprendiendo a agregar tooltips informativos y crear filtros dinámicos con inputs de Observable para que los usuarios puedan explorar los datos de manera interactiva.


Bibliografía

Brewer, C. (2023). Color Advice for Cartography [Https://colorbrewer2.org/].
Few, S. (2012). Show Me the Numbers: Designing Tables and Graphs to Enlighten (2a ed.). Analytics Press.
Mackinlay, J. (1986). Automating the design of graphical presentations of relational information. ACM Transactions on Graphics, 5(2), 110–141. https://doi.org/10.1145/22949.22950
Munzner, T. (2015). Visualization analysis & design. CRC Press.
Okabe, M. (2008). How to Make Figures and Presentations that are Friendly to Colorblind People [Https://jfly.uni-koeln.de/color/].
Tufte, E. (2001). The Visual Display of Quantitative Information (2a ed.). Graphics Press.
van der Walt, S. (2015). A better Default Colormap for Matplotlib. SciPy 2015.
Ware, C. (2021). Information visualization: perception for design (4th ed). Elsevier.
Wong, B. (2011). Points of view: Color blindness. Nature Methods, 8(6), 441–441. https://doi.org/10.1038/nmeth.1618