Interactividad

La importancia de la interactividad

Hasta ahora hemos construido visualizaciones estáticas: el lector observa, pero no puede explorar. La interactividad transforma una visualización en una herramienta de análisis (Heer & Shneiderman, 2012), permitiendo al usuario:

En esta lección aprenderemos a agregar:


Gráfico base

Partiremos del bubble chart de la lección anterior: gdp_usd vs expectativa de vida, con población como tamaño y región como color. Es el gráfico ideal para la interactividad porque tiene muchas variables y muchos puntos superpuestos, dos problemas que los controles interactivos resuelven de forma natural.

Ver código Plot.plot
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ño = población, Color = región",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (Billones USD)",
    grid: true,
    type: "log",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: { range: [3, 35] },
  color: {
    domain: dominioRegiones,
    range: coloresRegiones,
    legend: true,
  },
  marks: [
    Plot.dot(datosConRegion, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "Region",
      fillOpacity: 0.7,
      stroke: "white",
      strokeWidth: 1.5,
    }),
  ],
})

Este gráfico tiene dos limitaciones que la interactividad puede resolver:


Tooltips: mostrando información al inspeccionar

Observable Plot ofrece tres formas de mostrar información cuando el usuario interactúa con un elemento del gráfico, cada una con diferente nivel de control y presentación.


title: tooltip nativo del navegador

La propiedad title dentro de una marca define un texto que aparece como tooltip nativo del navegador al pasar el mouse sobre un elemento. Es la forma más simple y no requiere ninguna configuración adicional.

Ver código Plot.plot con title
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB, expectativa de vida y población mundial",
  subtitle: "Pasa el mouse sobre un punto para ver el país",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (Billones USD)",
    grid: true,
    type: "log",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: { range: [3, 35] },
  color: {
    domain: dominioRegiones,
    range: coloresRegiones,
    legend: true,
  },
  marks: [
    Plot.dot(datosConRegion, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "Region",
      fillOpacity: 0.7,
      stroke: "white",
      strokeWidth: 1.5,
      title: (d) => `${d.country}`,
    }),
  ],
})

¿Qué hace?

Limitaciones


tip: tooltip interactivo de Plot

La propiedad tip: true activa el sistema de tooltips propio de Observable Plot, que muestra automáticamente los valores de los canales mapeados (x, y, fill, r) con un diseño estilizado.

Ver código Plot.plot con tip: true
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB, expectativa de vida y población mundial",
  subtitle: "Pasa el mouse sobre un punto para ver los detalles",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (Billones USD)",
    grid: true,
    type: "log",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: { range: [3, 35] },
  color: {
    domain: dominioRegiones,
    range: coloresRegiones,
    legend: true,
  },
  marks: [
    Plot.dot(datosConRegion, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "Region",
      fillOpacity: 0.7,
      stroke: "white",
      strokeWidth: 1.5,
      tip: true,
    }),
  ],
})

¿Qué cambió?

Ventajas sobre title

Limitación

El tooltip de tip muestra los nombres de las columnas tal como están en los datos (por ejemplo, gdp_usd en vez de "PIB"). Para personalizar qué información aparece y cómo se presenta, necesitamos channels.


channels: información personalizada en tooltips

La propiedad channels permite agregar campos personalizados al tooltip de tip, controlando exactamente qué datos se muestran y con qué formato. Es la forma más completa de configurar tooltips en Observable Plot.

Ver código Plot.plot con channels
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB, expectativa de vida y población mundial",
  subtitle: "Pasa el mouse sobre un punto para ver los detalles del país",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (Billones USD)",
    grid: true,
    type: "log",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: { range: [3, 35] },
  color: {
    domain: dominioRegiones,
    range: coloresRegiones,
    legend: true,
  },
  marks: [
    Plot.dot(datosConRegion, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "Region",
      fillOpacity: 0.7,
      stroke: "white",
      strokeWidth: 1.5,
      tip: true,
      channels: {
        País: "country",
        Región: "Region",
        "Expectativa de vida": "life_expectancy",
        PIB: (d) => `$${(d.gdp_usd / 1e9).toFixed(1)}B`,
        Población: (d) => `${(d.population / 1e6).toFixed(1)}M`,
      },
    }),
  ],
})

