4

The goal 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 modified. This follows the d3 Circle Dragging IV example.

In the following code, when adding the clip path attribute to the g element and clippath elements:

  • Why does dragging not trigger on the cells?
  • Why do the nodes become obscured and the paths lose their styles on edges?
  • How can this be fixed to drag the nodes and trigger events on them like mouseovers?

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>

(Separate question, but nesting the paths in the g elements as in the Circle Dragging IV element causes undesired positioning of the paths off to the side of the graph.)

In a related question, using polygons instead of paths and clippaths, I can get the dragging to work, but am trying to use the clippath version as a comparison and not sure what are the differences, other than clippath seems to be preferred by Mike Bostock (d3 creator).

Community
  • 1
  • 1
Cliff Coulter
  • 402
  • 7
  • 20

2 Answers2

4

If the goal is:

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.

I'm going to step back a bit from the specifics of your code and try to get to the goal. I will use two primary sources (one which you reference) in this attempt to get there (and I may be way off base in doing so).

Source one: Mike Bostock's block circle dragging example.

Source two: Mike Bostock's Force-directed Graph example.

I hope that this approach at least helps to get to your goal (I took it partly as I was having difficulty with your snippet). It should be useful as a minimal example and proof of concept.


As with you, I'll use the circle dragging example as the foundation, and then I'll try to incorporate the force-directed example.

The key portions of the force directed graph that need to be imported are defining the simulation:

var simulation = d3.forceSimulation()

Assigning the nodes:

 simulation
      .nodes(circle)
      .on("tick", ticked);

( .nodes(graph.nodes) in original )

Instructing what to do on tick:

force.nodes(circles)
 .on('tick',ticked);

The ticked function:

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

