svg d3.js zoom force-layout

svg - enfoque semántico de la fuerza dirigida gráfico en d3



d3.js zoom (2)

Se han mostrado muchos casos de zoom geométrico orientado a la fuerza por SVG Geometric Zooming .

En el zoom geométrico, solo necesito agregar un atributo de transformación en la función de zoom. Sin embargo, en el zoom semántico, si solo agrego un atributo de transformación en el nodo, los enlaces no se conectarán al nodo. Entonces, me pregunto si existe una solución para el zoom geométrico para el gráfico dirigido a la fuerza en d3.

Aquí está mi ejemplo con zoom geométrico siguiendo el caso anterior. Tengo dos problemas:

  1. Cuando alejo, arrastre todo el gráfico, el gráfico desaparecerá de forma extraña.
  2. Usando la misma función de redibujado

function zoom() { vis.attr("transform", transform); } function transform(d){ return "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"; }

Esto solo actualiza el atributo "transformar" de un elemento svg. Pero, ¿cómo hacer que la función cambie la posición del nodo?

Pero lo que quiero hacer es un zoom semántico . He intentado modificar el zoom y la función de transformación, pero no estoy seguro de la forma correcta de hacerlo. Esto es lo que intento. Funciones que he cambiado:

function zoom() { node.call(transform); // update link position update(); } function transform(d){ // change node x, y position, not sure what function to put here. }


Tienes que transformar el nodo y volver a dibujar las rutas.

La idea del "zoom semántico" es que cambias la escala de tu diseño pero no el tamaño de los elementos individuales.

Si configuró el comportamiento del zoom como en el ejemplo vinculado, automáticamente actualiza las escalas xey. A continuación, restablece la posición de los nodos en función de estas escalas, y también puede volver a establecer la posición y la forma de los enlaces.

Si sus enlaces son líneas rectas, vuelva a establecer las posiciones x1, y1, x2 e y2 utilizando las escalas xey actualizadas. Si sus enlaces son rutas creadas con d3.svg.diagonal y las escalas xey, vuelva a establecer el atributo "d" con la misma función.

Si necesita instrucciones más específicas, tendrá que publicar su código.


Traté de encontrar un buen tutorial para vincular, pero no pude encontrar nada que realmente cubriera todos los problemas, así que voy a escribirlo paso a paso yo mismo.

Primero, necesitas entender claramente lo que estás tratando de lograr. Esto es diferente para los dos tipos de acercamiento. Realmente no me gusta la terminología que ha presentado Mike Bostock, (no es del todo coherente con los usos de los términos que no son d3), pero también podríamos seguir con ella para ser coherentes con otros ejemplos de d3.

En "acercamiento geométrico" , está haciendo zoom en toda la imagen. Los círculos y las líneas se hacen más grandes y más alejados. SVG tiene una manera fácil de lograr esto a través del atributo "transformar". Cuando configura transform="scale(2)" en un elemento SVG, se dibuja como si todo fuera el doble de grande. Para un círculo, su radio se dibuja dos veces grande, y sus posiciones cx y cy se plotean dos veces la distancia desde el punto (0,0). Todo el sistema de coordenadas cambia, por lo que una unidad ahora equivale a dos píxeles en la pantalla, no uno.

Del mismo modo, transform="translate(-50,100)" cambia todo el sistema de coordenadas, de modo que el punto (0,0) del sistema de coordenadas se mueve 50 unidades hacia la izquierda y 100 unidades hacia abajo desde la esquina superior izquierda (que es el punto de origen predeterminado).

Si ambos traducen y escalan un elemento SVG, el orden es importante. Si la traducción es anterior a la escala, entonces la traducción está en las unidades originales. Si la traducción está después de la escala, entonces la traducción está en las unidades escaladas.

El método d3.zoom.behavior() crea una función que escucha eventos de arrastre y arrastre del mouse, así como también eventos de pantalla táctil asociados con el zoom. Convierte estos eventos de usuario en un evento de "zoom" personalizado.

El evento de zoom recibe un factor de escala (un solo número) y un factor de conversión (una matriz de dos números), que el objeto de comportamiento calcula a partir de los movimientos del usuario. Lo que hagas con estos números depende de ti; ellos no cambian nada directamente . (Con la excepción de cuando se adjunta una escala a la función de comportamiento del zoom, como se describe más adelante).

Para el zoom geométrico, lo que suele hacer es establecer un atributo de transformación de escala y traducción en un elemento <g> que contiene el contenido que desea ampliar. Este ejemplo implementa ese método de zoom geométrico en un SVG simple que consiste en líneas de cuadrícula colocadas uniformemente :
http://jsfiddle.net/LYuta/2/

El código de zoom es simple:

function zoom() { console.log("zoom", d3.event.translate, d3.event.scale); vis.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")" ); }