¿Qué hace cada parte?

¿Cuándo usar cada tipo?

Tipo Cuándo usar Resultado
title Información mínima (solo nombre) Tooltip nativo del navegador
tip: true Información rápida, sin formato especial Tooltip estilizado con canales automáticos
tip + channels Información personalizada y formateada Tooltip estilizado con campos a medida

Recomendación: Usa tip: true con channels como opción predeterminada. Ofrece la mejor experiencia al usuario con mínimo esfuerzo.


Inputs: controles interactivos

Los Inputs son controles de interfaz (menús, sliders, checkboxes) que permiten al usuario modificar el gráfico en tiempo real. En Observable Framework, los inputs son reactivos: cuando cambian, el gráfico se actualiza automáticamente (Observable, Inc, s/f).

La sintaxis básica es:

const variable = view(Inputs.tipodeInput({opciones}));

Inputs.select: menú desplegable

Inputs.select crea un menú desplegable para elegir una opción de una lista. Es útil cuando hay varias opciones y se quiere mantener la interfaz compacta.

Ver código Inputs.select
const variableY = view(
  Inputs.select(
    [
      "life_expectancy",
      "birth_rate",
      "unemployment_rate_pct",
      "physicians_per_thousand",
    ],
    {
      label: "Variable en eje Y:",
      value: "life_expectancy",
    }
  )
)
Ver código Plot.plot con eje Y dinámico
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: `PIB vs ${variableY}`,
  subtitle: "Cambia la variable del eje Y con el selector de arriba",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (Billones USD)",
    grid: true,
    type: "log",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
  },
  y: {
    label: variableY,
    grid: true,
  },
  r: { range: [3, 35] },
  color: {
    domain: dominioRegiones,
    range: coloresRegiones,
    legend: true,
  },
  marks: [
    Plot.dot(datosConRegion, {
      x: "gdp_usd",
      y: variableY,
      r: "population",
      fill: "Region",
      fillOpacity: 0.7,
      stroke: "white",
      strokeWidth: 1.5,
      tip: true,
      channels: {
        País: "country",
        Región: "Region",
        [variableY]: variableY,
        PIB: (d) => `$${(d.gdp_usd / 1e9).toFixed(1)}B`,
      },
    }),
  ],
})

¿Qué cambió?

Se agregó antes del gráfico:

Y dentro del gráfico, variableY reemplaza el texto fijo "life_expectancy" en varios lugares:


Inputs.radio: botones de opción

Inputs.radio es similar a Inputs.select pero muestra todas las opciones como botones visibles simultáneamente:

Ver código Inputs.radio
const tipoEscala = view(
  Inputs.radio(["Lineal", "Logarítmica"], {
    label: "Escala del eje X:",
    value: "Logarítmica",
  })
)
Ver código Plot.plot con escala dinámica
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB vs expectativa de vida",
  subtitle: `Escala del eje X: ${tipoEscala}`,
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (Billones USD)",
    grid: true,
    type: tipoEscala === "Logarítmica" ? "log" : "linear",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: { range: [3, 35] },
  color: {
    domain: dominioRegiones,
    range: coloresRegiones,
    legend: true,
  },
  marks: [
    Plot.dot(datosConRegion, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "Region",
      fillOpacity: 0.7,
      stroke: "white",
      strokeWidth: 1.5,
      tip: true,
      channels: {
        País: "country",
        "Expectativa de vida": "life_expectancy",
        PIB: (d) => `$${(d.gdp_usd / 1e9).toFixed(1)}B`,
      },
    }),
  ],
})

¿Qué cambió?

Nota: El operador ternario funciona como una condición if/else en una sola línea.

¿Cuándo usar radio vs select?


