d3中力有向图的语义缩放

Posted

技术标签:

【中文标题】d3中力有向图的语义缩放【英文标题】:semantic zooming of the force directed graph in d3 【发布时间】:2014-02-16 03:15:42 【问题描述】:

SVG Geometric Zooming 已经展示了许多 cases 用于力导向图几何缩放。

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

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

    当我缩小,然后拖动整个图形时,图形会奇怪地消失。 使用相同的重绘函数
function zoom() 
  vis.attr("transform", transform);

function transform(d)
  return "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")";

这只会更新一个 svg 元素的“transform”属性。但是如何实现改变节点位置的功能呢?

但我想做的是semantic zooming。我曾尝试修改缩放和变换功能,但不确定正确的做法。 Here 是我尝试的。我改变的功能:

function zoom() 
  node.call(transform);
  // update link position
  update();

function transform(d)
  // change node x, y position, not sure what function to put here.


【问题讨论】:

您的“几何缩放”示例的主要问题是您的边距对于 JSFiddle 来说太大了,所以您的 SVG 是屏幕的一半。转换每个单独的元素而不是 <g> 也是低效的,但这是一个较小的问题。 您尝试“语义缩放”有很多问题: 1)您甚至没有尝试更新链接的位置。 2)您没有使用缩放,因此缩放无法直接更新任何内容,但您没有使用事件处理程序中的缩放事件。 3)相反,对于节点,您正在根据数据中它们现有的原始位置进行翻译。 4)由于节点的初始位置设置为cx和cy,所以平移是在原始位置之上添加而不是替换它。 从一个更简单的示例开始,然后逐步构建,确保您了解每一步。 @AmeliaBR 我不太明白example。为什么需要 .call(d3.behavior.zoom().x(x).y(y).scaleExtent([1, 8]).on("zoom", zoom)) 的缩放函数,如果位置已经在那个范围内了?在这里,他使用原始位置进行翻译。我只是试图模仿这个过程。我不确定如何根据 d3.event.translate 更新 x、y。我用缩放功能和更新链接更新了我的代码。请检查。 【参考方案1】:

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

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

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

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

如果您同时翻译 缩放 SVG 元素,则顺序很重要。如果 translate 是 before 比例,则翻译采用原始单位。如果 translate 是 after scale,则平移是按比例缩放的单位。

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

缩放事件被赋予一个比例因子(一个数字)和一个平移因子(一个由两个数字组成的数组),行为对象根据用户的移动计算得出。您如何处理这些数字取决于您; 它们不会直接改变任何东西(除了将比例附加到缩放行为函数时,如下所述。)

对于几何缩放,您通常会在包含您要缩放的内容的<g> 元素上设置缩放和平移变换属性。此示例在由均匀放置的网格线组成的简单 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 属性来完成的,这是一个 d3 选择,包含一个 <g> 元素,它本身包含我们想要缩放的所有内容。平移和缩放因子直接来自 d3 行为创建的缩放事件。

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

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

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

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

转换位于 <g> 元素上,而不是 <svg> 本身。这是因为 SVG 元素作为一个整体被视为 html 元素,并且具有不同的转换语法和属性。

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

可以通过将缩放行为直接附加到 SVG 对象来跳过矩形和第二个 <g> 元素,如 this version of the fiddle。但是,您通常不希望 整个 SVG 区域上的事件触发缩放,因此最好了解如何以及为何使用背景矩形选项。

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

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

图像的“几何缩放”相当简单,它可以用很少的代码实现,它可以让浏览器快速、平滑地进行更改。但是,您想要放大图表的原因通常是因为数据点太靠近且重叠。在这种情况下,只是让一切都变大并没有帮助。您希望将元素拉伸到更大的空间,同时保持各个点的大小相同。这就是“语义缩放”应运而生的地方。

图形的“语义缩放”,在Mike Bostock uses the term 的意义上,是在不缩放单个元素的情况下缩放图形的布局(请注意,对于其他上下文,“语义缩放”还有其他解释。)

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

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

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])
    .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,默认平移为[0,0],表示正常比例,不平移。

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

现在是缩放功能

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

它设置scaleFactor和translation变量,然后调用tick函数。 tick 函数执行所有定位:在初始化时、在 force-layout 刻度事件之后和缩放事件之后。好像

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

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

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

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

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

拖动功能是:

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

但是,对于语义缩放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

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

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

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

...我回来了:

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

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

但是,当您放大图表时,您会从显示整个数据范围切换到仅显示部分数据。所以数据值不再直接对应定位值,我们需要在它们之间进行转换。一个缩放函数会让这更容易。

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

当您缩放时,域和范围之间的关系会发生变化,因此这些值中​​的一个必须在比例上发生变化。幸运的是,我们不必自己计算公式,因为 D3 缩放行为会为我们计算它 - 如果我们使用其 .x() 和 @ 将缩放对象附加到缩放行为对象987654372@ 方法。

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

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

关键代码:

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

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

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

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

关键代码:

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

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

因此,在节点拖动事件中,使用比例尺的代码是:

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

其余的语义缩放强制布局示例,通过比例做出响应在这里:http://jsfiddle.net/cSn6w/10/


我相信这次讨论比您预期的要长得多,但我希望它不仅可以帮助您了解您需要做什么,还可以帮助您了解为什么 你需要这样做。当我看到代码显然是由一个实际上并不了解代码功能的人从多个示例中剪切并粘贴在一起的时,我感到非常沮丧。如果您了解代码,那么根据您的需要调整它会容易得多。希望这将为其他试图弄清楚如何执行类似任务的人提供一个很好的参考。

【讨论】:

谢谢!对于“简化力布局上的语义缩放”,为什么不直接在 zoom 函数中更改节点 x,y 属性,而不是在 tick() 函数中更改位置? 避免代码重复。每当您多次执行某项操作时,请尝试仅将其写出一次,然后在需要时使用该功能。我本可以将tick() 的名称更改为更有意义的名称,例如redraw(),但结构相同。每次 force-layout 发出刻度事件,或缩放行为发出缩放事件时,所有元素都需要根据 both 它们当前的 dx 和 dy and 定位当前缩放参数。对于拖动事件,更新所有内容比查找需要更新的链接更容易(如果有点额外的话)。 zoom.translate 究竟返回了什么?在 d3 中,它说“指定当前缩放平移向量。如果未指定,则返回当前平移向量,默认为 [0, 0].'。但是在哪一步指定了平移向量呢? d3.event.translated3.event.scale 的值都是在创建缩放事件对象时设置在缩放行为对象内部的。如果您想在程序中直接更改图像的缩放或比例(即,不响应用户缩放操作),那么您需要告诉缩放行为要使用什么平移和缩放,因此zoom.translate()。例如,如果用户从列表中选择了一个城市名称,您可能希望缩放地图以关注该城市。否则,您不会直接设置值。 可以看d3.mouse或者d3.behavior.zoom的源码。【参考方案2】:

您必须同时转换节点并重绘路径。

“语义缩放”的想法是您更改布局的比例,而不是单个元素的大小。

如果您在链接示例中设置了缩放行为,它会自动为您更新 x 和 y 比例。然后根据这些比例重新设置节点的位置,也可以重新设置链接的位置和形状。

如果您的链接是直线,请使用更新后的 x 和 y 比例重新设置 x1、y1、x2 和 y2 位置。如果您的链接是使用 d3.svg.diagonal 和 x 和 y 比例创建的路径,请使用相同的函数重新设置“d”属性。

如果您需要更具体的说明,您必须发布您的代码。

【讨论】:

请查看新帖。

以上是关于d3中力有向图的语义缩放的主要内容,如果未能解决你的问题,请参考以下文章

在 D3 强制布局链接标记上缩放箭头

D3 强制有向图 ajax 更新

d3:强制有向图:节点过滤

D3.js 强制有向图,通过使边缘相互排斥来减少边缘交叉

D3 具有非树数据的可折叠力有向图 - 链接对齐

将由多条线组成的标签垂直居中于 D3 力有向图中的节点上