El zoom se logra al configurar el atributo de transformación en "vis", que es una selección d3 que contiene un elemento <g> que contiene todo el contenido que queremos ampliar. Los factores de escala y trasladar vienen directamente del evento de zoom que creó el comportamiento d3.

El resultado es que todo se vuelve más grande o más pequeño: el ancho de las líneas de la cuadrícula, así como el espacio entre ellas. Las líneas aún tienen stroke-width:1.5; pero la definición de lo que 1.5 es igual en la pantalla ha cambiado para ellos y para cualquier otra cosa dentro del elemento <g> transformado.

Para cada evento de zoom, los factores de escala y de traducción también se registran en la consola. Al observar eso, notarás que si te alejas, la escala estará entre 0 y 1; si lo acerca, será mayor que 1. Si desplaza (arrastra para mover) el gráfico, la escala no cambiará en absoluto. Los números de traducción, sin embargo, cambian tanto en panorámica como en zoom. Esto se debe a que la traducción representa la posición del punto (0,0) en el gráfico relativo a la posición de la esquina superior izquierda de SVG. Cuando haces zoom, la distancia entre (0,0) y cualquier otro punto en el gráfico cambia. Por lo tanto, para mantener el contenido con el mouse o con el dedo en la misma posición en la pantalla, la posición del punto (0,0) debe moverse.

Hay varias otras cosas a las que debes prestarle atención en ese ejemplo:

  • .scaleExtent([min,max]) el objeto de comportamiento del zoom con el .scaleExtent([min,max]) . Esto establece un límite en los valores de escala que usará el comportamiento en el evento de zoom, sin importar cuánto gire el usuario la rueda.

  • La transformación está en un elemento <g> , no en el <svg> mismo. Esto se debe a que el elemento SVG como un todo se trata como un elemento HTML y tiene una sintaxis y propiedades de transformación diferentes.

  • El comportamiento del zoom se adjunta a un elemento <g> diferente , que contiene el <g> principal y un rectángulo de fondo. El rectángulo de fondo está allí para que se puedan observar los eventos mouse y touch incluso si el mouse o touch no está bien en una línea. El elemento <g> sí no tiene ningún alto o ancho, por lo que no puede responder a los eventos del usuario directamente, solo recibe eventos de sus hijos. He dejado el rectángulo negro para que pueda decir dónde está, pero puede configurar su estilo para fill:none; siempre que también lo configure en pointer-events:all; . El rectángulo no puede estar dentro de la <g> que se transforma, porque entonces el área que responde a los eventos de zoom también se reduciría cuando alejase, y posiblemente fuera de la vista desde el borde del SVG.

  • Puede omitir el rectángulo y el segundo elemento <g> adjuntando el comportamiento del zoom directamente al objeto SVG, como en esta versión del violín . Sin embargo, a menudo no desea que los eventos en toda el área de SVG activen el zoom, por lo que es bueno saber cómo y por qué usar la opción de rectángulo de fondo.

Este es el mismo método de acercamiento geométrico, aplicado a una versión simplificada de su diseño de fuerza :
http://jsfiddle.net/cSn6w/5/

Reduje el número de nodos y enlaces, y eliminé el comportamiento de arrastre de nodo y el comportamiento de expandir / colapsar de nodo, para que pueda centrarse en el zoom. También modifiqué el parámetro "fricción" para que el gráfico deje de moverse; acércate el zoom mientras todavía está en movimiento, y verás que todo seguirá moviéndose como antes.

El "acercamiento geométrico" de la imagen es bastante sencillo, se puede implementar con muy poco código y produce cambios rápidos y suaves por parte del navegador. Sin embargo, a menudo la razón por la que desea acercarse a un gráfico es porque los puntos de datos están muy juntos y superpuestos. En ese caso, simplemente hacer que todo sea más grande no ayuda. Desea estirar los elementos en un espacio mayor mientras mantiene los puntos individuales del mismo tamaño. Ahí es donde entra en acción el "zoom semántico".