Inputs.range: slider numérico

Inputs.range crea un deslizador para seleccionar un valor numérico dentro de un rango definido. Es ideal para filtrar datos por umbrales continuos.

Ver código Inputs.range
const poblacionMinima = view(
  Inputs.range([0, 500], {
    label: "Población mínima (millones):",
    step: 10,
    value: 0,
  })
)

Hacemos el filtro directamente en los datos para poderlos graficar de manera sencilla:

Ver preparación de datos
const datosFiltradosPoblacion = datosConRegion.filter(
  (d) => d.population >= poblacionMinima * 1e6
)
Ver código Plot.plot con filtro de población
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB vs expectativa de vida",
  subtitle: `Mostrando países con población ≥ ${poblacionMinima}M • ${datosFiltradosPoblacion.length} países`,
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (Billones USD)",
    grid: true,
    type: "log",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: { range: [3, 35] },
  color: {
    domain: dominioRegiones,
    range: coloresRegiones,
    legend: true,
  },
  marks: [
    Plot.dot(datosFiltradosPoblacion, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "Region",
      fillOpacity: 0.7,
      stroke: "white",
      strokeWidth: 1.5,
      tip: true,
      channels: {
        País: "country",
        Población: (d) => `${(d.population / 1e6).toFixed(1)}M`,
        "Expectativa de vida": "life_expectancy",
        PIB: (d) => `$${(d.gdp_usd / 1e9).toFixed(1)}B`,
      },
    }),
  ],
})

¿Qué cambió?

Se agregó:


Inputs.checkbox: selección múltiple

Inputs.checkbox permite activar o desactivar múltiples opciones simultáneamente. Es la elección natural cuando se quiere mostrar o esconder grupos de datos.

Ver código Inputs.checkbox
const regionesSeleccionadas = view(
  Inputs.checkbox(dominioRegiones, {
    label: "Mostrar regiones:",
    value: dominioRegiones,
  })
)
Ver preparación de datos
const datosFiltradosRegion = datosConRegion.filter((d) =>
  regionesSeleccionadas.includes(d["Region"])
)
Ver código Plot.plot con filtro de regiones
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "PIB vs expectativa de vida por región",
  subtitle: `${datosFiltradosRegion.length} países seleccionados`,
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (Billones USD)",
    grid: true,
    type: "log",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: { range: [3, 35] },
  color: {
    domain: dominioRegiones,
    range: coloresRegiones,
    legend: true,
  },
  marks: [
    Plot.dot(datosFiltradosRegion, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "Region",
      fillOpacity: 0.7,
      stroke: "white",
      strokeWidth: 1.5,
      tip: true,
      channels: {
        País: "country",
        Región: "Region",
        "Expectativa de vida": "life_expectancy",
        PIB: (d) => `$${(d.gdp_usd / 1e9).toFixed(1)}B`,
        Población: (d) => `${(d.population / 1e6).toFixed(1)}M`,
      },
    }),
  ],
})

¿Qué cambió?

Se agregó:

Diferencia con radio y select:


Inputs.search: búsqueda por texto

Inputs.search filtra los datos en tiempo real según el texto que escribe el usuario. Es especialmente útil para localizar países específicos en un gráfico con muchos puntos.

