d3.js - 如何使用 voronoi 多边形修改 d3 力布局以触发分组元素上的事件?

标签 d3.js drag force-layout voronoi clip-path

目标是结合d3力模拟、g元素和voronoi多边形,使节点上的触发事件更容易,例如拖动、鼠标悬停、工具提示等,可以用一个图形dynamically modified .这遵循 d3 Circle Dragging IV example .

在下面的代码中,当给g元素和clippath元素添加clip path属性时:

  • 为什么拖动不会在单元格上触发?
  • 为什么节点变得模糊并且
    路径在边缘失去样式?
  • 如何修复它以拖动节点并触发它们上的事件,例如鼠标悬停?


  • var data = [
      {
        "index" : 0,
          "vx" : 0,
            "vy" : 0,
              "x" : 842,
                "y" : 106
      },
        {
          "index" : 1,
            "vx" : 0,
              "vy" : 0,
                "x" : 839,
                  "y" : 56
        },
         {
            "index" : 2,
              "vx" : 0,
                "vy" : 0,
                  "x" : 771,
                    "y" : 72
          }
    ]
    
    var svg = d3.select("svg"),
        width = +svg.attr("width"),
        height = +svg.attr("height");
      
    var simulation = d3.forceSimulation(data)
    	.force("charge", d3.forceManyBody())
    	.force("center", d3.forceCenter(width / 2, height / 2))
    	.on("tick", ticked);
      
    var nodes = svg.append("g").attr("class", "nodes"),
        node = nodes.selectAll("g"),
        paths = svg.append("g").attr("class", "paths"),
        path = paths.selectAll("path");
    
    var voronoi = d3.voronoi()
    	.x(function(d) { return d.x; })
    	.y(function(d) { return d.y; })
    	.extent([[0, 0], [width, height]]);
      
    var update = function() {
    
      node = nodes.selectAll("g").data(data);
        var nodeEnter = node.enter()
      	.append("g")
      	.attr("class", "node")
        .attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; });
      nodeEnter.append("circle");
      nodeEnter.append("text")
        .text(function(d, i) { return i; });  
      node.merge(nodeEnter); 
      
      path = paths.selectAll(".path")
      .data(data)
      .enter().append("clipPath")
        .attr("id", function(d, i) { return "clip-" + i; })
        .append("path")
        .attr("class", "path");
      
      simulation.nodes(data);
      simulation.restart();
    
    }();
      
    function ticked() {
    	var node = nodes.selectAll("g");
      var diagram = voronoi(node.data()).polygons();
      
      paths.selectAll("path")
        .data(diagram)
        .enter()
        .append("clipPath")
        .attr("id", function(d, i) { return "clip-" + i; })
        .append("path")
        .attr("class", "path");
    
      paths.selectAll("path")
        .attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z"; });
      
      node.call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));  
    
      node
        .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")" });
    }
    
    function dragstarted(d) {
      if (!d3.event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }
    
    function dragged(d) {
      d.fx = d3.event.x;
      d.fy = d3.event.y;
    }
    
    function dragended(d) {
      if (!d3.event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    }
    svg {
      border: 1px solid #888888;  
    }
    
    circle {
      r: 3;
      cursor: move;
      fill: black;
    }
    
    .node {
      pointer-events: all;
    }
    
    path {
      fill: none;
      stroke: #999;
      pointer-events: all;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.1/d3.js"></script>
    <svg width="400" height="400"></svg>


    (单独的问题,但在圆形拖动 IV 元素中将路径嵌套在 g 元素中会导致不希望的路径定位到图形的一侧。)

    related question ,使用多边形而不是路径和剪辑路径,我可以让拖动工作,但我试图使用剪辑路径版本作为比较,并且不确定有什么区别,除了剪辑路径似乎是迈克博斯托克(d3 创建者)的首选.

    最佳答案

    如果目标是:

    is to combine d3 force simulation, g elements, and voronoi polygons to make trigger events on nodes easier, such as dragging, mouseovers, tooltips and so on with a graph that can be dynamically updated.



    我将从您的代码的细节中退一步,并尝试实现目标。在这次尝试中,我将使用两个主要来源(您引用的一个)(我这样做可能会偏离基础)。

    来源一:Mike Bostock's block circle dragging example .

    来源二:Mike Bostock's Force-directed Graph example .

    我希望这种方法至少有助于实现您的目标(我部分采用了它,因为我对您的代码段有困难)。作为一个最小的例子和概念证明,它应该是有用的。

    和你一样,我将使用圆拖动示例作为基础,然后我将尝试合并力导向示例。

    需要导入的力有向图的关键部分是定义模拟:
    var simulation = d3.forceSimulation()
    分配节点:
     simulation
          .nodes(circle)
          .on("tick", ticked);
    

    ( .nodes(graph.nodes) 原文 )

    指示在滴答上做什么:
    force.nodes(circles)
     .on('tick',ticked);
    

    勾选功能:
    function ticked() {
        circle.selectAll('circle')
            .attr("cx", function(d) { return d.x; })
            .attr("cy", function(d) { return d.y; });
      }
    

    (我们不需要链接部分,我们想要更新圆圈(而不是一个名为 node 的变量)

    以及属于拖动事件的部分。

    如果我们将所有这些导入到一个片段中(结合拖动事件,添加一个勾选的函数,我们得到:

    var svg = d3.select("svg"),
        width = +svg.attr("width"),
        height = +svg.attr("height"),
        radius = 32;
        
    var simulation = d3.forceSimulation()
        .force("charge", d3.forceManyBody())
    
    var circles = d3.range(20).map(function() {
      return {
        x: Math.round(Math.random() * (width - radius * 2) + radius),
        y: Math.round(Math.random() * (height - radius * 2) + radius)
      };
    });
    
    var color = d3.scaleOrdinal()
        .range(d3.schemeCategory20);
    
    var voronoi = d3.voronoi()
        .x(function(d) { return d.x; })
        .y(function(d) { return d.y; })
        .extent([[-1, -1], [width + 1, height + 1]]);
    
    var circle = svg.selectAll("g")
      .data(circles)
      .enter().append("g")
        .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));
    
    var cell = circle.append("path")
      .data(voronoi.polygons(circles))
        .attr("d", renderCell)
        .attr("id", function(d, i) { return "cell-" + i; });
    
    circle.append("clipPath")
        .attr("id", function(d, i) { return "clip-" + i; })
      .append("use")
        .attr("xlink:href", function(d, i) { return "#cell-" + i; });
    
    circle.append("circle")
        .attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; })
        .attr("r", radius)
        .style("fill", function(d, i) { return color(i); });
        
    simulation
        .nodes(circles)
        .on("tick", ticked);
    
    function ticked() {
      circle.selectAll('circle')
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
    }
    
    function dragstarted(d) {
      d3.select(this).raise().classed("active", true);
      if (!d3.event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }
    
    function dragged(d) {
      d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
      cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
      d.fx = d3.event.x;
      d.fy = d3.event.y;
    }
    
    function dragended(d, i) {
      d3.select(this).classed("active", false);
      if (!d3.event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    }
    
    function renderCell(d) {
      return d == null ? null : "M" + d.join("L") + "Z";
    }
    path {
      pointer-events: all;
      fill: none;
      stroke: #666;
      stroke-opacity: 0.2;
    }
    
    .active circle {
      stroke: #000;
      stroke-width: 2px;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
    <svg width="600" height="400"></svg>


    显而易见的问题是,除非有拖拽,否则单元格不会更新。为了解决这个问题,我们只需要获取拖动时更新单元格的行并将其放入勾选函数中,以便它在勾选时更新:

    var svg = d3.select("svg"),
        width = +svg.attr("width"),
        height = +svg.attr("height"),
        radius = 32;
        
    var simulation = d3.forceSimulation()
        .force("charge", d3.forceManyBody())
    
    var circles = d3.range(20).map(function() {
      return {
        x: Math.round(Math.random() * (width - radius * 2) + radius),
        y: Math.round(Math.random() * (height - radius * 2) + radius)
      };
    });
    
    var color = d3.scaleOrdinal()
        .range(d3.schemeCategory20);
    
    var voronoi = d3.voronoi()
        .x(function(d) { return d.x; })
        .y(function(d) { return d.y; })
        .extent([[-1, -1], [width + 1, height + 1]]);
    
    var circle = svg.selectAll("g")
      .data(circles)
      .enter().append("g")
        .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));
    
    var cell = circle.append("path")
      .data(voronoi.polygons(circles))
        .attr("d", renderCell)
        .attr("id", function(d, i) { return "cell-" + i; });
    
    circle.append("clipPath")
        .attr("id", function(d, i) { return "clip-" + i; })
      .append("use")
        .attr("xlink:href", function(d, i) { return "#cell-" + i; });
    
    circle.append("circle")
        .attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; })
        .attr("r", radius)
        .style("fill", function(d, i) { return color(i); });
    
    circle.append("text")
        .attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
        .attr("x", function(d) { return d.x; })
        .attr("y", function(d) { return d.y; })
        .attr("dy", '0.35em')
        .attr("text-anchor", function(d) { return 'middle'; })
        .attr("opacity", 0.6)
        .style("font-size", "1.8em")
        .style("font-family", "Sans-Serif")
        .text(function(d, i) { return i; })
        
    simulation
        .nodes(circles)
        .on("tick", ticked);
    
    function ticked() {
      circle.selectAll('circle')
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
    
      circle.selectAll('text')
        .attr("x", function(d) { return d.x; })
        .attr("y", function(d) { return d.y; });
        
      cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
    }
    
    function dragstarted(d) {
      d3.select(this).raise().classed("active", true);
      if (!d3.event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }
    
    function dragged(d) {
      d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
      cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
      d.fx = d3.event.x;
      d.fy = d3.event.y;
    }
    
    function dragended(d, i) {
      d3.select(this).classed("active", false);
      if (!d3.event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    }
    
    function renderCell(d) {
      return d == null ? null : "M" + d.join("L") + "Z";
    }
    path {
      pointer-events: all;
      fill: none;
      stroke: #666;
      stroke-opacity: 0.2;
    }
    
    .active circle {
      stroke: #000;
      stroke-width: 2px;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
    <svg width="600" height="400"></svg>


    更新:更新节点:

    添加和删​​除节点至少对我来说是复杂的。主要问题是上面的代码在拖动事件上使用 d3.selection.raise() 重新排列了 svg 组,如果仅使用数据元素增量,这可能会弄乱我的剪辑路径排序。同样,从数组中间删除项目,这会导致单元格、组和圆圈之间的配对问题。这种配对是主要的挑战——同时确保任何附加节点都在正确的父节点中并以正确的顺序排列。

    为了解决配对问题,我在数据中使用了一个新属性作为标识符,而不是增量。其次,我在添加时对单元格进行了一些特定操作:确保它们位于正确的父级中,并且单元格出现在 DOM 中的圆圈上方(使用 d3.selection.lower())。

    注意:我还没有找到一个很好的方法来删除一个圆圈并使 voronoi 保持在一个典型的更新周期中工作,所以我刚刚为每次删除重新创建了 - 据我所知,Voronoi 每次滴答都会重新计算,这应该不是问题。

    结果是(点击删除/添加,点击按钮切换删除/添加):

    var svg = d3.select("svg"),
        width = +svg.attr("width"),
        height = +svg.attr("height"),
        radius = 32;
    
    var n = 0;
    var circles = d3.range(15).map(function() {
      return {
    	n: n++,
        x: Math.round(Math.random() * (width - radius * 2) + radius),
        y: Math.round(Math.random() * (height - radius * 2) + radius)
      };
    });
    
    // control add/remove
    var addNew = false;
    d3.select('#control').append('input')
    	.attr('type','button')
    	.attr('value', addNew ? "Add" : "Remove")
    	.on('click', function(d) {
    		addNew = !addNew;
    		d3.select(this).attr('value', addNew ? "Add" : "Remove")
    		d3.selectAll('g').on('click', (addNew) ? add : remove);
    	});
    	
    
    var color = d3.scaleOrdinal()
        .range(d3.schemeCategory20);
    
    var voronoi = d3.voronoi()
        .x(function(d) { return d.x; })
        .y(function(d) { return d.y; })
        .extent([[-1, -1], [width + 1, height + 1]]);
    
    var circle = svg.selectAll("g")
      .data(circles)
      .enter().append("g")
      .attr('id',function(d) { return 'g-'+d.n })
      .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended))
      .on('click', (addNew) ? add : remove);
    
    var cell = circle.append("path")
      .data(voronoi.polygons(circles))
        .attr("d", renderCell)
    	.attr("class","cell")
        .attr("id", function(d) {  return "cell-" + d.data.n; });
    
    circle.append("clipPath")
        .attr("id", function(d) { return "clip-" + d.n; })
      .append("use")
        .attr("xlink:href", function(d) { return "#cell-" + d.n; });
    
    
    circle.append("circle")
        .attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; })
        .attr("r", radius)
        .style("fill", function(d) { return color(d.n); });
    	
    circle.append("text")
        .attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
        .attr("x", function(d) { return d.x; })
        .attr("y", function(d) { return d.y; })
        .attr("dy", '0.35em')
        .attr("text-anchor", function(d) { return 'middle'; })
        .attr("opacity", 0.6)
        .style("font-size", "1.8em")
        .style("font-family", "Sans-Serif")
        .text(function(d) { return d.n; })
    	
    var simulation = d3.forceSimulation()
       .nodes(circles)
       .force('charge', d3.forceManyBody());
       	
    
    simulation.nodes(circles)
     .on('tick',ticked);
     
         
    function ticked() {
    circle.selectAll('circle')
      .attr("cx", function(d) { return d.x; })
      .attr("cy", function(d) { return d.y; })
      
    circle.selectAll('text')
      .attr("x", function(d) { return d.x; })
      .attr("y", function(d) { return d.y; });  
    
    cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
    
    }
    
    function dragstarted(d) {
      if (!d3.event.active) simulation.alphaTarget(0.3).restart();
    	d.fx = d.x;
    	d.fy = d.y;
    }
    
    function dragged(d) {
      d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
      cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
      d.fx = d3.event.x;
      d.fy = d3.event.y;
      
      
    }
    
    function dragended(d) {
     if (!d3.event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    }
    
    
    
    
    function remove () {
    
    	d3.select(this).raise(); 
    	var id = d3.select(this).attr('id').split('-')[1];
    	id = +id;
    	
    	// Get the clicked item:
    	var index = circles.map(function(d) {
    		return d.n;
    	}).indexOf(id);
    	
    	circles.splice(index,1);
    	
    	// Update circle data:
    	var circle = svg.selectAll("g")
    	  .data(circles);
    	  
    	circle.exit().remove();
    	circle.selectAll("clipPath").exit().remove();
    	circle.selectAll("circle").exit().remove();
    	circle.selectAll("text").exit().remove();
    
    	//// Update voronoi:
    	d3.selectAll('.cell').remove();
    	cell = circle.append("path")
    	  .data(voronoi.polygons(circles))
    	  .attr("d", renderCell)
    	  .attr("class","cell")
    	  .attr("id", function(d) {  return "cell-" + d.data.n; });
    	
    	simulation.nodes(circles)
    		.on('tick',ticked);
    }
    
    function add() {
    	// Add circle to circles:
    	var coord = d3.mouse(this);
    	var newIndex = d3.max(circles, function(d) { return d.n; }) + 1;
    	circles.push({x: coord[0], y: coord[1], n: newIndex });
    	
    	// Enter and Append:	
    	circle = svg.selectAll("g").data(circles).enter()
    	
    	var newCircle = circle.append("g")
    	  .attr('id',function(d) { return 'g-'+d.n })
    	  .call(d3.drag()
    			.on("start", dragstarted)
    			.on("drag", dragged)
    			.on("end", dragended))
    	  .on('click',add)
    
    	cell = circle.selectAll("path")
    		.data(voronoi.polygons(circles)).enter();
    		
    	cell.select('#g-'+newIndex).append('path')
    	  .attr("d", renderCell)
    	  .attr("class","cell")
    	  .attr("id", function(d) { return "cell-" + d.data.n; });
    
    	newCircle.data(circles).enter();
    	
    	newCircle.append("clipPath")
    		.attr("id", function(d) { return "clip-" + d.n; })
    	    .append("use")
    		.attr("xlink:href", function(d) { return "#cell-" + d.n; });
    
    	newCircle.append("circle")
    		.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
    		.attr("cx", function(d) { return d.x; })
    		.attr("cy", function(d) { return d.y; })
    		.attr("r", radius)
    		.style("fill", function(d) { return color(d.n); });
    		
    	newCircle.append("text")
          .attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
          .attr("x", function(d) { return d.x; })
          .attr("y", function(d) { return d.y; })
          .attr("dy", '0.35em')
          .attr("text-anchor", function(d) { return 'middle'; })
          .attr("opacity", 0.6)
          .style("font-size", "1.8em")
          .style("font-family", "Sans-Serif")
          .text(function(d) { return d.n; })
    	  
    	cell = d3.selectAll('.cell');
    		
    	d3.select("#cell-"+newIndex).lower(); // ensure the path is above the circle in svg.
    	
    	simulation.nodes(circles)
    	  .on('tick',ticked);
    
    }
    
    function renderCell(d) {
      return d == null ? null : "M" + d.join("L") + "Z";
    }
    .cell {
      pointer-events: all;
      fill: none;
      stroke: #666;
      stroke-opacity: 0.2;
    }
    
    .active circle {
      stroke: #000;
      stroke-width: 2px;
    }
    
    svg {
      background: #eeeeee;
    }
    <script src="https://d3js.org/d3.v4.min.js"></script>
    
    <div id="control"> </div>
    <svg width="960" height="500"></svg>


    就您问题的特定部分而言,我发现您问题的前两个项目符号中的拖动和剪辑路径问题主要是配对剪辑路径、单元格和圆圈以及找到添加的正确方式的问题图表的新元素 - 我希望我在上面演示过。

    我希望这是最后一个片段更接近您遇到的具体问题,我希望上面的代码是清晰的 - 但它可能从清晰简洁的 Bostockian 到其他一些较低的标准。

    关于d3.js - 如何使用 voronoi 多边形修改 d3 力布局以触发分组元素上的事件?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42330163/

    相关文章:

    javascript - 防止图像拖动选择

    javascript - 具有特定目标目的地的 D3js 强制模拟

    javascript - D3.js 让节点出现在没有 "bounce"的中间

    javascript - typescript 和 d3 版本 v3 中的可折叠树

    javascript - 如何根据函数参数在 D3 中添加半圆?

    d3.js - 使用d3 js中的对角线函数在两点之间绘制曲线

    javascript - D3.js 可折叠力布局 : Links are not being generated

    jquery - ClientWidth 和 ClientHeight 未定义

    ios - iOS 发生冲突后,如何移动多个 View ?

    wpf - 用拇指拖动 wpf 窗口 : can it be transparent?