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:
- Explorar los datos a su propio ritmo
- Filtrar para enfocarse en lo que le interesa
- Comparar subconjuntos específicos
- Identificar valores concretos al inspeccionar elementos
En esta lección aprenderemos a agregar:
- Tooltips para mostrar información al inspeccionar elementos
- Inputs.select / Inputs.radio para elegir categorías
- Inputs.range para filtrar por valores numéricos
- Inputs.checkbox para seleccionar múltiples grupos
- Inputs.search para buscar elementos específicos
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:
- Con tantos puntos no sabemos qué país es cada uno sin etiquetarlos todos
- No podemos enfocarnos en una región o rango específico sin crear un gráfico nuevo
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?
- title: d =>
${d.country}: una función que recibe cada dato (d) y devuelve el texto del tooltip - El tooltip es un rectángulo amarillo del navegador que aparece al mantener el cursor sobre el punto
- Se puede incluir múltiples líneas usando \n:
title: d => [línea1, línea2].join("\n")
Limitaciones
- Apariencia básica: el estilo depende del navegador, sin formato ni colores
- Demora al aparecer: el tooltip nativo tiene un retardo de ~1 segundo
- Sin interacción: no se puede seleccionar texto ni hacer clic en el contenido
- No muestra líneas guía para ubicar el valor en los ejes
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ó?
- tip: true reemplaza a title: activa el tooltip interactivo de Plot
- El tooltip muestra automáticamente los valores de x, y, fill y r (todos los canales mapeados)
- Aparece instantáneamente al acercar el cursor, sin retardo
- Incluye líneas guía (crosshair) que facilitan ubicar el valor exacto en los ejes
Ventajas sobre title
- Diseño estilizado con formato automático
- Muestra todas las variables mapeadas sin necesidad de construir el texto manualmente
- Respuesta instantánea
- Líneas guía para localizar valores en los ejes
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?
- tip: true: activa el tooltip interactivo de Plot (necesario para que channels funcione)
- channels: { ... }: define los campos que aparecerán en el tooltip
- País: "country": la clave
"País"es la etiqueta visible, el valor"country"es la columna del dato - PIB: d =>
$${...}: cuando el valor es una función, permite formatear el dato antes de mostrarlo - Los channels reemplazan los canales automáticos (x, y, fill, r) en el tooltip, dándote control total
¿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}));
- view(): hace que el input sea visible en la página
- const variable: guarda el valor seleccionado
- La variable se actualiza automáticamente cuando el usuario interactúa
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:
- Inputs.select(): crea el menú con las opciones disponibles
- value: "life_expectancy": valor seleccionado por defecto
- label: texto descriptivo que aparece junto al selector
Y dentro del gráfico, variableY reemplaza el texto fijo "life_expectancy" en varios lugares:
- title: el título del gráfico se actualiza dinámicamente
- y: variableY: el eje Y cambia según la selección
- label: variableY: la etiqueta del eje Y también cambia
- [variableY]: variableY en channels: el tooltip muestra la variable seleccionada con su nombre como etiqueta (la sintaxis con corchetes permite usar una variable como clave de objeto)
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ó?
- Inputs.radio(): muestra las opciones como botones visibles en lugar de menú desplegable
- type: tipoEscala === "Logarítmica" ? "log" : "linear": cambia la escala del eje X según la selección usando un operador ternario
Nota: El operador ternario funciona como una condición if/else en una sola línea.
¿Cuándo usar radio vs select?
- radio: pocas opciones (2-4), cuando conviene que todas sean visibles de un vistazo
- select: muchas opciones (5+) o cuando el espacio es limitado
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.range([0, 500]): slider con valores entre 0 y 500
- step: 10: el slider avanza de 10 en 10
- datosFiltradosPoblacion: nueva variable con los datos filtrados según el valor del slider
- El subtítulo muestra dinámicamente cuántos países quedan tras el filtro
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ó:
- Inputs.checkbox(dominioRegiones): usa la variable compartida con el domain del color para garantizar consistencia
- value: dominioRegiones: todas las regiones seleccionadas por defecto
- .includes(d["Region"]): filtra solo los países cuya región está en el array seleccionado
Diferencia con radio y select:
- checkbox: múltiples opciones simultáneas (mostrar Europa Y Asia al mismo tiempo)
- radio / select: solo una opción a la vez
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:
- 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
- Puntos del resultado: los países encontrados se muestran con su color de región, alta opacidad y tip + channels
- Etiquetas del resultado: los países encontrados se etiquetan con su nombre
Propiedades de Inputs.search:
- columns: ["country"]: define en qué columna buscar, puede incluir varias
- placeholder: texto de ayuda que aparece en el campo vacío
- El resultado es siempre un array, incluso si hay un solo resultado
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?
- thresholds: numBins: controla el número de grupos del histograma con el slider
- variableHist: cambia completamente la variable visualizada con el selector
- tip: true: en histogramas, el tooltip automático muestra el rango del bin y el conteo, que es exactamente lo que se necesita
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?
- height: 50 + numPaises * 28: el alto del gráfico crece dinámicamente según el número de países, evitando que las barras se compriman
- orden === "Mayor a menor" ? ... : ...: el operador ternario invierte el ordenamiento según la selección del radio
- domain: datosBarras.map(d => d.country): fija el orden del eje Y para que coincida con el ordenamiento de los datos
- channels usa [variableBarra] como clave dinámica para mostrar la variable seleccionada en el tooltip
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?
- channels con claves dinámicas: [ejeX]: ejeX y [ejeY]: ejeY muestran en el tooltip las variables que el usuario seleccionó, con sus valores reales
- El gráfico detecta si hay una búsqueda activa (busquedaScatter.length < datosConRegion.length) para mostrar todos los puntos con color o solo los destacados
- Se eliminó
gdp_usddel eje X para evitar números ilegibles; las variables disponibles tienen rangos numéricos más naturales
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
- title: tooltip nativo del navegador, simple y universal pero con retardo y estilo básico
- tip: true: tooltip interactivo de Plot que muestra automáticamente los canales mapeados con líneas guía
- tip + channels: máximo control sobre qué información aparece y cómo se formatea
- La combinación tip + channels es la opción recomendada para la mayoría de los casos
Inputs reactivos
- Se declaran con const variable = view(Inputs.tipo({opciones}))
- El valor se actualiza automáticamente cuando el usuario interactúa
- Se usan directamente en el gráfico como cualquier variable de JavaScript
- Los datos filtrados deben estar en celdas separadas del gráfico
Filtrado de datos
- Los inputs no modifican los datos originales, siempre se filtra en una nueva variable
- Se pueden encadenar múltiples filtros con .filter().filter()
- Mostrar el conteo de elementos filtrados en el subtítulo orienta al usuario
Variables compartidas
- Definir dominioRegiones y coloresRegiones una sola vez evita inconsistencias
- Cualquier cambio en las regiones se refleja automáticamente en todos los inputs y gráficos que las usen
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:
- ¿Qué pregunta responde este filtro?
- ¿El usuario realmente necesita explorar esta dimensión?
- ¿La interactividad simplifica o complica la lectura?
Una visualización con pocos controles bien pensados es casi siempre mejor que una sobrecargada de opciones que confunden al lector.