El "zoom semántico" de un gráfico, en el sentido en que Mike Bostock usa el término , es para ampliar el diseño del gráfico sin hacer un acercamiento a los elementos individuales. (Tenga en cuenta que hay otras interpretaciones de "acercamiento semántico" para otros contextos).

Esto se hace cambiando la forma en que se calcula la posición de los elementos, así como la longitud de cualquier línea o camino que conecte objetos, sin cambiar el sistema de coordenadas subyacente que define qué tan grande es un píxel para establecer el ancho de línea o el tamaño de formas o texto.

Puede hacer estos cálculos usted mismo, utilizando los valores de trasladar y escalar para ubicar los objetos en función de estas fórmulas:

zoomedPositionX = d3.event.translate[0] + d3.event.scale * dataPositionX zoomedPositionY = d3.event.translate[1] + d3.event.scale * dataPositionY

He utilizado ese enfoque para implementar el zoom semántico en esta versión del ejemplo de líneas de división :
http://jsfiddle.net/LYuta/4/

Para las líneas verticales, originalmente se colocaron así

vLines.attr("x1", function(d){return d;}) .attr("y1", 0) .attr("x2", function(d){return d;}) .attr("y2", h);

En la función de zoom, eso se cambia a

vLines.attr("x1", function(d){ return d3.event.translate[0] + d*d3.event.scale; }) .attr("y1", d3.event.translate[1]) .attr("x2", function(d){ return d3.event.translate[0] + d*d3.event.scale; }) .attr("y2", d3.event.translate[1] + h*d3.event.scale);

Las líneas horizontales se cambian de manera similar. ¿El resultado? La posición y la longitud de las líneas cambian en el zoom, sin que las líneas se vuelvan más gruesas o más delgadas.

Se pone un poco complicado cuando tratamos de hacer lo mismo para el diseño de la fuerza. Esto se debe a que los objetos en el gráfico de diseño de fuerza también se están reubicando después de cada evento "tic". Para mantenerlos posicionados en los lugares correctos para el zoom, el método de posicionamiento de ticks tendrá que usar las fórmulas de posición ampliada. Lo que significa que:

  1. La escala y la traducción deben guardarse en una variable a la que se puede acceder mediante la función tic; y,
  2. Es necesario que haya valores predeterminados de escala y de traducción para la función de marca que se utilizará si el usuario no ha ampliado aún nada.

La escala predeterminada será 1, y la traducción predeterminada será [0,0], que representa la escala normal y no tiene traducción.

Esto es lo que parece con el zoom semántico en el diseño de fuerza simplificado :
http://jsfiddle.net/cSn6w/6/

La función de zoom es ahora

function zoom() { console.log("zoom", d3.event.translate, d3.event.scale); scaleFactor = d3.event.scale; translation = d3.event.translate; tick(); //update positions }

Establece las variables scaleFactor y translation, luego llama a la función tick. La función tictac hace todo el posicionamiento: en la inicialización, después de los eventos tick de diseño forzado y después de los eventos de zoom. Parece que

function tick() { linkLines.attr("x1", function (d) { return translation[0] + scaleFactor*d.source.x; }) .attr("y1", function (d) { return translation[1] + scaleFactor*d.source.y; }) .attr("x2", function (d) { return translation[0] + scaleFactor*d.target.x; }) .attr("y2", function (d) { return translation[1] + scaleFactor*d.target.y; }); nodeCircles.attr("cx", function (d) { return translation[0] + scaleFactor*d.x; }) .attr("cy", function (d) { return translation[1] + scaleFactor*d.y; }); }

Cada valor de posición para los círculos y los enlaces se ajusta mediante la traducción y el factor de escala. Si esto tiene sentido para usted, esto debería ser suficiente para su proyecto y no necesita usar escalas. Solo asegúrese de usar siempre esta fórmula para convertir las coordenadas de datos (dx y dy) y las coordenadas de visualización (cx, cy, x1, x2, etc.) utilizadas para colocar los objetos.

Donde esto se complica es si necesita hacer la conversión inversa de las coordenadas de visualización a las coordenadas de datos. Debe hacer esto si desea que el usuario pueda arrastrar nodos individuales; necesita configurar las coordenadas de los datos en función de la posición de la pantalla del nodo arrastrado. (Tenga en cuenta que esto no funcionaba correctamente en ninguno de sus ejemplos).

Para el zoom geométrico , la conversión entre la posición de la pantalla y la posición de los datos puede ser bajada con d3.mouse() . El uso de d3.mouse(SVGElement) calcula la posición del mouse en el sistema de coordenadas utilizado por ese elemento SVGElement . Entonces, si pasamos el elemento que representa la visualización transformada, devuelve coordenadas que se pueden usar directamente para establecer la posición de los objetos.

El diseño de fuerza de zoom geométrico arrastrable se ve así:
http://jsfiddle.net/cSn6w/7/

La función de arrastre es:

function dragged(d){ if (d.fixed) return; //root is fixed //get mouse coordinates relative to the visualization //coordinate system: var mouse = d3.mouse(vis.node()); d.x = mouse[0]; d.y = mouse[1]; tick();//re-position this node and any links }

Para el zoom semántico , sin embargo, las coordenadas SVG devueltas por d3.mouse() ya no se corresponden directamente con las coordenadas de datos. Debe tener en cuenta la escala y la traducción. Usted hace esto reorganizando las fórmulas dadas arriba:

zoomedPositionX = d3.event.translate[0] + d3.event.scale * dataPositionX zoomedPositionY = d3.event.translate[1] + d3.event.scale * dataPositionY

se convierte

dataPositionX = (zoomedPositionX - d3.event.translate[0]) / d3.event.scale dataPositionY = (zoomedPositionY - d3.event.translate[1]) / d3.event.scale

La función de arrastre para el ejemplo de zoom semántico es por lo tanto

function dragged(d){ if (d.fixed) return; //root is fixed //get mouse coordinates relative to the visualization //coordinate system: var mouse = d3.mouse(vis.node()); d.x = (mouse[0] - translation[0])/scaleFactor; d.y = (mouse[1] - translation[1])/scaleFactor; tick();//re-position this node and any links }

Este diseño de fuerza de zoom semántico arrastrable se implementa aquí:
http://jsfiddle.net/cSn6w/8/

Eso debería ser suficiente para volver a encarrilarnos. Volveré más tarde y añadiré una explicación de las escalas y cómo hacen todos estos cálculos más fáciles.

...Y estoy de vuelta:

Mirando todas las funciones de conversión de datos a visualización anteriores, ¿no te hace pensar "¿no sería más fácil tener una función para hacer esto cada vez?" Para eso son las escalas d3 : para convertir valores de datos a valores de visualización.

No suele ver escalas en los ejemplos de diseño de fuerza porque el objeto de disposición de fuerza le permite establecer un ancho y alto directamente, y luego crea valores de datos dx y dy dentro de ese rango. Establezca el ancho y la altura del diseño para su ancho y alto de visualización, y puede usar los valores de datos directamente para colocar objetos en la pantalla.

Sin embargo, cuando amplía el gráfico, pasa de tener toda la extensión de los datos a solo una porción visible. Entonces los valores de los datos ya no se corresponden directamente con los valores de posicionamiento, y tenemos que convertirlos entre ellos. Y una función de escala lo haría mucho más fácil.

En la terminología D3, los valores de datos esperados son el dominio y los valores de salida / visualización deseados son el rango . Por lo tanto, el dominio inicial de la escala se calculará según los valores máximos y mínimos esperados del diseño, mientras que el rango inicial será las coordenadas máximas y mínimas de la visualización.

Cuando haces zoom, la relación entre el dominio y el rango cambia, por lo que uno de esos valores tendrá que cambiar en la escala. Afortunadamente, no tenemos que descifrar las fórmulas nosotros mismos, porque el comportamiento del zoom D3 lo calcula para nosotros, si adjuntamos los objetos de escala al objeto de comportamiento del zoom utilizando sus métodos .x() y .y() .

Como resultado, si cambiamos los métodos de dibujo para usar las escalas, entonces todo lo que tenemos que hacer en el método de zoom es llamar a la función de dibujo.

Aquí está el zoom semántico del ejemplo de grilla implementado usando escalas :
http://jsfiddle.net/LYuta/5/

Clave:

/*** Configure zoom behaviour ***/ var zoomer = d3.behavior.zoom() .scaleExtent([0.1,10]) //allow 10 times zoom in or out .on("zoom", zoom) //define the event handler function .x(xScale) .y(yScale); //attach the scales so their domains //will be updated automatically function zoom() { console.log("zoom", d3.event.translate, d3.event.scale); //the zoom behaviour has already changed //the domain of the x and y scales //so we just have to redraw using them drawLines(); } function drawLines() { //put positioning in a separate function //that can be called at initialization as well vLines.attr("x1", function(d){ return xScale(d); }) .attr("y1", yScale(0) ) .attr("x2", function(d){ return xScale(d); }) /* etc. */

El objeto de comportamiento de zoom d3 modifica las escalas al cambiar su dominio. Puede obtener un efecto similar al cambiar el rango de escala, ya que la parte importante es cambiar la relación entre el dominio y el rango. Sin embargo, el rango tiene otro significado importante: representar los valores máximos y mínimos utilizados en la pantalla. Al solo cambiar el lado del dominio de la escala con el comportamiento del zoom, el rango todavía representa los valores de visualización válidos. Lo que nos permite implementar un tipo diferente de zoom, para cuando el usuario cambie el tamaño de la pantalla. Al dejar que SVG cambie el tamaño de acuerdo con el tamaño de la ventana y luego establecer el rango de la escala según el tamaño de SVG, el gráfico puede responder a diferentes tamaños de ventana / dispositivo.

Aquí está el ejemplo de la cuadrícula de zoom semántico, hecho receptivo con escalas :
http://jsfiddle.net/LYuta/9/

He dado las propiedades de alto y ancho basadas en porcentaje de SVG en CSS, que anularán los valores de altura y ancho de los atributos. En la secuencia de comandos, he movido todas las líneas que se relacionan con la altura y el ancho de la pantalla a una función que comprueba el elemento svg real para su altura y ancho actuales. Finalmente, agregué un oyente de cambio de tamaño de ventana para llamar a este método (que también desencadena un nuevo dibujo).

Clave:

/* Set the display size based on the SVG size and re-draw */ function setSize() { var svgStyles = window.getComputedStyle(svg.node()); var svgW = parseInt(svgStyles["width"]); var svgH = parseInt(svgStyles["height"]); //Set the output range of the scales xScale.range([0, svgW]); yScale.range([0, svgH]); //re-attach the scales to the zoom behaviour zoomer.x(xScale) .y(yScale); //resize the background rect.attr("width", svgW) .attr("height", svgH); //console.log(xScale.range(), yScale.range()); drawLines(); } //adapt size to window changes: window.addEventListener("resize", setSize, false) setSize(); //initialize width and height

Las mismas ideas: usar escalas para diseñar el gráfico, con un dominio cambiante del zoom y un rango cambiante de eventos de cambio de tamaño de la ventana, por supuesto, se pueden aplicar al diseño de fuerza. Sin embargo, todavía tenemos que lidiar con la complicación discutida anteriormente: cómo invertir la conversión de valores de datos a valores de visualización cuando se trata de eventos de arrastre de nodo. La escala lineal d3 tiene un método conveniente para eso también: scale.invert() . Si w = scale(x) entonces x = scale.invert(w) .

En el evento node-drag, el código que usa escalas es por lo tanto:

function dragged(d){ if (d.fixed) return; //root is fixed //get mouse coordinates relative to the visualization //coordinate system: var mouse = d3.mouse(vis.node()); d.x = xScale.invert(mouse[0]); d.y = yScale.invert(mouse[1]); tick();//re-position this node and any links }

El resto del ejemplo de diseño de fuerza de zoom semántico, hecho receptivo con escalas está aquí:
http://jsfiddle.net/cSn6w/10/

Estoy seguro de que fue una discusión mucho más larga de lo que esperabas, pero espero que te ayude a comprender no solo lo que debes hacer, sino también por qué debes hacerlo. Me siento realmente frustrado cuando veo código que obviamente ha sido cortado y pegado de múltiples ejemplos por alguien que realmente no entiende lo que hace el código. Si entiende el código, es mucho más fácil adaptarlo a sus necesidades. Y con suerte, esto servirá como una buena referencia para otras personas que intentan descubrir cómo hacer tareas similares.