d3中的力有向图的语义缩放 [英] sematic zooming of force directed graph in d3

查看:189
本文介绍了d3中的力有向图的语义缩放的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

许多案例已通过 SVG几何缩放显示为强制有向图几何缩放。



在几何缩放中,我只需要在缩放功能中添加一个transform属性。但是,在语义缩放中,如果我只在节点中添加transform属性,链接将不会连接到节点。所以,我想知道是否存在一个解决方案几何缩放d3中的力有向图。



这里是我的示例,其中几何缩放跟随在前面的情况之后。我有两个问题:


  1. 当我缩小时,拖动整个图形,图形将奇怪地消失。

  2. 使用相同的重绘函数




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


这只会更新一个svg元素的transform 属性。但是如何使函数改变节点位置呢?



但是我想做的是语义缩放。我已经尝试修改缩放和转换功能,但不确定正确的方法做。 这里是我的尝试。函数我改变了:


  function zoom(){
node.call );
//更新链接位置
update();
}
function transform(d){
//改变节点x,y位置,不知道这里放什么函数。
}






解决方案

我试图找到一个很好的教程链接,找到任何真正涵盖所有问题的东西,所以我将逐步自己写出来。



首先,你需要清楚地了解你试图完成。这对于两种类型的缩放是不同的。我不喜欢Mike Bostock介绍的术语,(它不完全符合非d3使用的术语),但我们可以坚持它与其他d3示例一致。



几何缩放中,您正在缩放整个图片。圆和线变得越来越大,越远越远。 SVG有一个简单的方法通过transform属性来实现这一点。当在SVG元素上设置 transform =scale(2)时,它就像是一切都是两倍大。对于一个圆,它的半径被绘制两倍,它是 cx cy (0,0)点。整个坐标系统改变,因此一个单位现在等于屏幕上的两个像素,而不是一个。



同样, transform =translate(-50,100)更改整个坐标系, ,0)坐标系向左移动50个单位,从左上角(默认原点)向下移动100个单位。



如果您既可以翻译缩放SVG元素,顺序也很重要。如果在缩放之前翻译是,则翻译是以原始单位。



/ mbostock / d3 / wiki / Zoom-Behavior> d3.zoom.behavior() 方法创建一个函数来监听鼠标滚轮和拖动事件以及与缩放相关联的触摸屏事件。它将这些用户事件转换为自定义缩放事件。



缩放事件给出一个缩放因子(单个数字)和一个平移因子(两个数字的数组),行为对象根据用户的移动计算。你用这些数字做什么取决于你; 他们不会直接更改任何内容



对于几何缩放,您通常使用的缩放功能do在包含要缩放的内容的< g> 元素上设置scale和translate transform属性。此示例在由均匀放置的网格线组成的简单SVG上实现几何缩放方法

http://jsfiddle.net/LYuta/2/



缩放代码很简单:

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

通过在vis上设置transform属性,包含< g> 元素的d3选择,其本身包含我们想要缩放的所有内容。平移和缩放因子直接来自d3行为创建的缩放事件。



结果是全部变大或变小 - 网格线的宽度以及它们之间的间距。这些行仍然有 stroke-width:1.5; ,但是在屏幕上的1.5等于的定义已经改变了他们和变换的< ; g> 元素。



对于每个缩放事件,平移和缩放因子也记录到控制台。看看,你会注意到,如果你缩小的比例将在0和1之间;如果放大,它将大于1.如果您平移(拖动移动)图形,比例将不会改变。但是,平移数字在缩放时都会改变。这是因为translate表示图中(0,0)点相对于SVG左上角位置的位置。当您缩放时,(0,0)和图表上的任何其他点之间的距离会改变。因此为了在屏幕上的相同位置保持鼠标或手指触摸的内容,(0,0)点的位置必须移动。






  • 我已经修改了缩放行为对象与 .scaleExtent([min,max]) 方法。


  • 变换已开启,但是对缩放事件中的行为将使用的缩放值设置限制a < g> 元素,而不是< svg> 这是因为SVG元素作为一个整体被当作一个HTML元素,并有不同的变换语法和属性。


  • 缩放行为附加到<包含主要< g> 的元素不同 < g> 长方形。背景矩形是这样,即使鼠标或触摸不在一行上,也可以观察到鼠标和触摸事件。 < g> 元素本身没有任何高度或宽度,因此不能直接响应用户事件,它只接收来自其子元素的事件。我把矩形留下黑色,这样你可以知道它的位置,但你可以设置它的风格为 fill:none; ,只要你还设置为 pointer-events:all; 。该矩形不能< g> 中被转换,因为那时响应缩放事件的区域也会缩小

  • 可以跳过矩形和第二个<$通过将缩放行为直接附加到SVG对象,如此版本中的c $ c>< g> 元素的小提琴。但是,您通常不希望在整个 SVG区域上的事件触发缩放,因此了解如何以及为什么要使用背景矩形选项很好。




