Saltar al contenido

El enfoque híbrido y el árbol de decisión

Dos capas superpuestas con el mismo sistema de coordenadas: lienzo para los datos densos, SVG para ejes, etiquetas e interacción, con el código de alineación que hace que no se descuadren.

El reparto

La regla, que ya salía en los tres artículos anteriores y aquí se hace explícita:

Al lienzo van las marcas de datos: los puntos de una dispersión, las líneas de una serie densa, las celdas de un mapa de calor, las áreas de un treemap grande. Son muchas, cambian juntas y no necesitan identidad individual en el DOM.

Al SVG va todo lo demás: la línea de los ejes, las marcas, las etiquetas numéricas, los títulos, la rejilla, las líneas de referencia, las anotaciones, la leyenda, el resaltado de la marca bajo el puntero, la retícula, el rectángulo de selección y las zonas de acierto ampliadas.

El criterio en una frase: si hay menos de unos cientos y lleva texto o interacción, va arriba en SVG.

Y una tercera capa que a veces conviene: un segundo lienzo para lo que cambia en cada fotograma. Si los datos son estáticos y solo se mueve el resaltado, tener el fondo en un lienzo que se dibuja una vez y el resaltado en otro encima evita repintar cien mil marcas por cada movimiento del ratón.

El montaje

HTML
<div class="gr" style="position:relative;width:100%;height:360px">  <canvas class="gr-datos" style="position:absolute;inset:0;width:100%;height:100%"></canvas>  <svg class="gr-encima" style="position:absolute;inset:0;width:100%;height:100%"></svg></div>
JS
const NS = 'http://www.w3.org/2000/svg';const M = { arriba: 16, dcha: 96, abajo: 32, izda: 48 };function montar(caja) {  const lienzo = caja.querySelector('.gr-datos');  const svg = caja.querySelector('.gr-encima');  const ctx = lienzo.getContext('2d');  function dimensionar() {    const { width: w, height: h } = caja.getBoundingClientRect();    const dpr = devicePixelRatio;    // Capa de datos: buffer en pixeles de dispositivo    lienzo.width = Math.round(w * dpr);    lienzo.height = Math.round(h * dpr);    // Una sola transformacion: escala del dispositivo mas el margen    ctx.setTransform(dpr, 0, 0, dpr, M.izda * dpr, M.arriba * dpr);    // Capa de encima: coordenadas CSS, uno a uno    svg.setAttribute('viewBox', `0 0 ${w} ${h}`);    return { w: w - M.izda - M.dcha, h: h - M.arriba - M.abajo };  }  return { lienzo, svg, ctx, dimensionar };}

Cuatro decisiones de ese código que son las que hacen que funcione.

El viewBox del SVG está en píxeles CSS, uno a uno. Así una coordenada vale lo mismo en las dos capas y no hay ninguna conversión que recordar. Un viewBox con otras unidades funciona igual de bien y obliga a convertir en cada punto de contacto, que es donde entran los errores.

El margen está dentro de la transformación del contexto. Con setTransform(dpr, 0, 0, dpr, M.izda * dpr, M.arriba * dpr), todo lo que dibujes en el lienzo usa coordenadas relativas al área de dibujo, igual que el grupo desplazado de SVG. Es el convenio de márgenes de el patrón de componente de gráfico aplicado a las dos capas a la vez. Fíjate en que el desplazamiento va multiplicado por el ratio, porque setTransform sustituye la matriz entera y sus dos últimos componentes están en píxeles de dispositivo.

El grupo del SVG lleva el mismo desplazamiento.

JS
const g = document.createElementNS(NS, 'g');g.setAttribute('transform', `translate(${M.izda},${M.arriba})`);

Los eventos. Decide una capa que los reciba y desactiva la otra. Lo habitual es que los reciba el SVG, porque ahí están las zonas de acierto y los elementos enfocables:

CSS
.gr-datos { pointer-events: none; }

Si prefieres resolverlos en el lienzo con hit testing, invierte el pointer-events y pon none en el SVG, salvo en los elementos concretos que sí deban recibirlos.

El árbol de decisión

Tres lecturas del árbol que conviene tener presentes.

La primera pregunta manda sobre todas las demás. Si el gráfico se imprime en un informe, o si tiene que ser navegable con lector de pantalla, o si va en un correo, el recuento de elementos es secundario: la respuesta es SVG, y si hay demasiados elementos, el trabajo consiste en reducirlos (agregando, filtrando, muestreando) y no en cambiar de medio.

La rama del híbrido es la que más se elige en la práctica para gráficos de producción con datos densos, porque «hacen falta ejes y etiquetas» es cierto casi siempre.

«Lienzo puro» es una hoja legítima, no un descuido: una visualización artística, un fondo generativo, un minimapa, un juego. Lo que la caracteriza es que no hay nada que leer.

Y una cuarta salida que el árbol no muestra porque queda fuera de este track: cuando ni siquiera el lienzo 2D llega, el siguiente paso es WebGL o WebGPU, con la rasterización hecha por la GPU. El punto de corte suele estar en el orden de los cientos de miles de marcas animadas.