Ver código Inputs.search
const busqueda = view(
  Inputs.search(datosConRegion, {
    label: "Buscar país:",
    placeholder: "Escribe el nombre de un país...",
    columns: ["country"],
  })
)
Ver código Plot.plot con destacado de búsqueda
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "Búsqueda de países",
  subtitle: `${busqueda.length} país(es) encontrado(s)`,
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (Billones USD)",
    grid: true,
    type: "log",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
  },
  r: { range: [3, 35] },
  color: {
    domain: dominioRegiones,
    range: coloresRegiones,
    legend: true,
  },
  marks: [
    // Todos los puntos en gris al fondo para mantener el contexto
    Plot.dot(datosConRegion, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "#e5e7eb",
      fillOpacity: 0.4,
      stroke: "none",
    }),
    // Puntos del resultado destacados con su color de región
    Plot.dot(busqueda, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "Region",
      fillOpacity: 0.9,
      stroke: "white",
      strokeWidth: 2,
      tip: true,
      channels: {
        País: "country",
        Región: "Region",
        "Expectativa de vida": "life_expectancy",
        PIB: (d) => `$${(d.gdp_usd / 1e9).toFixed(1)}B`,
        Población: (d) => `${(d.population / 1e6).toFixed(1)}M`,
      },
    }),
    // Etiqueta con el nombre del país encontrado
    Plot.text(busqueda, {
      x: "gdp_usd",
      y: "life_expectancy",
      text: "country",
      fontSize: 12,
      dy: -14,
      stroke: "black",
      strokeWidth: 1,
    }),
  ],
})

¿Qué cambió?

Este gráfico usa tres marcas combinadas para lograr un efecto de destacado:

  1. Todos los puntos en gris: mantiene el contexto visual para ver dónde está el país buscado en relación al resto del mundo
  2. Puntos del resultado: los países encontrados se muestran con su color de región, alta opacidad y tip + channels
  3. Etiquetas del resultado: los países encontrados se etiquetan con su nombre

Propiedades de Inputs.search:


Combinando inputs

Es posible combinar varios inputs para crear un explorador más completo. Cada input controla un aspecto diferente del filtro y se aplican en cadena:

Ver código Inputs.checkbox
const regionesCombinadas = view(
  Inputs.checkbox(dominioRegiones, {
    label: "Regiones:",
    value: dominioRegiones,
  })
)
Ver código Inputs.range
const expectativaMin = view(
  Inputs.range([40, 90], {
    label: "Expectativa de vida mínima (años):",
    step: 1,
    value: 40,
  })
)
Ver preparación de datos
const datosCombinados = datosConRegion
  .filter((d) => regionesCombinadas.includes(d["Region"]))
  .filter((d) => d["life_expectancy"] >= expectativaMin)
Ver código Plot.plot con filtros combinados
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: "Explorador de países",
  subtitle: `${datosCombinados.length} países • Expectativa de vida ≥ ${expectativaMin} años`,
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: "PIB (Billones USD)",
    grid: true,
    type: "log",
    tickFormat: (d) => `$${(d / 1e9).toFixed(0)}B`,
  },
  y: {
    label: "Expectativa de vida (años)",
    grid: true,
    domain: [40, 90],
  },
  r: { range: [3, 35] },
  color: {
    domain: dominioRegiones,
    range: coloresRegiones,
    legend: true,
  },
  marks: [
    Plot.dot(datosCombinados, {
      x: "gdp_usd",
      y: "life_expectancy",
      r: "population",
      fill: "Region",
      fillOpacity: 0.7,
      stroke: "white",
      strokeWidth: 1.5,
      tip: true,
      channels: {
        País: "country",
        Región: "Region",
        "Expectativa de vida": "life_expectancy",
        PIB: (d) => `$${(d.gdp_usd / 1e9).toFixed(1)}B`,
        Población: (d) => `${(d.population / 1e6).toFixed(1)}M`,
      },
    }),
  ],
})

¿Cómo funciona la combinación?

Cada input es independiente y su valor se aplica como un filtro adicional en cadena:

const datosCombinados = datosConRegion
  .filter(d => regionesCombinadas.includes(d["Region"]))  // Filtro 1: región
  .filter(d => d["life_expectancy"] >= expectativaMin);   // Filtro 2: expectativa

Observable Framework ejecuta las celdas en orden y actualiza automáticamente todas las celdas que dependen de un valor que cambió, por eso el gráfico se actualiza solo cuando el usuario mueve el slider o cambia las regiones.


Aplicando interactividad a otros tipos de gráficos

Los inputs y tooltips funcionan igual en cualquier tipo de marca. Veamos cómo aplicar lo aprendido a los tipos de gráficos de las lecciones anteriores.