这里有同样的几何缩放方法,适用于力布局的简化版本

http://jsfiddle.net/cSn6w/5/



我已经减少了节点和链接的数量,并且带走了节点拖动行为和节点扩展/折叠行为,因此您可以专注于缩放。我也改变了摩擦参数,使得图形停止移动需要更长时间;缩放它,而你仍然移动,你会看到一切将继续前面。



图像的几何缩放是相当简单,它可以实现具有非常少的代码,并且其导致由浏览器快速,平滑地改变。然而,通常,您想要放大图表的原因是因为数据点太靠近在一起和重叠。在这种情况下,只是使一切更大并没有帮助。您希望在更大的空间上拉伸元素,同时保持单个点的大小相同。这是语义缩放的起点。 ://stackoverflow.com/a/12335448/3128209> Mike Bostock使用术语,是缩放图形的布局,而不对单个元素进行缩放。 (注意,对于其他上下文,还有其他语义缩放的解释。)



这是通过改变位置,以及连接对象的任何线或路径的长度而不更改定义像素大小的底层坐标系统是为了设置线宽或形状或文本的大小。



您可以自己执行这些计算,使用平移和缩放值基于以下公式定位对象:

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

zoomedPositionY = d3.event。 translate [1] + d3.event.scale * dataPositionY



我使用这种方法来实现语义放大此版本网格线示例

http://jsfiddle.net/ LYuta / 4 /



对于垂直线,它们最初的位置如下:

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

在缩放功能中,它变为

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

水平线也同样改变。结果?



当我们尝试对强制执行相同操作时,它变得有点复杂布局。这是因为力布局图中的对象也在每个tick事件之后重新定位。为了使它们位于用于缩放的正确位置,刻度定位方法将必须使用缩放位置公式。这意味着:


  1. 规模和翻译必须保存在可以通过tick函数访问的变量中;和

  2. 如果用户尚未缩放任何内容,则需要为tick函数使用默认比例和翻译值。

默认比例将为1,默认翻译为[0,0],表示正常比例和无翻译。



以下是在简化强制布局 语义缩放的外观:

http ://jsfiddle.net/cSn6w/6/



缩放功能现在

  function zoom(){
console.log(zoom,d3.event.translate,d3.event.scale);
scaleFactor = d3.event.scale;
translate = d3.event.translate;
tick(); //更新位置
}

设置scaleFactor和翻译变量,功能。 tick函数 all 定位:初始化时,强制布局tick事件后和zoom事件后。它看起来像

  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 ,function(d){
return translation [1] + scaleFactor * dy;
});
}

圆圈和链接的每个位置值通过平移和比例因子进行调整。如果这对你有意义,这对于你的项目是足够的,你不需要使用秤。只要确保您始终使用此公式在数据坐标(dx和dy)和显示坐标(cx,cy,x1,x2等)用于定位对象。



如果您需要从显示坐标到数据坐标进行反向转换,这里变得复杂。如果您希望用户能够拖动单个节点,则需要执行此操作 - 您需要根据拖动节点的屏幕位置设置数据坐标。 (请注意,在任何一个示例中都无法正常工作。)



对于几何缩放,可以在屏幕位置和数据位置之间进行转换与 d3.mouse() 。使用 d3.mouse(SVGElement)在由该SVGElement 使用的坐标系统中计算鼠标的位置。因此,如果我们传递代表​​转换的可视化的元素,它返回可以直接用于设置对象位置的坐标。



zoom force-layout 看起来像这样:

http://jsfiddle.net/cSn6w/7/



拖曳功能是:

  (d){
if(d.fixed)return; // root is fixed

//获取相对于可视化的鼠标坐标
//坐标系:
var mouse = d3.mouse(vis.node());
d.x = mouse [0];
d.y = mouse [1];
tick(); //重新定位这个节点和任何链接
}

