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

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

在以下代码中,将剪辑路径属性添加到g元素和clippath元素时:

>为什么拖动不会触发细胞?
>为什么节点变得模糊不清
路径在边缘失去了它们的风格?
>如何解决此问题以拖动节点并触发事件,如鼠标悬停?

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中,使用多边形而不是路径和clippaths,我可以让拖动工作,但我试图使用clippath版本作为比较并且不确定有什么区别,除了clippath似乎是Mike Bostock的首选( 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)in original)

指示在刻度上做什么:

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>

显而易见的问题是,除非存在阻力,否则单元格不会更新.要解决这个问题,我们只需要在拖动时更新单元格的行,并将其放在勾选的函数中,以便在tick上更新:

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力布局以触发分组元素上的事件? - 代码日志