Histograma interactivo

Un selector de variable y un slider para controlar el número de bins permiten al usuario explorar diferentes distribuciones:

Ver código Inputs.select
const variableHist = view(
  Inputs.select(
    [
      "life_expectancy",
      "birth_rate",
      "unemployment_rate_pct",
      "physicians_per_thousand",
    ],
    {
      label: "Variable a visualizar:",
      value: "life_expectancy",
    }
  )
)
Ver código Inputs.range
const numBins = view(
  Inputs.range([5, 40], {
    label: "Número de grupos (bins):",
    step: 1,
    value: 15,
  })
)
Ver código Plot.plot histograma interactivo
Plot.plot({
  width: 900,
  height: 450,
  marginTop: 50,
  title: `Distribución de ${variableHist} a nivel mundial`,
  subtitle: `${numBins} grupos • Datos de 195 países (2023)`,
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: variableHist,
    grid: true,
  },
  y: {
    label: "Número de países",
    grid: true,
  },
  marks: [
    Plot.rectY(
      datos.filter((d) => d[variableHist] != null),
      Plot.binX(
        { y: "count" },
        {
          x: variableHist,
          thresholds: numBins,
          fill: "steelblue",
          fillOpacity: 0.7,
          stroke: "white",
          strokeWidth: 2,
          tip: true,
        }
      )
    ),
    Plot.ruleY([0]),
  ],
})

¿Qué hay de nuevo?


Barras horizontales interactivas

Un selector de variable, un slider para el número de países y un radio para el orden de visualización:

Ver código Inputs.select
const variableBarra = view(
  Inputs.select(["life_expectancy", "birth_rate", "physicians_per_thousand"], {
    label: "Variable a comparar:",
    value: "life_expectancy",
  })
)
Ver código Inputs.range
const numPaises = view(
  Inputs.range([5, 30], {
    label: "Número de países:",
    step: 1,
    value: 10,
  })
)
Ver código Inputs.radio
const orden = view(
  Inputs.radio(["Mayor a menor", "Menor a mayor"], {
    label: "Ordenar por:",
    value: "Mayor a menor",
  })
)
Ver preparación de datos
const datosBarras = datos
  .filter((d) => d[variableBarra] != null)
  .sort((a, b) =>
    orden === "Mayor a menor"
      ? b[variableBarra] - a[variableBarra]
      : a[variableBarra] - b[variableBarra]
  )
  .slice(0, numPaises)
Ver código Plot.plot barras interactivas
Plot.plot({
  width: 900,
  height: 50 + numPaises * 28,
  marginLeft: 160,
  marginBottom: 50,
  title: `Top ${numPaises} países por ${variableBarra}`,
  subtitle: `Ordenado de ${orden.toLowerCase()} • Año 2023`,
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: variableBarra,
    grid: true,
  },
  y: {
    label: null,
    domain: datosBarras.map((d) => d.country),
  },
  marks: [
    Plot.barX(datosBarras, {
      x: variableBarra,
      y: "country",
      fill: "steelblue",
      fillOpacity: 0.8,
      stroke: "darkblue",
      strokeWidth: 1,
      tip: true,
      channels: {
        País: "country",
        Continente: "continent",
        [variableBarra]: variableBarra,
      },
    }),
    Plot.text(datosBarras, {
      x: variableBarra,
      y: "country",
      text: (d) => `${d[variableBarra]}`,
      textAnchor: "start",
      dx: 5,
      fontSize: 11,
      fill: "black",
    }),
  ],
})

¿Qué hay de nuevo?


Scatter plot interactivo con búsqueda

Un scatter plot donde podemos elegir las variables de ambos ejes y buscar países específicos:

Ver código Inputs.select eje X
const ejeX = view(
  Inputs.select(
    [
      "birth_rate",
      "infant_mortality",
      "unemployment_rate_pct",
      "fertility_rate",
    ],
    {
      label: "Variable eje X:",
      value: "infant_mortality",
    }
  )
)
Ver código Inputs.select eje Y
const ejeY = view(
  Inputs.select(
    [
      "life_expectancy",
      "physicians_per_thousand",
      "birth_rate",
      "fertility_rate",
    ],
    {
      label: "Variable eje Y:",
      value: "life_expectancy",
    }
  )
)
Ver código Inputs.search
const busquedaScatter = view(
  Inputs.search(datosConRegion, {
    label: "Buscar país:",
    placeholder: "Escribe para destacar un país...",
    columns: ["country"],
  })
)
Ver código Plot.plot scatter con búsqueda
Plot.plot({
  width: 900,
  height: 550,
  marginLeft: 80,
  title: `${ejeX} vs ${ejeY}`,
  subtitle:
    busquedaScatter.length < datosConRegion.length
      ? `${busquedaScatter.length} país(es) destacado(s)`
      : "Datos de 195 países (2023)",
  caption: "Fuente: World Data 2023 (Kaggle)",
  x: {
    label: ejeX,
    grid: true,
  },
  y: {
    label: ejeY,
    grid: true,
  },
  color: {
    domain: dominioRegiones,
    range: coloresRegiones,
    legend: true,
  },
  marks: [
    Plot.dot(
      datosConRegion.filter((d) => d[ejeX] != null && d[ejeY] != null),
      {
        x: ejeX,
        y: ejeY,
        fill:
          busquedaScatter.length < datosConRegion.length ? "#e5e7eb" : "Region",
        fillOpacity: busquedaScatter.length < datosConRegion.length ? 0.4 : 0.7,
        stroke: "none",
        r: 5,
      }
    ),
    Plot.dot(
      busquedaScatter.filter((d) => d[ejeX] != null && d[ejeY] != null),
      {
        x: ejeX,
        y: ejeY,
        fill: "Region",
        fillOpacity: 0.9,
        stroke: "white",
        strokeWidth: 2,
        r: 7,
        tip: true,
        channels: {
          País: "country",
          Región: "Region",
          [ejeX]: ejeX,
          [ejeY]: ejeY,
        },
      }
    ),
    Plot.text(
      busquedaScatter.filter((d) => d[ejeX] != null && d[ejeY] != null),
      {
        x: ejeX,
        y: ejeY,
        text: "country",
        fontSize: 9,
        dy: -14,
        stroke: "black",
        strokeWidth: 1,
      }
    ),
  ],
})

¿Qué hay de nuevo?


Resumen de tooltips e inputs

Herramienta Resultado Cuándo usar
title Tooltip nativo del navegador Información mínima, exportación a imagen
tip: true Tooltip estilizado con canales automáticos Información rápida sin formato especial
tip + channels Tooltip estilizado con campos personalizados Información formateada y legible
Inputs.select Una opción, menú compacto Muchas opciones (5+)
Inputs.radio Una opción, botones visibles Pocas opciones (2-4)
Inputs.range Valor numérico continuo Filtrar por umbrales
Inputs.checkbox Múltiples opciones Mostrar/ocultar grupos
Inputs.search Array filtrado por texto Buscar elementos específicos

Conclusiones de la lección

En esta lección se ha explorado cómo transformar visualizaciones estáticas en herramientas interactivas de exploración de datos.


Conceptos clave

Tooltips

Inputs reactivos

Filtrado de datos

Variables compartidas


Reflexión final

La interactividad no debe agregarse por agregar: cada control debe responder una pregunta concreta que el usuario podría hacerse al ver el gráfico. Antes de agregar un input, pregúntate:

Una visualización con pocos controles bien pensados es casi siempre mejor que una sobrecargada de opciones que confunden al lector.


Bibliografía

Heer, J., & Shneiderman, B. (2012). Interactive dynamics for visual analysis. Communications of the ACM, 55(4), 45–54. https://doi.org/10.1145/2133806.2133821
Observable, Inc,. (s/f). Observable Inputs Documentation.