Reputation: 21
I am trying to create a force directed graph using d3. I got it working following this example for the enter case, so loading the code for the first time. But when I tried to incorporate some interactivity by (in this case) clicking a node, I noticed that the old nodes where not exiting correctly. I've looked at other examples and even questions like this one but still am unable to decypher what is wrong. Thanks in advance.
Index.js:
let globaldata = null;
let graphSvg = d3.select('svg');
let directedGraph = null;
let selectedNode = "construct";
function render() {
let data = globaldata;
//Directed Graph
graphSvg
.attr("height", 500)
.attr("width", 750);
let graphMargin = {
top: 50,
right: 100,
bottom: 100,
left: 80
};
let graphNodes = [];
let graphLinks = [];
let linkData = d3.nest()
.key(function(d) {
return d.callerType;
})
.key(function(d) {
return d.taskType;
}).entries(data);
linkData.shift();
linkData.forEach(element => {
let node = {
id: element.key
};
graphNodes.push(node);
});
console.log(JSON.parse(JSON.stringify(graphNodes)));
linkData.forEach(element => {
let sourceK = element.key;
let targetK = element.values[0].key;
graphLinks.push({
source: graphNodes.find(sourceN => sourceN.id == sourceK),
target: graphNodes.find(targetN => targetN.id == targetK),
amount: element.values[0].values.length
});
});
console.log(linkData);
console.log(graphLinks);
if (!directedGraph) {
directedGraph = new DirectedGraph(graphSvg, graphNodes, graphLinks, nodeClick, graphMargin);
} else {
directedGraph.render(graphNodes, graphLinks);
}
};
function nodeClick(id) {
selectedNode = id;
console.log("Selected Node:")
console.log(selectedNode)
render();
}
d3.csv('data/daten.csv', function(d) {
return {
taskID: d['TaskID'],
taskType: d['TaskType'],
start: +d['Start'],
end: +d['End'],
caller: d['Caller'],
callerType: d['CallerType'],
gen: +d['Generation']
};
}).then(function(data) {
globaldata = data
render();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
<svg />
And the directedGraph module:
DirectedGraph = function(svg, nodes, links, clickFunc, margin) {
this.svg = svg;
this.margin = margin;
this.height = +svg.attr("height");
this.width = +svg.attr("width");
this.innerHeight = this.height - this.margin.top - this.margin.bottom;
this.innerWidth = this.width - this.margin.left - this.margin.right;
this.xCenter = this.innerWidth * 0.5;
this.yCenter = this.innerHeight * 0.5;
this.clickFunc = clickFunc;
this.transformG = this.svg.append("g")
.attr("transform", `translate(${this.margin.left},${this.margin.top})`);
//Build arrowhead
this.transformG.append("defs").append("marker")
.attrs({
"id": "arrowhead",
"viewBox": "-4 -5 10 10",
"refX": 13,
"refY": 0,
"orient": "auto-start-reverse",
"markerWidth": 11,
"markerHeight": 7,
"xoverflow": "visible"
})
.append("svg:path")
.attr("d", "M -4,-5 L 6 ,0 L -4,5")
.attr("fill", "#999")
.style("stroke", "none");
this.links = this.transformG.append("g").attr("class", "links").selectAll(".link")
this.nodesG = this.transformG.append("g").attr("class", "nodes").selectAll(".node");
this.render(nodes, links);
};
DirectedGraph.prototype.render = function(nodes, links) {
let vis = this;
//ColorScale for node Color
let taskColor = d3.scaleOrdinal(d3.schemePastel1);
let amount = 0;
links.forEach(element => {
amount += element.amount;
})
let stroke = d3.scaleLinear()
.domain([1, amount])
.range([0.3, 20]);
//Radius of the Nodes
let nodeRadius = 10;
//Setting initial position of construct node
let pos0Node = nodes.filter(n => n.id === "construct");
pos0Node.fx = this.xCenter;
pos0Node.fy = this.yCenter;
//force direction Layout Simulation
simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(60).id(function(d) {
return d.id;
})) // .strength(0.2)
.force("charge", d3.forceManyBody().distanceMin(20).distanceMax(100).strength(-100))
.force("center", d3.forceCenter(this.xCenter, this.yCenter));
this.links = this.links.data(links);
this.links.exit().remove();
let gLinkEnter = this.links.enter().append("svg:path").attr("class", "link");
gLinkEnter
.attr("stroke", "#999999")
.attr("marker-end", "url(#arrowhead)")
.merge(this.links)
.style("stroke-width", function(d) {
return Math.sqrt(stroke(d.amount))
})
.attrs({
"class": "edgelabel1",
"id": function(d, i) {
return "edgelabel1" + i
},
"font-size": 10,
"fill": "none",
});
this.nodesG = this.nodesG.data(nodes);
this.nodesG.exit().remove();
let gNodeEnter = this.nodesG.enter().append("circle").attr("class", "node");
gNodeEnter
.merge(this.nodesG)
.attr("r", nodeRadius)
.attr("fill", function(d, i) {
return taskColor(i);
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
)
.on('click', d => this.clickFunc(d.id));
simulation
.nodes(nodes)
.on("tick", ticked());
simulation.force("link")
.links(links);
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart()
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function ticked() {
/*gLinkEnter
.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; });
*/
gLinkEnter.attr("d", function(d) {
let x1 = d.source.x;
let y1 = d.source.y;
let x2 = d.target.x;
let y2 = d.target.y;
let dx = x2 - x1;
let dy = y2 - y1;
let dr = Math.sqrt(dx * dx + dy * dy);
// Defaults for normal edge.
let drx = 0;
let dry = 0;
let xRotation = 0; // degrees
let largeArc = 0; // 1 or 0
let sweep = 1; // 1 or 0
// Self edge.
if (x1 === x2 && y1 === y2) {
// Fiddle with this angle to get loop oriented.
xRotation = -45;
// Needs to be 1.
largeArc = 1;
// Change sweep to change orientation of loop.
//sweep = 0;
// Make drx and dry different to get an ellipse
// instead of a circle.
drx = 20;
dry = 30;
// For whatever reason the arc collapses to a point if the beginning
// and ending points of the arc are the same, so kludge it.
x2 = x2 + 1;
y2 = y2 + 1;
}
return "M " + x1 + "," + y1 + " A " + drx + " " + dry + " " + xRotation + " " + largeArc + " " + sweep + " " + x2 + "," + y2;
});
gNodeEnter
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
};
And the Corresponding csv file
TaskID,TaskType,Start,End,Caller,CallerType,Generation
construct,construct,0,6.5,Null,Null,0
start1,startsweep,0.4,0.7,construct,construct,1
start2,startsweep,0.8,4,construct,construct,1
start3,startsweep,1.5,3,construct,construct,1
start4,startsweep,2,3.3,construct,construct,1
start5,startsweep,2.8,4.9,construct,construct,1
start6,startsweep,3.4,4,construct,construct,1
start7,startsweep,4.1,5.6,construct,construct,1
start8,startsweep,5,6,construct,construct,1
start9,startsweep,5.1,5.7,construct,construct,1
start10,startsweep,6,6.3,construct,construct,1
start11,startsweep,1.2,1.7,start2,startsweep,2
start12,startsweep,1.7,2.9,start2,startsweep,2
start13,startsweep,2.2,3,start2,startsweep,2
start14,startsweep,3.1,3.9,start2,startsweep,2
start15,startsweep,3,4,start5,startsweep,2
start16,startsweep,5.1,5.4,start8,startsweep,2
start17,startsweep,1.3,1.5,start11,startsweep,3
start18,startsweep,1.9,2.5,start12,startsweep,3
start19,startsweep,3.1,3.8,start15,startsweep,3
start20,startsweep,5.2,5.3,start16,startsweep,3
start21,startsweep,1.35,1.4,start17,startsweep,4
start22,startsweep,3.15,3.75,start19,startsweep,4
start23,startsweep,5.2,5.25,start20,startsweep,4
start24,startsweep,3.15,3.25,start22,startsweep,5
start25,startsweep,3.2,3.3,start22,startsweep,5
start26,startsweep,3.25,3.7,start22,startsweep,5
start27,startsweep,3.6,3.7,start22,startsweep,5
start28,startsweep,3.3,3.5,start26,startsweep,6
start29,startsweep,3.4,3.45,start28,startsweep,7
Upvotes: 1
Views: 170
Reputation: 7210
Seems to be that you run enter/exit on the same selection (nodesG
).
The correct code should be:
render() {
...
const nodesG = svg.selectAll('g.node').data(nodesData);
const newNodes = nodesG.enter().append('g').classed('node', true);
newNodes.append('circle')...
nodesG.exit().remove();
...
}
Upvotes: 1