D3 y Canvas en 3 pasos

La unión, el sorteo y la interactividad.

Supongamos que está creando una visualización de datos utilizando D3 y SVG. Puede alcanzar un techo cuando intenta mostrar varios miles de elementos al mismo tiempo. Su navegador puede comenzar a hincharse bajo el peso de todos esos elementos DOM.

¡Bueno, aquí viene HTML5 Canvas al rescate! Es mucho más rápido, por lo que puede resolver los problemas de inhalación de su navegador.

Pero puede encontrarse rápidamente intimidado. Porque D3 y Canvas funcionan un poco diferente de D3 y SVG, especialmente cuando se trata de dibujar y agregar interactividad.

Pero no temas, no es tan complicado. Cualquier experiencia que haya tenido con la creación de imágenes con D3 y SVG, o al acercarse a D3 con un renderizador diferente, lo ayudará enormemente.

Este tutorial se basa en los hombros de gigantes que ya han cubierto bien Canvas. Aprendí estos tres tutoriales de memoria y te recomiendo que también lo hagas:

  • Trabajando con D3.js y Canvas: cuándo y cómo de Irene Ros
  • Needles, Haystacks y la API de Canvas de Yannick Assogba
  • Aprendizajes de un adicto a D3.js al comenzar con Canvas de Nadieh Bremer

Entonces, ¿por qué seguir leyendo esto? Bueno, cuando quiero aprender algo nuevo, me ayuda mucho mirar el mismo tema desde ángulos ligeramente diferentes. Y este tutorial es un ángulo ligeramente diferente.

Además, este tutorial cubre los tres pasos clave: vincular datos, dibujar elementos y agregar interactividad, y hace todo esto de una vez, con un manual paso a paso agregado para configurarlo.

¿Qué construimos?

Una cuadrícula de bonitos colores

Una cuadrícula de (muchos) cuadrados. Sus colores no tienen ningún significado profundo, pero ¿no se ven bonitos? Lo importante es que puede actualizarlo (para cubrir los datos de enlace y actualización), que tiene muchos elementos (hasta 10,000 para que el lienzo pague) y que puede pasar el cursor sobre cada cuadrado para mostrar información específica del cuadrado (interactividad). Puedes jugar con él aquí en pantalla completa o aquí con todo el código

El modelo mental

Antes de sumergirnos, retrocedamos rápidamente y comprendamos conceptualmente lo que hacemos cuando creamos elementos con D3 para dibujarlos en la pantalla. Omita esto si solo quiere hacer cosas.

El primer paso cuando se usa D3 generalmente no implica dibujar, sino preparar todos los elementos que desea dibujar. Es un poco como construir algo de LEGO. Puede abrir la caja y comenzar a construir algo o puede mirar el manual primero y construirlo de acuerdo con el plan. El manual es su modelo mental, un plano o una receta de lo que desea construir.

