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).
Block version.
path { pointer-events: all; }
parent.select(child).attr('d' function(d) { ..do stuff.. });
node.data(data, function(d) { return d.id; })
Thanks Andrew Reid for your help.
If the goal is:
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:
(
.nodes(graph.nodes)
in original )Instructing what to do on tick:
The ticked function:
( 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:
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:
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):
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.