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

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

问题描述

许多案例 已通过 SVG 几何缩放 显示用于强制定向图形几何缩放.

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

这是我的示例,在前面的案例之后进行了几何缩放.我有两个问题:

  1. 当我缩小,然后拖动整个图形时,图形会奇怪地消失.
  2. 使用相同的重绘功能

<块引用>

function zoom() {vis.attr("转换", 转换);}函数变换(d){返回 "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")";}

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

但我想做的是语义缩放.我试图修改缩放和变换功能,但不确定正确的方法.这里是我的尝试.我改变的功能:

<块引用>

function zoom() {节点调用(转换);//更新链接位置更新();}函数变换(d){//改变节点 x, y 的位置,不知道该放什么函数.}

<小时>

解决方案

我试图找到一个很好的教程链接,但找不到任何真正涵盖所有问题的内容,所以我打算写出来自己一步一步来.

首先,您需要清楚地了解您要实现的目标.这对于两种类型的缩放是不同的.我不太喜欢 Mike Bostock 引入的术语(它与术语的非 d3 用法并不完全一致)但我们不妨坚持使用它以与其他 d3 示例保持一致.

几何缩放"中,您正在缩放整个图像.圆圈和线条变得越来越大,也越来越远.SVG 有一种简单的方法可以通过transform"属性来实现这一点.当你在 SVG 元素上设置 transform="scale(2)" 时,它被绘制成好像所有东西都是两倍大.对于一个圆,它的半径被绘制成两倍大,它的 cxcy 位置被绘制成距 (0,0) 点距离的两倍.整个坐标系都发生了变化,因此一个单位现在等于屏幕上的两个像素,而不是一个.

同样,transform="translate(-50,100)" 改变整个坐标系,使坐标系的 (0,0) 点向左移动 50 个单位和 100 个单位从左上角向下(这是默认的原点).

如果您同时翻译缩放一个 SVG 元素,那么顺序很重要.如果翻译是before 比例,那么翻译是在原始单位.如果翻译比例之后,则翻译以比例单位为单位.

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

缩放事件被赋予一个比例因子(一个数字)和一个平移因子(一个由两个数字组成的数组),行为对象根据用户的动作计算出这些因子.你用这些数字做什么取决于你;他们不会直接改变任何东西.(除了将缩放附加到缩放行为功能时,如后所述.)

对于几何缩放,您通常所做的是在包含要缩放的内容的 <g> 元素上设置缩放和平移变换属性.此示例在由均匀放置的网格线组成的简单 SVG 上实现了几何缩放方法:
http://jsfiddle.net/LYuta/2/

缩放代码很简单:

function zoom() {console.log("zoom", d3.event.translate, d3.event.scale);vis.attr("转换",翻译("+ d3.event.translate +)"+ " 规模(" + d3.event.scale + ")");}

缩放是通过在vis"上设置变换属性来完成的,这是一个 d3 选择,包含一个 <g> 元素,它本身包含我们想要缩放的所有内容.平移和缩放因子直接来自 d3 行为创建的缩放事件.

结果是一切变得更大或更小——网格线的宽度以及它们之间的间距.这些行仍然具有 stroke-width:1.5; 但屏幕上 1.5 等于什么的定义已经更改,对于它们以及转换后的 <g> 元素中的任何其他内容.

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

在该示例中,您还应该注意许多其他事项:

  • 我已经使用 <修改了缩放行为对象code>.scaleExtent([min,max]) 方法.这对行为将在缩放事件中使用的比例值设置了限制,无论用户旋转滚轮多少.

  • 转换在 元素上,而不是 本身.这是因为 SVG 元素作为一个整体被视为一个 HTML 元素,并且具有不同的转换语法和属性.

  • 缩放行为附加到一个 不同 元素,该元素包含主要的 和背景矩形.背景矩形就在那里,即使鼠标或触摸不在一条线上,也可以观察到鼠标和触摸事件.<g> 元素本身没有任何高度或宽度,因此不能直接响应用户事件,它只能从其子元素接收事件.我把矩形留成黑色,这样你就可以知道它在哪里,但是你可以将它的样式设置为 fill:none;,只要你也将它设置为 pointer-events:all;.矩形不能被转换的,因为当你缩小时,响应缩放事件的区域也会缩小,并且可能会在 SVG 边缘看不见.

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

以下是相同的几何缩放方法,应用于简化版的力布局:
http://jsfiddle.net/cSn6w/5/

我减少了节点和链接的数量,并取消了节点拖动行为和节点展开/折叠行为,因此您可以专注于缩放.我还更改了摩擦"参数,以便图形停止移动需要更长的时间;在它仍在移动时放大它,您会看到一切都将像以前一样保持移动.

图像的几何缩放"相当简单,只需很少的代码即可实现,并且浏览器可以快速、平滑地进行更改.但是,您想要放大图形的原因通常是因为数据点靠得太近和重叠.在这种情况下,仅仅把所有东西都做大是无济于事的.您希望将元素拉伸到更大的空间,同时保持各个点的大小相同.这就是语义缩放"的用武之地.

图表的

语义缩放",在Mike Bostock 使用术语, 是在不缩放单个元素的情况下缩放图形的布局.(注意,对于其他上下文,语义缩放"还有其他解释.)

这是通过改变元素的位置计算方式以及连接对象的任何线或路径的长度来实现的,没有 更改定义像素大小的底层坐标系,以设置线宽或形状或文本的大小.

可以自己进行这些计算,使用平移和缩放值根据这些公式定位对象:

zoomedPositionX = d3.event.translate[0] + d3.event.scale * dataPositionXzoomedPositionY = 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){返回 d3.event.translate[0] + d*d3.event.scale;}).attr("y1", d3.event.translate[1]).attr(x2",函数(d){返回 d3.event.translate[0] + d*d3.event.scale;}).attr("y2", d3.event.translate[1] + h*d3.event.scale);

水平线的变化类似.结果?缩放时线条的位置和长度会发生变化,线条不会变粗或变细.

当我们尝试对力布局做同样的事情时,它会变得有点复杂.这是因为力布局图中的对象也在每次滴答"事件后重新定位.为了将它们定位在正确的缩放位置,刻度定位方法将不得不使用缩放位置公式.这意味着:

  1. 比例和平移必须保存在一个可以被tick函数访问的变量中;并且,
  2. 如果用户尚未缩放任何内容,则需要为刻度函数使用默认的缩放和平移值.

默认比例为1,默认平移为[0,0],代表正常比例,不平移.

以下是简化的力布局上的语义缩放:
http://jsfiddle.net/cSn6w/6/

缩放功能现在是

function zoom() {console.log("zoom", d3.event.translate, d3.event.scale);scaleFactor = d3.event.scale;翻译 = d3.event.translate;打钩();//更新位置}

它设置scaleFactor和translation变量,然后调用tick函数.刻度函数完成所有定位:在初始化时、强制布局刻度事件之后和缩放事件之后.看起来像

function tick() {linkLines.attr("x1", 函数 (d) {返回翻译[0] + scaleFactor*d.source.x;}).attr("y1", 函数 (d) {返回翻译[1] + scaleFactor*d.source.y;}).attr("x2", 函数 (d) {返回翻译[0] + scaleFactor*d.target.x;}).attr("y2", 函数 (d) {返回翻译[1] + scaleFactor*d.target.y;});nodeCircles.attr(cx",函数(d){返回翻译[0] + scaleFactor*d.x;}).attr(cy",函数(d){返回翻译[1] + scaleFactor*d.y;});}

圆圈和链接的每个位置值都通过平移和比例因子进行调整.如果这对您有意义,那么这对于您的项目应该足够了,您不需要使用比例.只要确保您始终使用此公式在数据坐标(dx和dy)和显示坐标(cx、cy、x1、x2等)之间进行转换即可用于定位对象.

如果您需要从显示坐标到数据坐标进行反向转换,这就变得复杂了.如果您希望用户能够拖动单个节点,则需要这样做——您需要根据被拖动节点的屏幕位置设置数据坐标.(请注意,这在您的两个示例中都无法正常工作).

对于几何缩放,可以使用d3.mouse().使用 d3.mouse(SVGElement) 计算鼠标在该 SVGElement 使用的坐标系中的位置.所以如果我们传入表示转换后的可视化的元素,它返回的坐标可以直接用于设置对象的位置.

可拖动的几何缩放力布局如下所示:
http://jsfiddle.net/cSn6w/7/

拖动功能为:

拖拽函数(d){if (d.fixed) 返回;//root是固定的//获取鼠标相对于可视化的坐标//坐标系:var 鼠标 = d3.mouse(vis.node());d.x = 鼠标 [0];d.y = 鼠标 [1];tick();//重新定位这个节点和任何链接}

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

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

变成

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

语义缩放示例的拖动功能因此是

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

可拖动语义缩放强制布局在此处实现:
http://jsfiddle.net/cSn6w/8/

这应该足以让您重回正轨.稍后我会回来添加对比例的解释以及它们如何使所有这些计算更容易.

...我回来了:

看看上面所有的数据到显示的转换函数,是不是让你觉得每次都有一个函数来做这件事不是更容易吗?"这就是 d3 scales 的用途:将数据值转换为显示值.

您不会经常在 force-layout 示例中看到 scales,因为 force layout 对象允许您直接设置宽度和高度,然后在该范围内创建 d.x 和 d.y 数据值.将布局宽度和高度设置为您的可视化宽度和高度,您可以直接使用数据值在显示中定位对象.

但是,当您放大图形时,您会从数据的整个范围可见切换为仅部分可见.所以数据值不再直接对应定位值,我们需要在它们之间进行转换.一个比例函数会让这件事变得更容易.

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

缩放时,域和范围之间的关系会发生变化,因此这些值之一必须在比例尺上发生变化.幸运的是,我们不必自己弄清楚公式,因为 D3 缩放行为会为我们计算——如果我们使用它的 .x 将缩放对象附加到缩放行为对象().y() 方法.

因此,如果我们将绘图方法更改为使用比例尺,那么我们在缩放方法中要做的就是调用绘图函数.

这是使用比例实现的网格示例的语义缩放:
http://jsfiddle.net/LYuta/5/

关键代码:

/*** 配置缩放行为 ***/var zoomer = d3.behavior.zoom().scaleExtent([0.1,10])//允许10倍放大或缩小.on(缩放",缩放)//定义事件处理函数.x(xScale).y(yScale);//附加秤,以便它们的域//会自动更新功能缩放(){console.log("zoom", d3.event.translate, d3.event.scale);//缩放行为已经改变//x 和 y 尺度的域//所以我们只需要使用它们重绘画线();}函数画线(){//将定位放在一个单独的函数中//也可以在初始化时调用vLines.attr("x1", function(d){返回 xScale(d);}).attr("y1", yScale(0) ).attr(x2",函数(d){返回 xScale(d);})/* 等等. */

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

这是语义缩放网格示例,使用比例进行响应:
http://jsfiddle.net/LYuta/9/

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

关键代码:

/* 根据SVG尺寸设置显示尺寸并重新绘制*/函数 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("宽度", svgW).attr("高度", svgH);//console.log(xScale.range(), yScale.range());画线();}//调整大小以适应窗口变化:window.addEventListener("resize", setSize, false)设置大小();//初始化宽度和高度

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

在 node-drag 事件中,因此使用 scales 的代码是:

拖拽函数(d){if (d.fixed) 返回;//root是固定的//获取鼠标相对于可视化的坐标//坐标系:var 鼠标 = d3.mouse(vis.node());d.x = xScale.invert(鼠标[0]);d.y = yScale.invert(鼠标[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 semantic 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.
}


解决方案

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天全站免登陆