Un modelo mental convertido en material (Mike, 2009 https://creativecommons.org/licenses/by/2.0/)

¿Cuál es el modelo de D3? Además de la gran cantidad de funciones y métodos útiles que calculan posiciones, modifican conjuntos de datos (los diseños) y generan funciones que dibujan, por ejemplo, rutas para nosotros, D3 tiene un modelo de cómo las vidas de los elementos deberían evolucionar en la pantalla . Tiene una cierta forma de pensar sobre el ciclo de vida de cada elemento.

Menos etéreamente, inyecta datos en un DOM aún inexistente, y D3 crea nuevos elementos de su elección según los datos que inyecta. Por lo general, un elemento por punto de datos. Si desea inyectar nuevos datos en el DOM, puede hacerlo y D3 identifica qué elementos deben crearse recientemente, qué elementos pueden permanecer y qué elementos deben empaquetarse y salir de la pantalla.

D3 se usa generalmente junto con SVG o, a veces, con elementos HTML. En este caso ortodoxo, puede ver los datos en el DOM cuando elige verlos a través de la consola, por ejemplo. Puede tomarlo, puede moverlo hacia arriba o hacia abajo en el DOM y, lo que es más importante, puede agregar interactividad a cada elemento que desee mostrar, por ejemplo, una información sobre herramientas.

Pero, a la baja, no puedes mostrar muchos elementos. ¿Por qué? Debido a que cuantos más elementos ingrese al DOM, más difícil será trabajar el navegador para mostrarlos todos. Deje que también se muevan y el navegador debe volver a calcularlos constantemente. Cuanto más agotado esté el navegador, menor será su velocidad de cuadros o FPS (cuadros por segundo), que mide cuántos cuadros puede pintar el navegador por segundo. Una velocidad de fotogramas de 60 es buena y permite una experiencia fluida siempre que no se pierdan fotogramas: una velocidad de fotogramas de cualquier cosa por debajo de 30 puede ser igual a un paseo entrecortado. Entonces, cuando desee mostrar más elementos, puede volver al lienzo.

¿Por qué lienzo? Canvas es un elemento HTML5 que viene con su propia API para pintar sobre él. Todos los elementos dibujados en el elemento del lienzo no se manifestarán en el DOM y ahorrarán mucho trabajo para el navegador. Se dibujan en modo inmediato. Esto significa que los elementos renderizados no se guardarán en el DOM, pero sus instrucciones los dibujarán directamente a un marco en particular. El DOM solo conoce el elemento de lienzo; todo en él está solo en la memoria. Si desea cambiar los elementos de su lienzo, debe volver a dibujar la escena para el siguiente fotograma.

El problema con esto es, por supuesto, que no puede comunicarse directamente con estos elementos no materiales que viven en la memoria. Tienes que encontrar una manera de hablarles indirectamente. Aquí es donde entra el modelo D3, así como elementos DOM personalizados o "virtuales". Lo que harás en principal es:

  1. Vincula tus datos a elementos DOM personalizados. No viven en el DOM, sino solo en la memoria (en un DOM "virtual") y describen el ciclo de vida de estos elementos de una manera D3 conocida.
  2. Usa el lienzo para dibujar estos elementos.
  3. Agregue interactividad con una técnica llamada "selección".

Vamos a hacerlo.

Los datos

Antes de comenzar a codificar, produzcamos algunos datos. Supongamos que quiere 5,000 puntos de datos. Así que creemos una matriz con 5,000 elementos, cada uno de los cuales es un objeto con un solo valor de propiedad que lleva el índice del elemento. Así es como lo crea con d3.range (). d3.range () es una función de utilidad D3, que crea una matriz basada en su argumento:

datos var = [];
d3.range (5000) .forEach (function (el) {
  data.push ({valor: el});
});

Así es como se ven los datos en la consola

Emociones!

El contenedor de lona y sus herramientas.

El elemento del lienzo es un elemento HTML. Conceptualmente es muy similar a cualquier elemento SVG-parent-element, que al menos generalmente agrego a un contenedor simple div como en:

Entonces, vamos a agregarlo a su contenedor con D3 como en ...

var ancho = 750, altura = 400;
var canvas = d3.select ('# contenedor')
  .append ('lienzo')
  .attr ('ancho', ancho)
  .attr ('altura', altura);
contexto var = canvas.node (). getContext ('2d');

También debe agregar el contexto, que es la caja de herramientas del lienzo. La variable de contexto es a partir de ahora el objeto que lleva todas las propiedades y métodos, los pinceles y los colores que necesita dibujar en el lienzo. Sin el contexto, el elemento de lienzo permanecería vacío y blanco. Eso es todo lo que necesita configurar: un lienzo y sus herramientas ...

Base de Stilfehler - Trabajo propio, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=5899171; Lego azul por David Lofink, 2008 https://creativecommons.org/licenses/by/2.0/

El HTML

…es simple. La estructura HTML principal de su sitio será:


Rejillas de colores



... toma números entre 1 y 10k

La estructura Javascript

En un nivel superior solo necesitas 2 funciones:

enlace de datos (datos) {
  // Vincula datos a elementos personalizados.
}
dibujar() {
  // Dibuja los elementos en el lienzo.
}

Bastante sencillo hasta ahora.

Atar los elementos

Para vincular datos a los elementos, primero cree un elemento base para todos sus elementos personalizados que producirá y dibujará. Si conoce bien D3, piense en ello como un reemplazo del elemento SVG:

var customBase = document.createElement ('personalizado');
var personalizado = d3.select (customBase);
// Este es su reemplazo de SVG y el padre de todos los demás elementos

Luego agrega algunas configuraciones para su cuadrícula. En resumen, esta configuración le permite dibujar una cuadrícula de cuadrados. 100 cuadrados construyen un "paquete" y hay un salto de línea después de 10 paquetes (o después de 1,000 cuadrados). Puede ajustar esto para diferentes "parcelación" de los cuadrados o diferentes saltos de línea. O simplemente no te preocupes por eso. Sugiero lo último ...

// Configuración para una cuadrícula con 10 celdas seguidas,
// 100 celdas en un bloque y 1000 celdas en una fila.
var groupSpacing = 4;
var cellSpacing = 2;
var offsetTop = altura / 5;
var cellSize = Math.floor ((ancho - 11 * groupSpacing) / 100) - cellSpacing;

Ahora comencemos la misión de enlace de datos. Primero saquemos las necesidades y creemos una escala de colores que aplicará a sus cuadrados un poco más tarde.

función databind (datos) {
// Obtenga una escala para los colores, no es esencial pero sí agradable.
colourScale = d3.scaleSequential (d3.interpolateSpectral)
                .domain (d3.extent (datos, función (d) {return d;}));

Ahora vamos a unir sus datos al "reemplazo-SVG" que llamó personalizado anteriormente y agregar elementos personalizados aún no existentes con la clase .rect

var join = custom.selectAll ('custom.rect')
  .data (datos);

Ingresa los elementos personalizados (recuerde que nada ingresa al DOM, todo esto está en la memoria).

var enterSel = join.enter ()
  .append ('personalizado')
  .attr ('clase', 'rect')
  .attr ("x", función (d, i) {
    var x0 = Math.floor (i / 100)% 10, x1 = Math.floor (i% 10);
    return groupSpacing * x0 + (cellSpacing + cellSize) * (x1 + x0 * 10); })
  .attr ("y", función (d, i) {
  var y0 = Math.floor (i / 1000), y1 = Math.floor (i% 100/10);
  return groupSpacing * y0 + (cellSpacing + cellSize) * (y1 + y0 * 10); })
  .attr ('ancho', 0)
  .attr ('altura', 0);

Cuando un elemento ingresa a su modelo, simplemente le da una posición x e a, así como un ancho y una altura de 0, que cambiará en la próxima selección de actualización ...

Combina la selección de ingreso en la selección de actualización y define todos los atributos para la actualización e ingreso de selección. Esto incluye un valor de ancho y alto, así como un color de la escala de colores que construyó anteriormente:

unirse
  .merge (enterSel)
  .transición()
  .attr ('ancho', cellSize)
  .attr ('altura', cellSize)
  .attr ('fillStyle', función (d) {return colourScale (d);});

Dos cosas notables sobre esta última línea. Cuando trabajas con SVG, esta línea sería

.style ('color', función (d) {return colourScale (d);})

Pero con el lienzo usas .attr (). ¿Por qué? Su interés principal aquí es encontrar una forma sencilla de transferir información específica de elementos. Aquí desea transferir una cadena de color desde databind () a la función draw (). Utiliza el elemento simplemente como un recipiente para transportar sus datos al lugar donde se representan en el lienzo.

Esa es una distinción muy importante: cuando se trabaja con SVG o HTML, puede vincular datos a elementos y dibujar o aplicar estilos a los elementos en un solo paso. En lienzo necesitas dos pasos. Primero vincula los datos y luego los dibuja. No puede diseñar los elementos mientras se vincula. Solo existen en la memoria y el lienzo no se puede diseñar a través de las propiedades de estilo CSS, que es exactamente a lo que se accede al usar .style ().

Al principio, esto puede parecer limitante ya que puede hacer menos en un solo paso, pero conceptualmente es casi más limpio y también le da algo de libertad. .attr () nos permite enviar cualquier par clave-valor en el viaje. Podría usar otros métodos, como la propiedad HTML .dataset, por ejemplo, pero .attr () funcionará bien.

Tenga en cuenta que no decimos color sino fillStyle. Para ser honesto, podría usar color o podría usar chooChooTrain aquí. Solo necesitará recordar esto cuando obtenga la información más tarde durante el dibujo. Sin embargo, como el lienzo utiliza una propiedad llamada fillStyle para dar estilo a los elementos, parece más apropiado en este caso.

Finalmente, también define la selección de salida, decidiendo qué pasará con los elementos que salen.

var exitSel = join.exit ()
  .transición()
  .attr ('ancho', 0)
  .attr ('altura', 0)
  .retirar();

¡Eso es! Puede cerrar su función databind () y continuar ...

} // databind ()

Esto no da mucho miedo al venir de D3, ya que es casi exactamente lo mismo. Ahora ha creado con éxito su modelo de datos, la forma en que la aplicación considerará los datos. Cada elemento obtendrá las propiedades que necesita dibujar a través de las funciones .attr () y a cada elemento se le asignará un estado de ciclo de vida dependiendo de los datos inyectados. Nuestro modelo estándar D3.

Dibujando los elementos

Por Kristina Alexanderson, https://creativecommons.org/licenses/by-nc-nd/2.0/ 2011

Ahora necesita escribir la función de dibujar para obtener los elementos en la pantalla. Observemos aquí que todavía no ha pasado nada. Todavía no ha llamado a databind () porque primero necesita encontrar una forma de dibujarlo en el lienzo. Así que aquí vamos ... La función draw () no necesita tomar ningún argumento en este caso:

función draw () {

Como se mencionó fugazmente anteriormente, debe encargarse de limpiar el lienzo cada vez que dibuja de nuevo. El DOM es material, ya que cuando dibujas un elemento rect sobre él y cambias su valor x, se moverá en la dirección x y el DOM se encargará de este movimiento (o la pintura) automáticamente.

Si mueve un rect de x = 0 a x = 1 en un momento determinado (después de presionar un botón, por ejemplo), el navegador moverá el rect de 0 a 1 dentro de una marca o pintura de marco (que tiene aproximadamente 16 ms de largo ) Si lo mueve de 0 a 10, lo hará en un tiempo dependiendo de la duración en la que solicitó que ocurriera esta transición, tal vez 1 píxel por marca tal vez 8 píxeles por marca (para más información, lea esta publicación de blog).

Pero le dirá al píxel en 0 que el rect ha desaparecido y al píxel en 1 que hay un rect ahora. El lienzo no hace esto. Debe decirle al lienzo qué pintar, y si pinta algo nuevo, debe decirle que elimine la pintura anterior.

Comencemos limpiando todo lo que pueda estar en el lienzo antes de dibujar. Así es cómo:

context.clearRect (0, 0, ancho, alto); // Borrar el lienzo.

Simple.

Ahora tu…

  1. ... obtener todos los elementos para
  2. recorrer todos los elementos y
  3. tome la información que ha almacenado en la función databind () para dibujar el elemento:
// Dibuja cada elemento personalizado individual con sus propiedades.
elementos var = custom.selectAll ('custom.rect');
// Toma todos los elementos a los que enlazaste datos en la función databind ().
elements.each (function (d, i) {// Para cada elemento virtual / personalizado ...
  nodo var = d3.select (esto);
  // Este es cada elemento individual en el ciclo.
  
  context.fillStyle = node.attr ('fillStyle');
  // Aquí recuperas el color del nodo individual en memoria y configuras el fillStyle para la pintura del lienzo
  context.fillRect (node.attr ('x'), node.attr ('y'), node.attr ('ancho'), node.attr ('alto'));
  // Aquí recuperas la posición del nodo y la aplicas a la función de contexto fillRect que llenará y pintará el cuadrado.
}); // Recorre cada elemento.

¡Y eso es! Puedes cerrar la función draw ()

} // dibujar()

Cuando comencé con el lienzo después de un tiempo de querer sumergirme en él, esta simplicidad realmente me animó.

Sin embargo, todavía no ha sucedido nada en el navegador. Tenemos las herramientas en la función databind () y draw (), pero todavía no se ha dibujado nada. ¿Cómo haces esto? Si solo desea dibujar una imagen o imagen estática, simplemente llame:

enlace de datos (datos);
dibujar();

Esto uniría los datos a los elementos personalizados, que vivirían en la memoria y luego los dibujarían, ¡una vez!

Pero tienes transiciones. Recuerde más arriba: cuando escribió la función databind (), cambió el ancho y el alto de la celda de 0 a su tamaño, así como el color de negro (predeterminado) al color del elemento respectivo. Una transición D3 predeterminada dura 250 milisegundos, por lo que debe volver a dibujar los cuadrados muchas veces en estos 250 ms para obtener una transición sin problemas. ¿Cómo haces esto?

De nuevo es simple. Simplemente llame a databind (data) para crear nuestros elementos personalizados antes de llamar repetidamente a draw () durante el tiempo que se requiera la transición para ejecutarse. Entonces en nuestro caso al menos 250 ms. Puede usar setInterval () para esto, pero realmente deberíamos usar requestAnimationFrame () para ser lo más eficiente posible (para más información, lea esto). Hay algunas formas de usarlo, pero manteniéndome dentro del espíritu D3, sugiero usar d3.timer () que implementa requestAnimationFrame () además de ser fácil de usar. Así que, aquí vamos:

// === Primera llamada === //
databind (d3.range (valor)); // Construye los elementos personalizados en la memoria.
var t = d3.timer (función (transcurrido) {
  dibujar();
  if (transcurrido> 300) t.stop ();
}); // Temporizador que ejecuta la función de dibujar repetidamente durante 300 ms.

d3.timer () llama a la devolución de llamada repetidamente hasta que transcurre (que es el tiempo transcurrido en milisegundos desde la instanciación) pasa de 300 y luego se detiene el temporizador. En estos 300 milisegundos, ejecuta el empate () en cada tic (aproximadamente cada 16 ms). draw () luego mira los atributos de cada elemento y los dibuja en consecuencia.

Así es como funciona una transición en lienzo. Llama a la función de dibujo justo después de la función de enlace muchas veces. Cualquiera que sea su modelo D3 configurado para la transición (posiciones, colores, tamaños), se volverá a dibujar muchas veces con pequeños cambios incrementales para cada dibujo

Tenga en cuenta que draw () debe venir justo después de la función databind (). No podría pedirle a la máquina que ejecute databind (), luego haga otra cosa por un segundo y luego llame a draw (). Debido a que después de 1 segundo, los estados de transición calculados por su función databind () ya han hecho la transición. Hecho, espolvoreado y olvidado.

¡Eso es! Ha vinculado datos a elementos personalizados y los ha dibujado al lienzo.

Deje que el usuario actualice el número de cuadrados

Para darle al usuario la oportunidad de repetir esta hazaña con un número personalizado de elementos (ok, semi-personalizado con un máximo de 10,000) agrega el siguiente oyente y controlador a su cuadro de entrada de texto:

// === Listeners / handlers === //
d3.select ('# entrada de texto'). on ('keydown', function () {
if (d3.event.keyCode === 13) {
// Solo haga algo si el usuario presiona return (código clave 13).
  if (+ this.value <1 || + this.value> 10000) {
  // Si el usuario baja a 1 o más de 10k ...
     
    d3.select ('# text-explicar'). classed ('alert', verdadero);
    // ... resalta la nota sobre el rango y el retorno.
    regreso;
  } más {
  // Si el usuario escribe un número razonable ...
    d3.select ('# text-explicar'). classed ('alerta', falso);
    // ... eliminar posibles colores de alerta de la nota ...
    valor = + este.valor; // ... establece el valor ...
    databind (d3.range (valor)); // ... y enlazar los datos.
    var t = d3.timer (función (transcurrido) {
      dibujar();
  
      if (transcurrido> 300) t.stop ();
    }); // Temporizador que ejecuta la función de dibujar repetidamente durante 300 ms.
  
  } // Si el usuario presiona regresar.
}); // Escucha / manejador de entrada de texto

Aquí está de nuevo, nuestra cuadrícula colorida de cuadros de lienzo, lista para ser actualizada y redibujada:

Interactividad

El mayor "dolor" con el lienzo en comparación con SVG o HTML es que no hay elementos materiales que vivan en el DOM. Si lo hubiera, podría registrar oyentes en los elementos y agregar controladores a los oyentes. Por ejemplo, puede activar un mouse-over en un elemento rect SVG y cada vez que se activa el oyente, puede hacer algo al rect. Como mostrar valores de datos almacenados con el rect en una información sobre herramientas.

Con el lienzo, debe encontrar otra forma de hacer que un evento se escuche en nuestros elementos del lienzo. Afortunadamente, hay una serie de personas inteligentes que pensaron en una forma indirecta pero lógica.

Entonces, ¿qué interactividad queremos? Como se dijo anteriormente, busquemos información sobre herramientas y supongamos que desea mostrar el índice del cuadrado en una información sobre herramientas tan pronto como pase el cursor sobre el elemento. No es muy emocionante, pero la clave es que puede acceder a los datos vinculados al elemento al pasar el mouse sobre él.

Cosecha

Hay algunos pasos involucrados (aunque todos lógicos). Pero en resumen, construirás dos lienzos para lograr esto. Un lienzo principal que produce nuestro lienzo visual y otro oculto (ya que no podemos verlo) que produce el mismo visual. La clave aquí es que todos los elementos en el segundo lienzo estarán exactamente en la misma posición en relación con el origen del lienzo en comparación con el primer lienzo. Entonces el cuadrado 1 comienza en 0,0 en el lienzo principal, así como en el lienzo oculto. Square 2 comienza en 8,0 en el lienzo principal, así como en el lienzo oculto, etc.

Solo hay una diferencia importante. Cada elemento en el lienzo oculto obtendrá un color único. Crearemos un objeto (o más bien una matriz o mapa asociativo por brevedad) que vincule cada color único con los datos de cada elemento.

¿Por qué? Porque a continuación adjuntamos un detector de movimiento del mouse al lienzo principal para recuperar un flujo de posiciones del mouse. En cada posición del mouse, podemos usar un método propio de lienzo para "elegir" el color en esta posición exacta. Luego solo buscamos el color en nuestra matriz asociativa y tenemos los datos. Y estamos volando ...

Por Kenny Louie, https://creativecommons.org/licenses/by/2.0/ 2010

Podrías decir "bueno, mis cuadrados ya tienen un color único, ¿puedo usar esos?" Y de hecho, podrías usarlos. Sin embargo, su interactividad se iría por la ventana tan pronto como decida quitar los cuadrados de los colores. Por lo tanto, debe asegurarse de tener siempre un lienzo, el lienzo oculto, que tenga un conjunto garantizado de colores únicos para los cuadrados.

Apliquemos esta técnica paso a paso. El código que ha creado hasta ahora puede permanecer tal como está; solo tiene que agregarlo a medida que avanza.

1. Prepare el lienzo oculto

Primero, creemos el lienzo oculto que albergará nuestro visual con un color único por cuadrado.

1.1 Cree un elemento de lienzo oculto y establezca su CSS en {display: none; }.

// Cambia el nombre del lienzo principal y agrégale una clase 'mainCanvas'.
var mainCanvas = d3.select ('# contenedor')
  .append ('lienzo')
  .classed ('mainCanvas', verdadero)
  .attr ('ancho', ancho) .attr ('alto', alto);
 
// nuevo -----------------------------------
// Agrega el lienzo oculto y dale la clase 'hiddenCanvas'.
var hiddenCanvas = d3.select ('# contenedor')
  .append ('lienzo')
  .classed ('hiddenCanvas', verdadero)
  .attr ('ancho', ancho)
  .attr ('altura', altura);

De hecho, no estableceré el lienzo como oculto en este ejemplo para mostrar lo que está sucediendo. Pero para hacerlo, solo agregue .hiddenCanvas {display: none; } a tu CSS y el hecho está hecho.

1.2 Construya la variable de contexto en la función draw () y pase dos argumentos a la función: el lienzo así como un booleano llamado 'oculto' que determina qué lienzo construimos (oculto = verdadero || falso) como en:

función dibujar (lienzo, oculto) {

1.3 Ahora necesita adaptar todas las funciones de dibujo para incluir los dos nuevos argumentos de draw (). Entonces, a partir de ahora, no solo llamará a draw () sino a draw (mainCanvas, false) o draw (hiddenCanvas, true)

2. Aplicar colores únicos a los elementos ocultos y asignarlos

Aquí, querido lector, viene la parte clave de nuestra operación, el motor de nuestro camión, la especia en nuestra sopa.

Por Andrew Becraft, 2007 https://creativecommons.org/licenses/by-nc-sa/2.0/

2.1 Incluye una función para generar un nuevo color único cada vez que se llama (a través de Stack Overflow)

// Función para crear nuevos colores para la selección.
var nextCol = 1;
función genColor () {
  
  var ret = [];
  if (nextCol <16777215) {
    
    ret.push (nextCol & 0xff); // R
    ret.push ((nextCol & 0xff00) >> 8); // G
    ret.push ((nextCol & 0xff0000) >> 16); // B
    nextCol + = 1;
  
  }
var col = "rgb (" + ret.join (',') + ")";
volver col;
}

genColour () produce una cadena que define el color en la forma rgb (0,0,0). Cada vez que se llama incrementa el valor R en uno. Una vez que alcanza 255, incrementa el valor de G en 1 y restablece el valor de R a 0. Una vez que alcanza r (255,255,0) incrementa el valor de B en 1 restableciendo R y G a 0 y así sucesivamente.

Entonces, en total, puede tener 256 * 256 * 256 = 16.777.216 elementos para retener un color único. Sin embargo, puedo asegurarle que su navegador morirá de antemano. Incluso con lienzo (tutorial de webGL a seguir).

2.2 Cree el objeto de mapa que hará un seguimiento de qué elemento personalizado tiene qué color único:

var colourToNode = {}; // Mapa para rastrear el color de los nodos.

Puede agregar la función genColour () donde quiera en su script, siempre que esté fuera del alcance de la función databind () y draw (). Pero tenga en cuenta que su variable de mapa debe crearse antes y más allá del alcance de la función databind ().

2.3 Agregue un color único a cada elemento personalizado como, por ejemplo, .attr ('fillStyleHidden') y
2.4 construir el objeto del mapa durante la creación del elemento

Aquí usará su genColour () de "color-canon" en nuestra función databind () cuando asigne el fillStyle a nuestros elementos. Como también tiene acceso a cada punto de datos mientras está vinculado a cada elemento, puede unir el color y los datos en su mapa colourToNode.

unirse
  .merge (enterSel)
  .transición()
  .attr ('ancho', cellSize)
  .attr ('altura', cellSize)
  .attr ('fillStyle', función (d) {
    return colorScale (d.value);
  });
  // nuevo ----------------------------------------------- ------
  
  .attr ('fillStyleHidden', función (d) {
    if (! d.hiddenCol) {
      d.hiddenCol = genColor ();
      colourToNode [d.hiddenCol] = d;
    }
    // Aquí usted (1) agrega un color único como propiedad a cada elemento
    // y (2) asignan el color al nodo en colourToNode-map.
    return d.hiddenCol;
});

2.5 Ahora puede colorear los elementos de acuerdo con el lienzo que representa la función draw (). Agrega un condicional en el fillStyle en la función draw () aplicando los colores para nuestro visual al lienzo principal y los colores únicos al lienzo oculto. Es una frase simple:

context.fillStyle = oculto? node.attr ('fillStyleHidden'): node.attr ('fillStyle');
// El color del nodo depende del lienzo que dibujes.

El lienzo principal todavía se ve igual, por supuesto:

Finalmente, agreguemos algo de interactividad y comencemos dibujando el lienzo oculto cada vez que muevamos el mouse a nuestro lienzo principal.

3. Recoge los colores con el mouse

3.1 Primero, simplemente registre un oyente en el lienzo principal, escuchando los eventos de movimiento del mouse.

d3.select ('. mainCanvas'). on ('mousemove', function () {
});

¿Por qué mover el mouse? Como no puede registrar oyentes con cuadrados individuales, pero tiene que usar todo el lienzo, no podrá trabajar con eventos de desplazamiento del mouse o de salida, ya que solo se activarán al ingresar el lienzo, no los elementos. Para obtener la posición del mouse en su lienzo, puede mover el mouse o hacer clic / mousedown.

d3.select ('. mainCanvas'). on ('mousemove', function () {
  dibujar (lienzo oculto, verdadero); // Dibuja el lienzo oculto.
});

De esta manera, lo primero que activa nuestro usuario al pasar el mouse sobre el lienzo principal es crear, sin saberlo, el lienzo oculto. Como se dijo, en producción este lienzo estaría oculto, pero para nuestros propósitos educativos queremos verlo y, de hecho, desencadenar el lienzo oculto para que se dibuje cuando el mouse se mueva sobre el lienzo principal de esta manera:

Los colores en el lienzo principal varían de negro a rojo, de rgb (0,0,0) a rgb (255,0,0) y luego parece que se repite el mismo rango de negro a rojo. Sin embargo, ahora el color varía de un negro ligeramente más verde, precisamente de rgb (0,1,0) a rgb (255,1,0):

Acercándonos a los primeros doscientos cuadrados, aquí están los colores del primero, el 256 y el 257:

3.3 Como nuestro lienzo oculto es estructuralmente una copia al carbón de nuestro lienzo principal, todos los elementos del lienzo oculto estarán en la misma posición que los elementos de nuestro lienzo principal. Por lo tanto, ahora puede usar las posiciones x e y del mouse que está recopilando del oyente en el lienzo principal para establecer la misma ubicación en el lienzo oculto. De vuelta en el oyente, agrega:

d3.select ('. mainCanvas'). on ('mousemove', function () {
  
  // Dibuja el lienzo oculto.
  dibujar (lienzo oculto, verdadero);
  // Obtener posiciones del mouse desde el lienzo principal.
  var mouseX = d3.event.layerX || d3.event.offsetX;
  var mouseY = d3.event.layerY || d3.event.offsetY; });

Tenga en cuenta que aquí tomamos las propiedades event.layerX y event.layerY que devuelven la posición del mouse, incluido el desplazamiento. Esto puede romperse, así que use offsetX como alternativa (o simplemente use offsetX).

3.4 La selección: Canvas permite en gran medida el acceso a los datos de píxeles sobre los que el mouse se desplaza con la función getImageData () y su propiedad .data. En plena floración esto se verá así:

getImageData (posX, posY, 1, 1) .data.

Devolverá una matriz con cuatro números: el R, el G, el B y el valor alfa. A medida que construyó diligentemente el mapa colourToNode asignando los datos del elemento a cada uno de sus colores ocultos, ahora puede acceder a los datos de este elemento simplemente buscando el color en el mapa.

d3.select ('. mainCanvas'). on ('mousemove', function () {
  // Dibuja el lienzo oculto.
  dibujar (lienzo oculto, verdadero);
  // Obtener posiciones del mouse desde el lienzo principal.
  var mouseX = d3.event.layerX || d3.event.offsetX;
  var mouseY = d3.event.layerY || d3.event.offsetY;
// nuevo -----------------------------------------------
  // Obtenga la caja de herramientas para el lienzo oculto.
  var hiddenCtx = hiddenCanvas.node (). getContext ('2d');
  // Elige el color de la posición del mouse.
  var col = hiddenCtx.getImageData (mouseX, mouseY, 1, 1) .data;
  // Luego, clasifique los valores de forma que nuestro objeto de mapa pueda leerlos.
  var colKey = 'rgb (' + col [0] + ',' + col [1] + ',' + col [2] + ')';
  // ¡Obtén los datos de nuestro mapa!
  var nodeData = colourToNode [colKey];
  console.log (nodeData);
});

Y, de hecho, registrar el nodeData en la consola devuelve un objeto cada vez que pasa el cursor sobre un cuadrado:

Los datos por nodo ahora muestran el valor que constituye los datos originales, así como la clave hiddenCol que muestra el color de este nodo para el lienzo oculto:

3.5 Finalmente, y eso es una formalidad, agrega la información sobre herramientas

d3.select ('. mainCanvas'). on ('mousemove', function () {
  // Dibuja el lienzo oculto.
  dibujar (lienzo oculto, verdadero);
  // Obtener posiciones del mouse desde el lienzo principal.
  var mouseX = d3.event.layerX || d3.event.offsetX;
  var mouseY = d3.event.layerY || d3.event.offsetY;
  // Obtenga la caja de herramientas para el lienzo oculto.
  var hiddenCtx = hiddenCanvas.node (). getContext ('2d');
  // Elige el color de la posición del mouse.
  var col = hiddenCtx.getImageData (mouseX, mouseY, 1, 1) .data;
  // Luego, clasifique los valores de forma que nuestro objeto de mapa pueda leerlos.
  var colKey = 'rgb (' + col [0] + ',' + col [1] + ',' + col [2] + ')';
  // ¡Obtén los datos de nuestro mapa!
  var nodeData = colourToNode [colKey];
  
  console.log (nodeData);
  // nuevo -----------------------------------------------
  if (nodeData) {
  // Muestra la información sobre herramientas solo cuando se encuentra nodeData encontrado por el mouse
    d3.select ('# información sobre herramientas')
      .style ('opacidad', 0.8)
      .style ('top', d3.event.pageY + 5 + 'px')
      .style ('left', d3.event.pageX + 5 + 'px')
      .html (nodeData.value);
  } más {
  // Ocultar la información sobre herramientas cuando el mouse no encuentra nodeData.
  
    d3.select ('# tooltip'). style ('opacity', 0);
  
  }
}); // escucha / manejador de lienzo

¡Eso es! Ha visualizado una gran cantidad de elementos en el lienzo, más de lo que hubiera podido disfrutar sin problemas con SVG. Todavía usó el modelo de ciclo de vida de D3 y agregó cierta interactividad para acceder a los datos adjuntos a cada elemento. Estos tres pasos deberían permitirle hacer casi cualquier cosa o al menos más de lo que está acostumbrado cuando trabaja con D3 y SVG.

Hay un manual paso a paso desde cero hasta el D3 / lienzo interactivo en mi blog que permite enlaces a páginas internas. De esta manera, puede ver todo el proceso en una vista y hacer clic en él con facilidad:

Haga clic para acceder al manual.

... y aquí está el código completo de nuevo.

Espero que hayas disfrutado leyendo esto y por favor saluda y / o ...

lars verspohl www.datamake.io @lars_vers https://www.linkedin.com/in/larsverspohl

... siempre está agradecido por un like o un seguimiento que puede devolver.