Hay una buena posibilidad de que te hayas tropezado aquí sin saber realmente qué es Canvas o D3, así que antes de sumergirnos, tomémonos un minuto para hacer un rápido resumen. D3 (o D3.js) es la abreviatura de tres importantes palabras D: Documentos de datos. Es una opción común – y excelente – para construir visualizaciones interactivas para la web. D3 sobresale cuando los datos deben ser vinculados a elementos interactivos, y cuando se desea una transición sin problemas.
Y Canvas es una forma de incrustar gráficos basados en píxeles en tus páginas web HTML, lo que permite un rendimiento y efectos que no serían posibles con el HTML normal (o el primo del arte vectorial del HTML, SVG).
Aunque a menudo se utilizan por separado, D3 y Canvas pueden combinarse para producir visualizaciones interactivas atractivas y de alto rendimiento. Y es por eso que elegí usar D3.js y Canvas juntos para construir la página de aterrizaje interactiva para mostrar 5.000 cursos y contando. Profundicemos en el desarrollo con D3 y veamos cómo.
Tutorial de D3: D3 y Canvas en acción
Para celebrar el haber pasado la marca de los 5.000 cursos (¡vaya, son muchos cursos!), quería una visualización divertida que ayudara a capturar el hito. Queríamos producir algo parecido a la visualización de la Encuesta de Tasas de los Independientes de Peter Cook con recuadros que representaran cada uno de los más de 5.000 cursos, animados interactivamente en diferentes grupos.
La visualización de Pete usó el enfoque estándar de D3.js, creando elementos SVG RECT separados para cada caja. El enfoque estándar con D3 es crear elementos DOM, ya sea HTML o SVG. Esta suele ser una buena forma de trabajar, pero tiene limitaciones en el número de elementos que puede manejar a la vez, particularmente en cuanto a la cantidad de elementos que pueden ser animados al mismo tiempo. Así que esto funcionó muy bien para 566 cajas pero, como puedes imaginar, no funcionaría tan bien para 5.000. Aquí es donde Canvas entró en juego. Para trabajar en este tema, puedes renderizar en el Lienzo en lugar del DOM. (Esto también abre posibilidades para gráficos y efectos que serían difíciles con elementos DOM.)
Tutorial de D3: Entonces, ¿qué es Canvas?
El lienzo es un elemento HTML que puedes incrustar en tu documento HTML, y dibujar formas y texto usando JavaScript. A nivel conceptual, el HTML regular es un árbol de elementos a partir de los cuales el navegador resuelve cómo dibujar la página, mientras que el lienzo es una cuadrícula de píxeles sobre la que tienes control directo.
Y si tener el control directo no es suficiente para persuadirte, considera la simplicidad de Canvas. Para dibujar en el Lienzo desde JavaScript, todo lo que necesitas es obtener un «contexto» 2D, que es un objeto que puedes usar para dibujar en el lienzo usando simples llamadas de método para hacer líneas, círculos, texto y cualquier otra cosa que puedas necesitar. Puedes obtener un contexto de dibujo llamando al método getContext en el elemento del lienzo pasando la cadena «2d».
var myCanvas = document.querySelector($0027canvas$0027);var context = myCanvas.getContext($00272d$0027);
Tutorial de D3: Usando el lienzo
Ahora tienes un contexto que puedes usar métodos de llamada para dibujar rectángulos, círculos, líneas, texto y caminos (que consisten en líneas y curvas). Un contexto de dibujo tiene un color de relleno actual, un color de trazo actual (el color de la línea del contorno) y un valor alfa actual (qué tan transparente es) que cualquier cosa que dibujes usará.
Lo importante es recordar que estás dibujando píxeles directamente en el buffer del lienzo. Así que, a diferencia del SVG, una vez que has dibujado algo, las cosas que estaban debajo han desaparecido para siempre (a menos que estés dibujando de forma semitransparente usando el valor alfa). Debes tener esto en mente porque la única manera de borrar algo es dibujar encima de él otra vez.
Para dibujar una caja que represente un curso, primero hay que establecer el color de relleno. Una vez hecho esto, dibuja un rectángulo relleno en un punto determinado. El sistema de coordenadas está en píxeles y es relativo a la esquina superior izquierda del elemento del lienzo.
context.fillColor = "rojo";context.fillRect(izquierda, arriba, ancho, alto);
Hay algunos enfoques que podría tomar para actualizar los gráficos dibujados en un lienzo. El primero es dibujar sobre el contenido existente, cambiando sólo las áreas que lo necesitan. Esto es eficiente si sólo necesitas actualizar pequeñas partes, pero puede ser complicado averiguar exactamente lo que ha cambiado cada vez. Un enfoque más simple es simplemente borrar el contenido del lienzo y volver a dibujar todo cada vez que se cambia algo. Este enfoque de borrar y redibujar suena lento, pero dependiendo de la complejidad de tu dibujo, puede ser en realidad increíblemente rápido.
Ahora estamos listos para escribir una función de dibujo que se puede llamar cada cuadro al actualizar la vista. La función de dibujo para esta visualización sólo necesita dibujar un cuadro para cada curso, nos encargaremos de los encabezados de texto y otros bits más tarde. A la función se le dará información sobre la posición y el color de cada cuadro y un tamaño único para todos ellos. También se le dará una bandera para cada cuadro indicando el alfa (su nivel de transparencia). Pasaremos los valores como matrices paralelas en lugar de una sola matriz de objetos porque esto facilita las cosas más adelante.
función dibujar(contexto, tamaño, izquierda, derecha, color, alfa) { // Primero despeja el lienzo de cualquier dibujo existente context.clearRect(0, 0, context.canvas.width, context.canvas.height); for(var i = 0; i < left. longitud; i++) { // Optimización para bloques completamente transparentes (por ejemplo, ocultos) si (alpha[i] == 0) continúa; context.fillStyle = color[i]; context.globalAlpha = alpha[i]; context.fillRect(left[i], right[i], size, size); }}
Tutorial de D3: Posición, posición, posición
Puedes ver que la función de dibujo no decide nada acerca de dónde se colocan las cajas. Debido a esto, es una buena práctica separar completamente la disposición del código de dibujo real. Ahora podemos escribir una función que calcule la ubicación de cada caja completamente independiente del código de dibujo.
Para calcular la posición de cada cuadro en esta visualización particular, necesitamos primero agruparlos por la agrupación actual (según lo establecido por el usuario) y ordenarlos. También querremos almacenar la posición en la que comienza cada grupo porque necesitaremos esa información para posicionar las etiquetas más tarde.
El código de cálculo de la posición calcula dónde debe moverse cada caja de curso, pero no queremos moverlas directamente allí. En su lugar, nos gustaría animar suavemente desde su posición anterior a la nueva posición. Cuando se trabaja con elementos DOM en D3.js esto es tan simple como añadir un .transition() antes de establecer los atributos, pero es ligeramente más complejo cuando estamos dibujando en el lienzo. Por suerte, todavía podemos usar las utilidades de D3 para la interpolación y la facilitación.
La función d3.interpolar puede tomar dos valores -un valor anterior y uno nuevo- y devolver una función que «interpola» entre ambos. Esta función devuelta acepta un valor entre 0 y 1; a 0 devuelve el valor anterior, y a 1 el nuevo valor. Para los valores entre 0 y 1, devolverá un valor entre el anterior y el nuevo.
D3 es muy inteligente en la interpolación. Puede interpolar números, colores, fechas, e incluso es lo suficientemente bueno para interpolar múltiples números en una cadena (por ejemplo, «300 12px sans-serif» a «500 36px Comic-Sans»). Si se dan matrices, d3.interpolate devuelve un interpolador que interpola cada valor de la primera matriz al valor correspondiente de la segunda. Incluso funciona si la segunda matriz es más larga que la primera (sólo devuelve los nuevos valores de esos elementos y no se molesta en interpolar).
Así que podemos obtener las coordenadas, el color y el alfa (también conocido como transparencia) en matrices separadas y luego usar la función d3.interpolate para crear un interpolador para ellos. Sólo necesitamos hacer esto una vez por cada vez que cambie la agrupación.
let x = [], y = [], color = [], alpha = [];for(let i = 0; i < courses.length; i++) { x.push(courses[i].x); y.push(courses[i].y); color.push(getColor(courses[i])); alpha.push(isVisible(courses[i]) ? 1 : 0);}let ix = d3.interpolate(lastX, x);let iy = d3.interpolate(lastY, y);let icolor = d3.interpolate(lastColor, color);let idraw = d3.interpolate(lastAlpha, alpha);
Puedes ver que el alfa está puesto en 1 (totalmente opaco) para los cursos actualmente visibles, y en 0 (totalmente transparente) para los invisibles. La interpolación significa que tendremos un buen desvanecimiento dentro y fuera cuando los cursos se vuelvan invisibles o reaparezcan.
Con los interpoladores en su lugar, ahora tenemos que ejecutar un bucle de animación que redibuja al lienzo cada fotograma. D3.js proporciona una función útil llamada d3.timer que llamará a una función de devolución de llamada una vez por cada frame que pase en el número de milisegundos desde que el temporizador comenzó.
La función de devolución de llamada necesita convertir primero el tiempo transcurrido desde que comenzó la transición en un número entre 0 y 1 que indique el progreso. Luego llama a cada uno de los interpoladores para obtener los valores actualizados de las posiciones y los colores, y pasa los valores resultantes a la función de sorteo definida anteriormente. Cuando la transición se completa, devolvemos el valor verdadero de la llamada, lo que indica que el temporizador debe detenerse.
d3.timer(function(timeSinceStart) { let t = Math.min(timeSinceStart/duration, 1); draw(context, size, idraw(t), ix(t), iy(t), icolor(t)); if (t === 1) { return true; }});
Tutorial de D3: Cómo ser interactivo
Hasta ahora hemos dibujado un montón de elementos en la página, pero ¿qué pasa si también queremos que el usuario interactúe con ellos? Para esta visualización, los mouseovers debían ser detectados en las cajas que representan los cursos así como los clics. En una página normal de HTML y SVG se podrían establecer manejadores de eventos en los elementos que representan cada caja, pero como estamos usando Canvas eso no es posible.
En su lugar, necesitamos poner manejadores de eventos en el propio elemento de lona. Todavía tendremos los eventos del ratón, pero no estarán automáticamente vinculados a un elemento en particular que hayamos dibujado. Podemos, sin embargo, obtener las coordenadas del evento relativas a nuestro elemento de lienzo usando d3.mouse.
canvas.on($0027click$0027, () = > { // Obtener las coordenadas de click relativas al elemento canvas var coordinates = d3.mouse(canvas.node()); // Coordenadas es un arreglo de 2 elementos: [x,y]; var course = getCourseAtCoordinates(coordenadas[0], coordenadas[1]); if (course) { window.open(course.url, $0027_new$0027); }});
Como pueden ver, nos corresponde a nosotros escribir el código para tomar las coordenadas del evento del ratón y averiguar su correspondiente elemento. Recuerden que ya tenemos las coordenadas x e y almacenadas en una estructura de datos para cada una de nuestras cajas, así que el método más simple es hacer un bucle sobre cada una de ellas y comprobar cada una a su vez hasta que encontremos una coincidencia. Pero como ya habrán adivinado, esto es ineficiente… y para una disposición tan regular hay una forma mucho más rápida.
Para esta visualización específica, decidí encontrar el grupo/título del ratón que estaba bajo el primero (usando la matriz groupTop calculada anteriormente). Luego calculé el índice del curso dentro de eso con algunas matemáticas simples.
función getCourseAtCoordinates(x, y) { for(let i = groupKeys.length - 1; i >= 0; i--) { let g = groupKeys[i]; if (groupTop[g] < y) { // Sabemos que estamos en este grupo, conocemos el tamaño y el espacio de los bloques // por lo que averiguar cuál es la fila y la columna a la que estamos apuntando es fácil. var row = Math.floor((y - groupTop[g] - groupSpacing) / (blockSize + spacing)); var col = Math.floor(x / (blockSize + spacing)); // Ahora obtendremos el índice del curso var index = row * cols + col; // Y finalmente el curso mismo var course = groups[g][index]; return course || null; } } retorno nulo;}
Esto funciona bien para nuestra visualización específica, principalmente porque las áreas de impacto son simples (son sólo cajas) y están regularmente dispuestas en una cuadrícula. Pero para diferentes situaciones hay técnicas más complejas. Para tomar una gran cantidad de objetos en el espacio 2d, y luego localizar el más cercano a un punto dado, está el d3.quadtree de D3.
Aunque si tienes formas irregulares, deberías considerar dibujar todo dos veces; una vez a tu elemento de lienzo normal y otra vez a un lienzo separado de detección de colisiones fuera de la pantalla. Al dibujar en el lienzo de detección de colisiones, elegirás un color diferente para cada elemento (tienes 16 millones para elegir). Luego, cuando quieras detectar un objeto puedes tomar una muestra del color en las coordenadas actuales del ratón.
Tutorial de D3: Mezclar y combinar
El lienzo funciona muy bien para ciertos tipos de elementos, pero lo más probable es que también tenga otras partes de su visualización que serían más fáciles de construir utilizando elementos regulares de HTML o SVG DOM. Por supuesto, no hay nada malo en usar un poco de ambos, e incluso puedes posicionar los elementos DOM sobre la parte superior del lienzo para combinarlos. La visualización utiliza esta técnica para crear los encabezados de grupo y posicionarlos en los huecos dejados por la función de dibujo del lienzo. Debido a que calculamos las posiciones de los grupos por separado del código de dibujo, es fácil reutilizar esas coordenadas para posicionar los elementos de encabezado.
Tutorial de D3: Para llevar
Ya han visto cómo usé D3 para dar vida a los datos y crear los 5.000 cursos de visualización de datos.
Ahora, es hora de que lo pongas en acción. Si estás listo para aprender D3.js y comenzar con las visualizaciones interactivas de D3, mira mi introducción a los cursos de D3.js y D3 aquí.
COMPARTIR: