zoom semântico do gráfico direcionado à força em d3

Muitos casos foram mostrados para zoom geométrico gráfico direcionado à força pelo SVG Geometric Zooming .

No zoom geométrico, eu só preciso adicionar um atributo de transformação na function de zoom. No entanto, no zoom semântico, se eu apenas adicionar um atributo de transformação no nó, os links não se conectarão ao nó. Então, eu estou querendo saber se existe uma solução para o zoom geométrico para o gráfico direcionado à força em d3.

Aqui está o meu exemplo com zoom geométrico seguindo o caso anterior. Eu tenho dois problemas:

  1. Quando eu diminuir o zoom, arraste o gráfico inteiro, o gráfico irá desaparecer estranhamente.
  2. Usando a mesma function de redesenho
function zoom() { vis.attr("transform", transform); } function transform(d){ return "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"; } 

Isso só atualiza o atributo “transformar” de um elemento svg. Mas como fazer a function para mudar a posição do nó?

Mas o que eu quero fazer é zoom semântico . Eu tentei modificar o zoom e transformar a function, mas não tenho certeza o caminho certo a fazer. Aqui está o que eu tento. Funções que mudei:

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

Eu tentei encontrar um bom tutorial para vincular, mas não consegui encontrar nada que realmente cobrisse todos os problemas, então vou escrevê-lo passo-a-passo.

Primeiro, você precisa entender claramente o que está tentando realizar. Isso é diferente para os dois tipos de zoom. Eu realmente não gosto da terminologia que Mike Bostock introduziu (não é inteiramente consistente com os usos não-d3 dos termos), mas também podemos ficar com ela para ser consistente com outros exemplos de d3.

Em “zoom geométrico” você está ampliando a imagem inteira. Círculos e linhas ficam maiores e mais distantes. O SVG tem uma maneira fácil de realizar isso através do atributo “transformar”. Quando você define transform="scale(2)" em um elemento SVG, ele é desenhado como se tudo fosse duas vezes maior. Para um círculo, o raio é desenhado duas vezes maior, e as posições cx e cy são plotadas duas vezes a distância do ponto (0,0). Todo o sistema de coordenadas muda, então uma unidade agora é igual a dois pixels na canvas, não um.

Da mesma forma, transform="translate(-50,100)" altera todo o sistema de coordenadas, de modo que o ponto (0,0) do sistema de coordenadas seja movido 50 unidades para a esquerda e 100 unidades para baixo a partir do canto superior esquerdo (que é o ponto de origem padrão).

Se você traduzir e dimensionar um elemento SVG, a ordem é importante. Se a tradução é antes da escala, a tradução está nas unidades originais. Se a tradução é depois da escala, então a tradução está nas unidades escalonadas.

O método d3.zoom.behavior() cria uma function que escuta events de roda e arrastar do mouse, bem como events de canvas de toque associados a zoom. Ele converte esses events de usuário em um evento “zoom” personalizado.

O evento de zoom recebe um fator de escala (um único número) e um fator de conversão (um array de dois números), que o object de comportamento calcula a partir dos movimentos do usuário. O que você faz com esses números é com você; eles não mudam nada diretamente . (Com exceção de quando você anexa uma escala à function de comportamento de zoom, conforme descrito posteriormente).

Para zoom geométrico, o que você normalmente faz é definir uma escala e converter o atributo de transformação em um elemento que contém o conteúdo que você deseja ampliar. Este exemplo implementa esse método de zoom geométrico em um SVG simples que consiste em linhas de grade uniformemente posicionadas :
http://jsfiddle.net/LYuta/2/

O código de zoom é simplesmente:

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

O zoom é realizado definindo o atributo de transformação em “vis”, que é uma seleção d3 contendo um elemento que contém todo o conteúdo que queremos ampliar. Os fatores de conversão e dimensionamento vêm diretamente do evento de zoom criado pelo comportamento de d3.

O resultado é que tudo fica maior ou menor – a largura das linhas de grade, bem como o espaçamento entre elas. As linhas ainda têm stroke-width:1.5; mas a definição do que 1.5 é igual na canvas mudou para eles e qualquer outra coisa dentro do elemento transformado.

Para cada evento de zoom, os fatores de conversão e dimensionamento também são registrados no console. Olhando para isso, você notará que, se você diminuir o zoom, a escala ficará entre 0 e 1; se você aumentar o zoom, ele será maior que 1. Se você deslocar (arrastar para mover) o gráfico, a escala não será alterada. Os números de tradução, no entanto, mudam tanto no pan como no zoom. Isso ocorre porque a conversão representa a posição do ponto (0,0) no gráfico em relação à posição do canto superior esquerdo do SVG. Quando você aplica zoom, a distância entre (0,0) e qualquer outro ponto no gráfico muda. Portanto, para manter o conteúdo sob o mouse ou o toque com o dedo na mesma posição na canvas, a posição do ponto (0,0) deve se mover.

Há várias outras coisas que você deve prestar atenção nesse exemplo:

  • Eu modifiquei o object de comportamento de zoom com o .scaleExtent([min,max]) . Isso define um limite nos valores de escala que o comportamento usará no evento de zoom, independentemente do quanto o usuário gira a roda.

  • A transformação está em um elemento , não no próprio . Isso porque o elemento SVG como um todo é tratado como um elemento HTML e possui uma syntax e propriedades de transformação diferentes.

  • O comportamento do zoom é anexado a um elemento diferente , que contém o principal e um retângulo de plano de fundo. O retângulo de fundo existe para que os events de mouse e toque possam ser observados, mesmo se o mouse ou o toque não estiverem em uma linha. O elemento si não tem altura nem largura e, portanto, não pode responder diretamente aos events do usuário, apenas recebe events de seus filhos. Deixei o retângulo preto para que você possa dizer onde está, mas pode definir o estilo para fill:none; contanto que você também o configure para pointer-events:all; . O retângulo não pode estar dentro do que é transformado, porque a área que responde aos events de zoom também diminuirá quando você diminuir o zoom e possivelmente ficar fora da vista da borda do SVG.

  • Você pode pular o retângulo e o segundo elemento anexando o comportamento do zoom diretamente ao object SVG, como nesta versão do violino . No entanto, muitas vezes você não deseja que events em toda a área do SVG acionem o zoom, por isso é bom saber como e por que usar a opção de retângulo de fundo.

Aqui está o mesmo método de zoom geométrico, aplicado a uma versão simplificada do layout de força :
http://jsfiddle.net/cSn6w/5/

Reduzi o número de nós e links e removi o comportamento de arrastar e arrastar com nó e o comportamento de expansão / recolhimento do nó, para que você possa se concentrar no zoom. Eu também mudei o parâmetro “friction” para que demore mais tempo para o gráfico parar de se mover; faça zoom enquanto ainda está em movimento, e você verá que tudo continuará se movendo como antes.

O “zoom geométrico” da imagem é bastante simples, pode ser implementado com muito pouco código e resulta em alterações rápidas e suaves pelo navegador. No entanto, muitas vezes, o motivo pelo qual você deseja aumentar o zoom em um gráfico é porque os pontos de dados estão muito próximos e sobrepostos. Nesse caso, apenas tornar tudo maior não ajuda. Você deseja estender os elementos em um espaço maior, mantendo os pontos individuais no mesmo tamanho. É aí que “zoom semântico” entra em vigor.

O “zoom semântico” de um gráfico, no sentido em que Mike Bostock usa o termo , é ampliar o layout do gráfico sem aplicar zoom em elementos individuais. (Observe que há outras interpretações de “zoom semântico” para outros contextos).

Isso é feito alterando a maneira como a posição dos elementos é calculada, bem como o comprimento de quaisquer linhas ou caminhos que conectam objects, sem alterar o sistema de coordenadas subjacente que define o tamanho de um pixel com a finalidade de definir a largura da linha ou tamanho de formas ou texto.

Você mesmo pode fazer esses cálculos usando os valores de conversão e escala para posicionar os objects com base nessas fórmulas:

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

Eu usei essa abordagem para implementar o zoom semântico nesta versão do exemplo de linhas de grade :
http://jsfiddle.net/LYuta/4/

Para as linhas verticais, elas foram originalmente posicionadas assim

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

Na function de zoom, isso é alterado para

 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); 

As linhas horizontais são alteradas de forma semelhante. O resultado? A posição e o comprimento das linhas mudam no zoom, sem que as linhas fiquem mais grossas ou mais finas.

Fica um pouco complicado quando tentamos fazer o mesmo para o layout da força. Isso porque os objects no gráfico de layout de força também estão sendo reposicionados após cada evento “tick”. Para mantê-los posicionados nos locais corretos para o zoom, o método de posicionamento de carrapatos terá que usar as fórmulas de posição ampliada. O que significa que:

  1. A escala e a tradução devem ser salvas em uma variável que possa ser acessada pela function tick; e,
  2. É necessário haver valores padrão de escala e conversão para a function de marcação a ser usada se o usuário ainda não tiver ampliado nada.

A escala padrão será 1 e a tradução padrão será [0,0], representando escala normal e sem conversão.

Aqui está o que parece com o zoom semântico no layout de força simplificada :
http://jsfiddle.net/cSn6w/6/

A function de zoom é agora

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

Define as variables ​​scaleFactor e translation e, em seguida, chama a function tick. A function tick faz todo o posicionamento: na boot, depois dos events tick do layout force e após os events 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*dx; }) .attr("cy", function (d) { return translation[1] + scaleFactor*dy; }); } 

Cada valor de posição para os círculos e os links é ajustado pela tradução e pelo fator de escala. Se isso faz sentido para você, isso deve ser suficiente para o seu projeto e você não precisa usar escalas. Apenas certifique-se de usar sempre esta fórmula para converter entre as coordenadas de dados (dx e dy) e as coordenadas de exibição (cx, cy, x1, x2, etc.) usadas para posicionar os objects.

Onde isso fica complicado é se você precisa fazer a conversão inversa de coordenadas de exibição para coordenadas de dados. Você precisa fazer isso se quiser que o usuário seja capaz de arrastar nós individuais – você precisa definir a coordenada de dados com base na posição da canvas do nó arrastado. (Observe que isso não funcionou corretamente em nenhum dos seus exemplos).

Para zoom geométrico , a conversão entre a posição da canvas e a posição dos dados pode ser reduzida com d3.mouse() . Usando d3.mouse(SVGElement) calcula a posição do mouse no sistema de coordenadas usado por esse SVGElement . Então, se passarmos no elemento que representa a visualização transformada, ele retorna coordenadas que podem ser usadas diretamente para definir a posição dos objects.

O layout de força de zoom geométrico arrastável se parece com isto:
http://jsfiddle.net/cSn6w/7/

A function de arrastar é:

 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()); dx = mouse[0]; dy = mouse[1]; tick();//re-position this node and any links } 

Para o zoom semântico , no entanto, as coordenadas SVG retornadas por d3.mouse() não correspondem mais diretamente às coordenadas de dados. Você tem que levar em conta a escala e a tradução. Você faz isso reorganizando as fórmulas fornecidas acima:

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

torna-se

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

A function de arrastar para o exemplo de zoom semântico é, portanto,

 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()); dx = (mouse[0] - translation[0])/scaleFactor; dy = (mouse[1] - translation[1])/scaleFactor; tick();//re-position this node and any links } 

Este layout de força de zoom semântico arrastável é implementado aqui:
http://jsfiddle.net/cSn6w/8/

Isso deve ser suficiente para você voltar aos rails. Voltarei mais tarde e adicionarei uma explicação das escalas e como elas facilitam todos esses cálculos.

… e estou de volta:

Analisando todas as funções de conversão de dados para exibição acima, você não acha que “não seria mais fácil ter uma function para fazer isso todas as vezes?” É para isso que as escalas d3 são: converter valores de dados para exibir valores.

Você não costuma ver escalas nos exemplos de layout de força porque o object de layout de força permite definir diretamente uma largura e a altura e, em seguida, cria valores de dados dx e dy dentro desse intervalo. Defina a largura e a altura do layout para a largura e a altura de sua visualização, e você pode usar os valores de dados diretamente para posicionar objects na exibição.

No entanto, quando você aumenta o zoom no gráfico, você deixa de ter toda a extensão dos dados visíveis para ter apenas uma parte visível. Portanto, os valores de dados não correspondem mais diretamente aos valores de posicionamento e precisamos convertê-los. E uma function de escala tornaria isso muito mais fácil.

Na terminologia D3, os valores de dados esperados são o domínio e os valores de saída / exibição desejados são o intervalo . O domínio inicial da escala será, portanto, pelos valores máximos e mínimos esperados do layout, enquanto o intervalo inicial será as coordenadas máxima e mínima na visualização.

Quando você aplica zoom, a relação entre o domínio e o intervalo muda; portanto, um desses valores terá que ser alterado na escala. Felizmente, não precisamos descobrir as fórmulas, porque o comportamento do zoom D3 calcula isso para nós – se appendmos os objects de escala ao object de comportamento de zoom usando seus methods .x() e .y() .

Como resultado, se alterarmos os methods de desenho para usar as escalas, tudo o que precisamos fazer no método de zoom é chamar a function de desenho.

Aqui está o zoom semântico do exemplo de grade implementado usando escalas :
http://jsfiddle.net/LYuta/5/

Código chave:

 /*** 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. */ 

O object de comportamento de zoom d3 modifica as escalas alterando seu domínio. Você pode obter um efeito semelhante alterando o intervalo de escala, pois a parte importante é alterar o relacionamento entre domínio e intervalo. No entanto, o intervalo tem outro significado importante: representar os valores máximo e mínimo usados ​​na exibição. Alterando apenas o lado do domínio da escala com o comportamento de zoom, o intervalo ainda representa os valores de exibição válidos. O que nos permite implementar um tipo diferente de zoom, para quando o usuário redimensiona a exibição. Ao permitir que o SVG mude de tamanho de acordo com o tamanho da janela e, em seguida, defina o intervalo da escala com base no tamanho SVG, o gráfico pode responder a diferentes tamanhos de janela / dispositivo.

Aqui está o exemplo da grade de zoom semântico, feito responsivo com escalas :
http://jsfiddle.net/LYuta/9/

Eu dei as propriedades de altura e largura baseadas em porcentagem de SVG em CSS, que irão sobrepor os valores de altura e largura do atributo. No script, movi todas as linhas relacionadas à altura e largura de exibição em uma function que verifica o elemento svg real para sua altura e largura atuais. Finalmente, adicionei um ouvinte de redimensionamento de janela para chamar esse método (que também aciona um novo sorteio).

Código chave:

 /* 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 

As mesmas idéias – usando escalas para o layout do gráfico, com um domínio em mudança a partir do zoom e um intervalo de mudança a partir de events de redimensionamento de janela – podem, é claro, ser aplicadas ao layout de força. No entanto, ainda temos que lidar com a complicação discutida acima: como reverter a conversão dos valores de dados para exibir valores ao lidar com events de arrastamento de nó. A escala linear d3 também tem um método conveniente para isso: scale.invert() . Se w = scale(x) então x = scale.invert(w) .

No evento de arrastar com nó, o código usando escalas é, portanto:

 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()); dx = xScale.invert(mouse[0]); dy = yScale.invert(mouse[1]); tick();//re-position this node and any links } 

O restante do exemplo de layout de força de zoom semântico, feito responsivo com escalas, está aqui:
http://jsfiddle.net/cSn6w/10/


Tenho certeza que foi muito mais uma discussão do que você esperava, mas espero que ajude você a entender não apenas o que precisa fazer, mas também por que precisa fazê-lo. Eu fico realmente frustrado quando vejo código que foi obviamente cortado e colado de vários exemplos por alguém que não entende realmente o que o código faz. Se você entender o código, é muito mais fácil adaptá-lo às suas necessidades. E esperamos que isso sirva como uma boa referência para outras pessoas que tentam descobrir como fazer tarefas semelhantes.

Você precisa transformar o nó e redesenhar os caminhos.

A idéia de “zoom semântico” é que você altera a escala do seu layout, mas não o tamanho dos elementos individuais.

Se você configurou o comportamento de zoom como no exemplo vinculado, ele atualizará automaticamente as escalas xe y para você. Você então redefine a posição dos nós com base nessas escalas e também pode redefinir a posição e a forma dos links.

Se os seus links forem retos, redefina as posições x1, y1, x2 e y2 usando as escalas x e y atualizadas. Se seus links forem caminhos criados com d3.svg.diagonal e as escalas x e y, redefina o atributo “d” com a mesma function.

Se você precisar de instruções mais específicas, terá que postar seu código.