对于语义缩放 d3.mouse()返回的SVG坐标不再直接对应于数据坐标。你必须考虑规模和翻译。您可以通过重新排列上面给出的公式来完成此操作:

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

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

成为

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

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

因此语义缩放示例的拖动函数为

  function dragged(d){
if(d.fixed)return; // root is fixed

//获取相对于可视化的鼠标坐标
//坐标系:
var mouse = d3.mouse(vis.node());
d.x =(mouse [0] - translation [0])/ scaleFactor;
d.y =(mouse [1] - translation [1])/ scaleFactor;
tick(); //重新定位这个节点和任何链接
}

此处实施此可拖动语义缩放force-layout

http:/ /jsfiddle.net/cSn6w/8/



这应该足以让你回到正轨。我会稍后再来,并添加一个关于磅秤的解释,以及它们如何使所有这些计算更容易。



...返回:



查看上述所有数据到显示转换函数,不是让你认为一个函数每次都这样做?这就是 d3比例用于:将数据值转换为显示值。



你不经常看到力布局示例中的尺度,因为力布局对象允许您直接设置宽度和高度,然后创建dx和dy数据值。将布局宽度和高度设置为可视化宽度和高度,您可以直接使用数据值来定位显示中的对象。



但是,放大时图表,您从整个数据范围可见,只有一部分可见。因此,数据值不再直接对应于定位值,我们需要在它们之间进行转换。



在D3术语中,预期的数据值是和所需的输出/显示值是范围。因此,尺度的初始域将由布局中的预期最大值和最小值,而初始范围将是可视化上的最大和最小坐标。



当你缩放,域和范围之间的关系发生变化,所以其中的一个值将必须在规模上改变。幸运的是,我们不必自己计算公式,因为D3缩放行为会为我们计算它 - if 我们使用 .x()和 .y()方法。



因此,如果我们更改绘图方法以使用缩放,那么我们在缩放方法中所要做的就是调用绘图函数。



这里是使用缩放比例实现的网格示例的语义缩放:

http://jsfiddle.net/LYuta/5/



键码:

  / ***配置缩放行为*** / 
var zoomer = d3.behavior.zoom()
.scaleExtent([0.1,10 ])
//允许10次放大或缩小
.on(zoom,zoom)
//定义事件处理函数
.x(xScale)
.y(yScale);
//附加缩放比例,以便它们的域
//将自动更新

function zoom(){
console.log(zoom,d3.event .translate,d3.event.scale);

//缩放行为已经改变
// x和y的域缩放
//所以我们只需要使用它们重绘
drawLines );
}
function drawLines(){
//将定位放在单独的函数中
//在初始化时可以调用
vLines.attr(x1 ,函数(d){
return xScale(d);
})
.attr(y1,yScale(0))
.attr(x2 function(d){
return xScale(d);
})
/ * etc. * /

d3缩放行为对象通过更改域来修改缩放。您可以通过更改比例范围获得类似的效果,因为重要的是更改域和范围之间的关系。但是,该范围具有另一个重要的含义:表示显示中使用的最大值和最小值。通过仅使用缩放行为更改标尺的域边,该范围仍然表示有效的显示值。这允许我们实现不同类型的缩放,当用户重新调整显示大小时。通过让SVG根据窗口大小更改大小,然后根据SVG大小设置缩放范围,图表可以响应不同的窗口/设备大小。



以下是语义缩放网格示例,使用缩放比例

http: //jsfiddle.net/LYuta/9/



我已经在CSS中给出了基于SVG百分比的高度和宽度属性,这将覆盖属性高度和宽度值。在脚本中,我将与显示高度和宽度相关的所有行都移动到一个函数中,该函数检查实际的svg元素的当前高度和宽度。最后,我添加了一个调整窗口大小的监听器来调用此方法(也触发重新绘制)。



键代码:

  / *根据SVG大小设置显示大小并重新绘制* / 
function setSize(){
var svgStyles = window .getComputedStyle(svg.node());
var svgW = parseInt(svgStyles [width]);
var svgH = parseInt(svgStyles [height]);

//设置缩放的输出范围
xScale.range([0,svgW]);
yScale.range([0,svgH]);

//将缩放重新附加到缩放行为
zoomer.x(xScale)
.y(yScale);

//调整背景大小
rect.attr(width,svgW)
.attr(height,svgH);

//console.log(xScale.range(),yScale.range());
drawLines();
}

//根据窗口更改调整大小:
window.addEventListener(resize,setSize,false)

setSize //初始化宽度和高度

同样的想法 - 使用比例来布局图形,域从缩放和从窗口大小调整事件的变化范围 - 当然可以应用于力布局。然而,我们仍然必须处理上面讨论的复杂性:如何在处理节点拖动事件时从数据值到显示值的反转。 d3线性标度也有一个方便的方法: scale.invert()。如果 w = scale(x),则 x = scale.invert(w)



在节点拖动事件中,使用缩放的代码是:

  ){
if(d.fixed)return; // root is fixed

//获取相对于可视化的鼠标坐标
//坐标系:
var mouse = d3.mouse(vis.node());
d.x = xScale.invert(mouse [0]);
d.y = yScale.invert(mouse [1]);
tick(); //重新定位这个节点和任何链接
}

语义缩放力布局示例的其余部分如下所示:

http://jsfiddle.net/cSn6w/10/






我相信是一个比你想象的讨论更长的时间,但我希望它不仅能帮助你理解你需要做什么,而且还能帮助你理解为什么需要这样做。 我真的很失望,当我看到的代码显然是从多个示例中剪切和粘贴的人,实际上并不了解代码的作用。如果你理解代码,它很容易适应你的需要。希望这将为其他试图找出如何做类似任务的人提供一个很好的参考。


Many cases have been shown for force directed graph geometric zooming by SVG Geometric Zooming.

In geometric zooming, I only need to add a transform attribute in zoom function. However, in sematic zooming, if I only add a transform attribute in node, links won't connect to node. So, I am wondering whether there exist a solution for geometric zooming for force directed graph in d3.

Here is my example with geometric zooming following previous case. I have two problems:

  1. When I zoom out, then drag whole graph, the graph will strangely disappear.
  2. Using the same redraw function

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

This only update one svg element's "transform" attribute. But how to make the function to change the node position?

But what I want to do is semantic zooming. I have tried to modify zoom and transform function, but not sure the right way to do. Here is what I try. Functions I have changed:

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

Thanks!


解决方案

I tried to find a good tutorial to link to, but couldn't find anything that really covered all the issues, so I'm going to write it out step-by-step myself.

First, you need to clearly understand what you're trying to accomplish. This is different for the two types of zooming. I don't really like the terminology Mike Bostock has introduced, (it's not entirely consistent with non-d3 uses of the terms) but we might as well stick with it to be consistent with other d3 examples.

In "geometric zooming" you are zooming the entire image. Circles and lines get bigger as well as farther apart. SVG has an easy way to accomplish this through the "transform" attribute. When you set transform="scale(2)" on an SVG element, it is drawn as if everything was twice as big. For a circle, it's radius gets drawn twice a big, and it's cx and cy positions get plotted twice the distance from the (0,0) point. The entire coordinate system changes, so one unit is now equal to two pixels on screen, not one.

Likewise, transform="translate(-50,100)" changes the entire coordinate system, so that the (0,0) point of the coordinate system gets moved 50 units to the left and 100 units down from the top-left corner (which is the default origin point).

If you both translate and scale an SVG element, the order is important. If translate is before scale, than the translation is in the original units. If translate is after scale, than the translation is in the scaled units.

The d3.zoom.behavior() method creates a function that listens for mouse wheel and drag events, as well as for touch screen events associated with zooming. It converts these user events into a custom "zoom" event.

The zoom event is given a scale factor (a single number) and a translate factor (an array of two numbers), which the behaviour object calculates from the user's movements. What you do with these numbers is up to you; they don't change anything directly. (With the exception of when you attach a scale to the zoom behaviour function, as described later.)

For geometric zooming, what you usually do is set a scale and translate transform attribute on a <g> element that contains the content you want to zoom. This example implements that geometric zooming method on a simple SVG consisting of evenly placed gridlines:
http://jsfiddle.net/LYuta/2/

The zoom code is simply:

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

The zoom is accomplished by setting the transform attribute on "vis", which is a d3 selection containing a <g> element which itself contains all the content we want to zoom. The translate and scale factors come directly from the zoom event that the d3 behaviour created.

The result is that everything gets bigger or smaller -- the width of the gridlines as well as the spacing between them. The lines still have stroke-width:1.5; but the definition of what 1.5 equals on the screen has changed for them and anything else within the transformed <g> element.

For every zoom event, the translate and scale factors are also logged to the console. Looking at that, you'll notice that if you're zoomed out the scale will be between 0 and 1; if you're zoomed in it will be greater than 1. If you pan (drag to move) the graph, the scale won't change at all. The translate numbers, however, change on both pan and zoom. That's because the translate represents the position of the (0,0) point in the graph relative to the position of the top-left-corner of the SVG. When you zoom, the distance between (0,0) and any other point on the graph changes. So in order to keep the content under the mouse or finger-touch in the same position on the screen, the position of the (0,0) point has to move.

There are a number of other things you should pay attention to in that example:

  • I've modified the zoom behaviour object with the .scaleExtent([min,max]) method. This sets a limit on the scale values that the behaviour will use in the zoom event, no matter how much the user spins the wheel.

  • The transform is on a <g> element, not the <svg> itself. That's because the SVG element as a whole is treated as an HTML element, and has a different transform syntax and properties.

  • The zoom behaviour is attached to a different <g> element, that contains the main <g> and a background rectangle. The background rectangle is there so that mouse and touch events can be observed even if the mouse or touch isn't right on a line. The <g> element itself doesn't have any height or width and so can't respond to user events directly, it only receives events from its children. I've left the rectangle black so you can tell where it is, but you can set it's style to fill:none; so long as you also set it to pointer-events:all;. The rectangle can't be inside the <g> that gets transformed, because then the area that responds to zoom events would also shrink when you zoom out, and possibly go out of sight off the edge of the SVG.

  • You could skip the rectangle and second <g> element by attaching the zoom behaviour directly to the SVG object, as in this version of the fiddle. However, you often don't want events on the entire SVG area to trigger the zoom, so it is good to know how and why to use the background rectangle option.

Here's the same geometric zooming method, applied to a simplified version of your force layout:
http://jsfiddle.net/cSn6w/5/

I've reduced the number of nodes and links, and taken away the node-drag behaviour and the node-expand/collapse behaviour, so you can focus on the zoom. I've also changed the "friction" parameter so that it takes longer for the graph to stop moving; zoom it while it's still moving, and you'll see that everything will keep moving as before .

"Geometric zooming" of the image is fairly straightforward, it can be implemented with very little code, and it results in fast, smooth changes by the browser. However, often the reason you want to zoom in on a graph is because the datapoints are too close together and overlapping. In that case, just making everything bigger doesn't help. You want to stretch the elements out over a larger space while keeping the individual points the same size. That's where "semantic zooming" comes into place.

"Semantic zooming" of a graph, in the sense that Mike Bostock uses the term, is to zoom the layout of the graph without zooming on individual elements. (Note, there are other interpretations of "semantic zooming" for other contexts.)

This is done by changing the way the position of elements is calculated, as well as the length of any lines or paths that connect objects, without changing the underlying coordinate system that defines how big a pixel is for the purpose of setting line width or the size of shapes or text.

You can do these calculations yourself, using the translate and scale values to position the objects based on these formulas:

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

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

I've used that approach to implement semantic zooming in this version of the gridlines example:
http://jsfiddle.net/LYuta/4/

For the vertical lines, they were originally positioned like this

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

In the zoom function, that gets changed to

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

The horizontal lines are changed similarly. The result? The position and length of the lines changes on the zoom, without the lines getting thicker or thinner.

It gets a little complicated when we try to do the same for the force layout. That's because the objects in the force layout graph are also being re-positioned after every "tick" event. In order to keep them positioned in the correct places for the zoom, the tick-positioning method is going to have to use the zoomed-position formulas. Which means that:

  1. The scale and translation have to be saved in a variable that can be accessed by the tick function; and,
  2. There needs to be default scale and translation values for the tick function to use if the user hasn't zoomed anything yet.

The default scale will be 1, and the default translation will be [0,0], representing normal scale and no translation.

Here's what it looks like with semantic zooming on the simplified force layout:
http://jsfiddle.net/cSn6w/6/

The zoom function is now

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

It sets the scaleFactor and translation variables, then calls the tick function. The tick function does all the positioning: at initialization, after force-layout tick events, and after zoom events. It looks like

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

Every position value for the circles and the links is adjusted by the translation and the scale factor. If this makes sense to you, this should be sufficient for your project and you don't need to use scales. Just make sure that you always use this formula to convert between the data coordinates (d.x and d.y) and the display coordinates (cx, cy, x1, x2, etc.) used to position the objects.

Where this gets complicated is if you need to do the reverse conversion from display coordinates to data coordinates. You need to do this if you want the user to be able to drag individual nodes -- you need to set the data coordinate based on the screen position of the dragged node. (Note that this wasn't working properly in either of your examples).

For geometric zoom, converting between screen position and data position can be down with d3.mouse(). Using d3.mouse(SVGElement) calculates the position of the mouse in the coordinate system used by that SVGElement. So if we pass in the element representing the transformed visualization, it returns coordinates that can be used directly to set the position of the objects.

The draggable geometric-zoom force-layout looks like this:
http://jsfiddle.net/cSn6w/7/

The drag function is:

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
}

For semantic zoom, however, the SVG coordinates returned by d3.mouse() no longer directly correspond to the data coordinates. You have to factor in the scale and translation. You do this by re-arranging the formulas given above:

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

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

becomes

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

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

The drag function for the semantic zoom example is therefore

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
}

This draggable semantic-zoom force-layout is implemented here:
http://jsfiddle.net/cSn6w/8/

That should be enough to get you back on track. I'll come back later and add an explanation of scales and how they make all these calculations easier.

...and I'm back:

Looking at all the data-to-display conversion functions above, doesn't it make you think "wouldn't it be easier to have a function to do this each time?" That's what the the d3 scales are for: to convert data values to display values.

You don't often see scales in the force-layout examples because the force layout object allows you to set a width and height directly, and then creates d.x and d.y data values within that range. Set the layout width and height to your visualization width and height, and you can use the data values directly for positioning objects in the display.

However, when you zoom in on the graph, you switch from having the entire extent of the data visible to only having a portion visible. So the data values no longer directly correspond to positioning values, and we need to convert between them. And a scale function would make that a lot easier.

In D3 terminology, the expected data values are the domain and the desired output/display values are the range. The initial domain of the scale will therefore by the expected maximum and minimum values from the layout, while the initial range will be the maximum and minimum coordinates on the visualization.

When you zoom, the relationship between domain and range changes, so one of those values will have to change on the scale. Luckily, we don't have to figure out the formulas ourselves, because the D3 zoom behaviour calculates it for us -- if we attach the scale objects to the zoom behaviour object using its .x() and .y() methods.

As a result, if we change the drawing methods to use the scales, then all we have to do in the zoom method is call the drawing function.

Here's the semantic zoom of the grid example implemented using scales:
http://jsfiddle.net/LYuta/5/

Key code:

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

The d3 zoom behaviour object modifies the scales by changing their domain. You could get a similar effect by changing the scale range, since the important part is changing the relationship between domain and range. However, the range has another important meaning: representing the maximum and minimum values used in the display. By only changing the domain side of the scale with the zoom behaviour, the range still represents the valid display values. Which allows us to implement a different type of zoom, for when the user re-sizes the display. By letting the SVG change size according to the window size, and then setting the range of the scale based on the SVG size, the graph can be responsive to different window/device sizes.

Here's the semantic zoom grid example, made responsive with scales:
http://jsfiddle.net/LYuta/9/

I've given the SVG percentage-based height and width properties in CSS, which will over-ride the attribute height and width values. In the script, I've moved all the lines which relate to the display height and width into a function that checks the actual svg element for it's current height and width. Finally, I've added a window resize listener to call this method (which also triggers a re-draw).

Key code:

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

The same ideas -- using scales to layout the graph, with a changing domain from the zoom and a changing range from window resize events -- can of course be applied to the force-layout. However, we still have to deal with the complication discussed above: how to reverse the conversion from data values to display values when dealing with node-drag events. The d3 linear scale has a convenient method for that, too: scale.invert(). If w = scale(x) then x = scale.invert(w).

In the node-drag event, the code using scales is therefore:

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
}

The rest of the semantic zoom force-layout example, made responsive with scales is here:
http://jsfiddle.net/cSn6w/10/


I'm sure that was a lot longer a discussion than you were expecting, but I hope it helps you understand not only what you need to do, but also why you need to do it. I get really frustrated when I see code that has obviously been cut-and-pasted together from multiple examples by someone who doesn't actually understand what the code does. If you understand the code, it's a lot easier to adapt it to your needs. And hopefully, this will serve as a good reference for other people trying to figure out how to do similar tasks.

这篇关于d3中的力有向图的语义缩放的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