( we don't need the link portion, and we want to update the circles (rather than a variable named node )

And the portions that fall in the drag events.

If we import all that into a snippet (combining drag events, adding a ticked function, and we get:

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>

The obvious problem is that the cells don't update unless there is a drag. To solve this we just need to take the line that updates the cells on drag and put it in the ticked function so it updates on 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>

update: updating nodes:

Adding and removing nodes is where it got complicated for me at least. The primary issue was that the code above rearranged the svg groups with d3.selection.raise() on drag events, which could mess up my clip path ordering if using only the data element increment. Likewise with removing items from within the middle of the array, this would cause pairing issues between cells, groups, and circles. This pairing was the primary challenge - along with ensuring any appended nodes were in the proper parent and in the right order.

To solve the pairing issues, I used a new property in the data to use as an identifier, rather than the increment. Secondly, I do a couple specific manipulations of the cells when adding: ensuring they are in the right parent and that the cell appears above the circle in the DOM (using d3.selection.lower()).

Note: I haven't managed a good way to remove a circle and keep the voronoi working with a typical update cycle, so I've just recreated for each removal - and since as far as I know the Voronoi is recalculated every tick, this shouldn't be an issue.

The result is (click to remove/add, click the button to toggle remove/add):

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>

In terms of specific parts of your question, I found that the dragging and clip path issues in the first two bullets of your question were a largely problem of pairing clip paths, cells, and circles as well as finding the right manner in which to add new elements to the chart - which I hope I demonstrated above.

I hope this is last snippet is closer to the specific problems you were encountering, and I hope that the code above is clear - but it likely went from the clear and concise Bostockian to some other lower standard.

Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • Thank you for your work put into this and the related question Andrew (would never have suspected the css fill:none part). One required piece of the puzzle is the update function - there are going to be nodes (and links) that are dynamically inserted and removed; sorry if that wasn't clear. With the way you've solved the clip path problem may make the update function irrelevant. Part of the confusion for me is how to bind and scope nodes inside/outside of the tick and update functions. Going to step through it this weekend, and hopefully find a new angle to the problem from your work. – Cliff Coulter Feb 25 '17 at 12:59
  • I was thinking this may be a little off, but I figured it couldn't hurt. It's a great question and its fun toying with possible solutions. I'll see what I can add to it in terms of links or updates. – Andrew Reid Feb 25 '17 at 18:03
  • I think you've solved it, just not sure how yet ;) You've got the grouped elements, so it should with an additional text sibling. The transform in the above was to move the group of elements which seemed to not work properly when the drag was on the circle. Working with what you've done as well, to use it with update/enter/remove. Hoping to have a real time updating graph, that will display text or a bubble of data on hover, and draggable, using the voronoi for ease. Sorry, I know it's a tall order bringing all the separate functions into one. – Cliff Coulter Feb 25 '17 at 19:52
  • Here's a modified fiddle of your solution with text element added: https://jsfiddle.net/dunderjeep/56x7z2yy/3/ - the text elements are positioned off of the svg. The idea is that they would 'follow' the circles. But if the grouped elements are transformed with the clip elements, then the voronoi is calculated against the group position and are all messed up : https://jsfiddle.net/dunderjeep/56x7z2yy/5/. So it's trying to synthesize these two features. Clear as mud? – Cliff Coulter Feb 25 '17 at 20:19
  • Got the text to 'follow' along, by selecting and positioning it in a separate call : `circle.selectAll('text') .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")" }); `' [here](https://jsfiddle.net/dunderjeep/56x7z2yy/7/) – Cliff Coulter Feb 26 '17 at 14:37
  • Now, just to add the update function to complete it and I think we're good. – Cliff Coulter Feb 26 '17 at 14:46
  • @CliffCoulter I edited Andrew's excellent answer, to show how to add text. Andrew, hope you don't mind. – meetamit Feb 26 '17 at 21:34
  • My attempts at an update function are partially successful, I've been a little preoccupied, but should have something tonight. – Andrew Reid Feb 26 '17 at 21:53
  • I've got a update function for adding/removing in the answer now. Surprisingly challenging, it is probably not as elegant or efficient as possible, but *should* be helpful, maybe? It's too early for me to think straight, so I hope the explanation works, I'll edit it later today if needed, but I'm on the road for a bit today. – Andrew Reid Feb 27 '17 at 17:08
  • There may be one glitch on the removal end, I can't reliably reproduce it, will be re-evaluating later today. – Andrew Reid Feb 27 '17 at 17:23
  • Great work! Relieved I'm not the only one who found this challenging. And very helpful - the dent in my forehead is grateful not to be banging against the wall :) You've got this; I'm just going to leave it up to incubate a little longer and need more time to work through it. – Cliff Coulter Feb 28 '17 at 01:58
  • Thanks, I got a bit of a dent in the forehead too trying to understand what appeared to be nonsensical changes to the visualization. Once I finally started to see what was going wrong in the DOM it started to make more sense. Challenging for sure, but it's a rewarding and innovative visualization type. I'd love to see what others take on the problem is, so please do leave it up. That's why I posted a partial answer at first, to see if others would join in and give it a go. I think force links might be fairly straight forward, I'll see what I can do but I'm away from my computer for a few days. – Andrew Reid Feb 28 '17 at 02:47
  • I've got another working example [here](https://jsfiddle.net/dunderjeep/ouuy1606/6/) using source: [Modifying Force Layout](https://bl.ocks.org/mbostock/1095795), and referencing id's for the nodes eliminates the indexing issue. Will be following up with a fleshed out example later. – Cliff Coulter Mar 04 '17 at 14:30
  • [Here's](https://jsfiddle.net/dunderjeep/ouuy1606/) the example with mouseover events, making the node group active so visibility or attributes on the nested elements can be determined in the css. Evidently, clip path isn't necessary to accomplish the goal. Thank you for helping me see how to select the right elements, and pointing out the pointer events which I never would have checked. – Cliff Coulter Mar 04 '17 at 15:31
  • Looks nice, glad to see the links in there. And that indexing issue - so much trouble because of it. – Andrew Reid Mar 04 '17 at 16:54
  • Yeah, sorry that the indexes and clip paths were misleading (they confused me too). I'm going to edit the question, and answer it to bring it closer to the true intentions, to hopefully make it more clear, searchable, and helpful to others with similar intentions but the bounty and credit goes to you for putting it on the right track. – Cliff Coulter Mar 04 '17 at 17:04
  • Don't apologize, was rewarding to track that down and find a solution – Andrew Reid Mar 04 '17 at 17:09
2

Block version.

  • Why does dragging not trigger on the cells?
    • Because if the cell attribute has fill:none, then it must have pointer-events:all.
  • Why do the nodes become obscured and the paths lose their styles on edges?
    • Because the clip path was targeting the g elements position instead of the circles position.
  • How can this be fixed to drag the nodes and trigger events on them like mouseovers?
    • use path attr pointer-events: all, path { pointer-events: all; }
    • select the desired child element such as circle, or text, in the drag or tick event for positioning parent.select(child).attr('d' function(d) { ..do stuff.. });
    • use node id's for references to simplify data array updates or deletions node.data(data, function(d) { return d.id; })

Thanks Andrew Reid for your help.

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height"),
    color = d3.scaleOrdinal(d3.schemeCategory10);

var a = {id: "a"},
    b = {id: "b"},
    c = {id: "c"},
    data = [a, b, c],
    links = [];

var simulation = d3.forceSimulation(data)
    .force("charge", d3.forceManyBody().strength(-10))
    .force("link", d3.forceLink(links).distance(200))
  .force("center", d3.forceCenter(width / 2, height / 2))
    .alphaTarget(1)
    .on("tick", ticked);

var link = svg.append("g").attr("class", "links").selectAll(".link"),
    node = svg.append("g").attr("class", "nodes").selectAll(".node");
    
var voronoi = d3.voronoi()
 .x(function(d) { return d.x; })
 .y(function(d) { return d.y; })
 .extent([[-1, 1], [width + 1, height + 1]]);

update();

d3.timeout(function() {
  links.push({source: a, target: b}); // Add a-b.
  links.push({source: b, target: c}); // Add b-c.
  links.push({source: c, target: a}); // Add c-a.
  update();
}, 1000);

d3.interval(function() {
  data.pop(); // Remove c.
  links.pop(); // Remove c-a.
  links.pop(); // Remove b-c.
  update();
}, 5000, d3.now());

d3.interval(function() {
  data.push(c); // Re-add c.
  links.push({source: b, target: c}); // Re-add b-c.
  links.push({source: c, target: a}); // Re-add c-a.
  update();
}, 5000, d3.now() + 1000);

function update() {

  node = node.data(data, function(d) { return d.id; });
  node.exit().remove();
  var nodeEnter = node.enter().append("g")
   .attr("class", "node")
    .on("mouseover", mouseover)
    .on("mouseout", mouseout);
  nodeEnter.append("circle").attr("fill", function(d) { return color(d.id); }).attr("r", 8);
  nodeEnter.append("text")
    .attr("dx", 12)
    .attr("dy", ".35em")
   .text(function(d) { return d.id; });
  nodeEnter.append("path").attr("class", "path");
  nodeEnter.call(d3.drag()
                 .on("start", dragstarted)
                 .on("drag", dragged)
                 .on("end", dragended));
  node = node.merge(nodeEnter);


  // Apply the general update pattern to the links.
  link = link.data(links, function(d) { return d.source.id + "-" + d.target.id; });
  link.exit().remove();
  link = link.enter().append("line").merge(link);

  // Update and restart the simulation.
  simulation.nodes(data);
  simulation.force("link").links(links);
  simulation.alpha(1).restart();
}

function mouseover(d) {
 d3.select(this).raise().classed("active", true);
}

function mouseout(d) {
 d3.select(this).raise().classed("active", false);
}

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.fx = d3.event.x).attr("cy", d.fy = d3.event.y);
}

function dragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}

function ticked() {
  node.select("circle")
   .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });
    
  node.select("path")
   .data(voronoi.polygons(data))
    .attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z"; });
    
  node.select("text")
   .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")" });

  link.attr("x1", function(d) { return d.source.x; })
      .attr("y1", function(d) { return d.source.y; })
      .attr("x2", function(d) { return d.target.x; })
      .attr("y2", function(d) { return d.target.y; });
}
path {
  pointer-events: all;
  fill: none;
  stroke: #666;
  stroke-opacity: 0.2;
}

.active path {
  fill: #111;  
  opacity: 0.05;
}

.active text {
  visibility: visible;
}

.active circle {
  stroke: #000;
  stroke-width: 1.5px;
}

svg {
  border: 1px solid #888;  
}

.links {
  stroke: #000;
  stroke-width: 1.5;
}

.nodes {
  stroke-width: 1.5;
}

text {
  pointer-events: none;
  font: 1.8em sans-serif;
  visibility: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="400" height="400"></svg>
Cliff Coulter
  • 402
  • 7
  • 20