Adding new nodes to Force-directed layout - javascript

First question on Stack Overflow, so bear with me! I am new to d3.js, but have been consistently amazed by what others are able to accomplish with it... and almost as amazed by how little headway I've been able to make with it myself! Clearly I'm not grokking something, so I hope that the kind souls here can show me the light.
My intention is to make a reusable javascript function which simply does the following:
Creates a blank force-directed graph in a specified DOM element
Allows you to add and delete labeled, image-bearing nodes to that graph, specifying connections between them
I've taken http://bl.ocks.org/950642 as a starting point, since that's essentially the kind of layout I want to be able to create:
Here's what my code looks like:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="underscore-min.js"></script>
<script type="text/javascript" src="d3.v2.min.js"></script>
<style type="text/css">
.link { stroke: #ccc; }
.nodetext { pointer-events: none; font: 10px sans-serif; }
body { width:100%; height:100%; margin:none; padding:none; }
#graph { width:500px;height:500px; border:3px solid black;border-radius:12px; margin:auto; }
</style>
</head>
<body>
<div id="graph"></div>
</body>
<script type="text/javascript">
function myGraph(el) {
// Initialise the graph object
var graph = this.graph = {
"nodes":[{"name":"Cause"},{"name":"Effect"}],
"links":[{"source":0,"target":1}]
};
// Add and remove elements on the graph object
this.addNode = function (name) {
graph["nodes"].push({"name":name});
update();
}
this.removeNode = function (name) {
graph["nodes"] = _.filter(graph["nodes"], function(node) {return (node["name"] != name)});
graph["links"] = _.filter(graph["links"], function(link) {return ((link["source"]["name"] != name)&&(link["target"]["name"] != name))});
update();
}
var findNode = function (name) {
for (var i in graph["nodes"]) if (graph["nodes"][i]["name"] === name) return graph["nodes"][i];
}
this.addLink = function (source, target) {
graph["links"].push({"source":findNode(source),"target":findNode(target)});
update();
}
// set up the D3 visualisation in the specified element
var w = $(el).innerWidth(),
h = $(el).innerHeight();
var vis = d3.select(el).append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.nodes(graph.nodes)
.links(graph.links)
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);
var update = function () {
var link = vis.selectAll("line.link")
.data(graph.links);
link.enter().insert("line")
.attr("class", "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; });
link.exit().remove();
var node = vis.selectAll("g.node")
.data(graph.nodes);
node.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("image")
.attr("class", "circle")
.attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
.attr("x", "-8px")
.attr("y", "-8px")
.attr("width", "16px")
.attr("height", "16px");
node.append("text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
node.exit().remove();
force.on("tick", function() {
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; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
// Restart the force layout.
force
.nodes(graph.nodes)
.links(graph.links)
.start();
}
// Make it all go
update();
}
graph = new myGraph("#graph");
// These are the sort of commands I want to be able to give the object.
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");
</script>
</html>
Every time I add a new node, it re-labels all of the existing nodes; these pile on top of each other and things start to get ugly. I understand why this is: because when I call the update() function function upon adding a new node, it does a node.append(...) to the entire data set. I can't figure out how to do this for only the node I'm adding... and I can only apparently use node.enter() to create a single new element, so that doesn't work for the additional elements I need bound to the node. How can I fix this?
Thank you for any guidance that you're able to give on any of this issue!
Edited because I quickly fixed a source of several other bugs that were previously mentioned

After many long hours of being unable to get this working, I finally stumbled across a demo that I don't think is linked any of the documentation: http://bl.ocks.org/1095795:
This demo contained the keys which finally helped me crack the problem.
Adding multiple objects on an enter() can be done by assigning the enter() to a variable, and then appending to that. This makes sense. The second critical part is that the node and link arrays must be based on the force() -- otherwise the graph and model will go out of synch as nodes are deleted and added.
This is because if a new array is constructed instead, it will lack the following attributes:
index - the zero-based index of the node within the nodes array.
x - the x-coordinate of the current node position.
y - the y-coordinate of the current node position.
px - the x-coordinate of the previous node position.
py - the y-coordinate of the previous node position.
fixed - a boolean indicating whether node position is locked.
weight - the node weight; the number of associated links.
These attributes are not strictly needed for the call to force.nodes(), but if these are not present, then they would be randomly initialised by force.start() on the first call.
If anybody is curious, the working code looks like this:
<script type="text/javascript">
function myGraph(el) {
// Add and remove elements on the graph object
this.addNode = function (id) {
nodes.push({"id":id});
update();
}
this.removeNode = function (id) {
var i = 0;
var n = findNode(id);
while (i < links.length) {
if ((links[i]['source'] === n)||(links[i]['target'] == n)) links.splice(i,1);
else i++;
}
var index = findNodeIndex(id);
if(index !== undefined) {
nodes.splice(index, 1);
update();
}
}
this.addLink = function (sourceId, targetId) {
var sourceNode = findNode(sourceId);
var targetNode = findNode(targetId);
if((sourceNode !== undefined) && (targetNode !== undefined)) {
links.push({"source": sourceNode, "target": targetNode});
update();
}
}
var findNode = function (id) {
for (var i=0; i < nodes.length; i++) {
if (nodes[i].id === id)
return nodes[i]
};
}
var findNodeIndex = function (id) {
for (var i=0; i < nodes.length; i++) {
if (nodes[i].id === id)
return i
};
}
// set up the D3 visualisation in the specified element
var w = $(el).innerWidth(),
h = $(el).innerHeight();
var vis = this.vis = d3.select(el).append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);
var nodes = force.nodes(),
links = force.links();
var update = function () {
var link = vis.selectAll("line.link")
.data(links, function(d) { return d.source.id + "-" + d.target.id; });
link.enter().insert("line")
.attr("class", "link");
link.exit().remove();
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id;});
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("image")
.attr("class", "circle")
.attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
.attr("x", "-8px")
.attr("y", "-8px")
.attr("width", "16px")
.attr("height", "16px");
nodeEnter.append("text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {return d.id});
node.exit().remove();
force.on("tick", function() {
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; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
// Restart the force layout.
force.start();
}
// Make it all go
update();
}
graph = new myGraph("#graph");
// You can do this from the console as much as you like...
graph.addNode("Cause");
graph.addNode("Effect");
graph.addLink("Cause", "Effect");
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");
</script>

Related

d3.js Molecule Diagram only working on the last element of the object

so I'm trying to create a visual representations of a couple of vlans and the connections of switches in each of them. I tried implementing it with this example I found online https://bl.ocks.org/mbostock/3037015 , the problem is that when i created a loop to go through all of the vlans, only the last vlan is drawn, there's really no reason I can see of why this is happening since all elements are calling the function.
If I remove the last element from the array with delete data['80'] then the one before the last starts working, so the only one working it the last one of the dictionary object, don't why though
code:
var data = {{ graph_vlans | safe }};
console.log(data);
$(document).ready(() => {
//-----------------------------------------------------------------
// TREE DISPLAY ---------------------------------------------------
//-----------------------------------------------------------------
var toggler = document.getElementsByClassName("caret");
for (var i = 0; i < toggler.length; i++) {
toggler[i].addEventListener("click", function () {
this.parentElement.querySelector(".nested").classList.toggle("active");
this.classList.toggle("caret-down");
});
}
//-----------------------------------------------------------------
// NETWORK DIAGRAM ------------------------------------------------
//-----------------------------------------------------------------
var width = 960, height = 500;
var color = d3.scale.category20();
var radius = d3.scale.sqrt().range([0, 6]);
var i = 0;
for (var key in data) {
console.log(key);
console.log(key["4"]);
var svg = d3.select("#graph_" + key).append("svg").attr("width", width).attr("height", height);
var force = d3.layout.force()
.size([width, height])
.charge(-400)
.linkDistance(function (d) {
return radius(d.source.size) + radius(d.target.size) + 20;
});
var graph = data[key];
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("g")
.attr("class", "link");
link.append("line")
.style("stroke-width", function (d) {
return (d.bond * 2 - 1) * 2 + "px";
});
link.filter(function (d) {
return d.bond > 1;
}).append("line")
.attr("class", "separator");
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", function (d) {
return radius(d.size);
})
.style("fill", function (d) {
return color(d.atom);
});
node.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.atom;
});
force.nodes(graph.nodes)
.links(graph.links)
.on("tick", tick)
.start();
i++;
}
function tick() {
link.selectAll("line")
.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;
});
node.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
});
Problem
I made some fake data for your plot and got this:
Your other force layouts are drawing, they're just not positioned. They're at [0,0] - barely visible here, in the top left corner of the SVG. So why is this?
Each for loop iteration you redefine any existing link and node variables - their scope extends beyond the for statement so you overwrite the previous defintion. var restricts a variables scope by function, the for statement doesn't limit scope if using var.
Because of this, when you call the tick function for each force layout, only the last layout is updated because node and link refer to the last layouts nodes and links.
So only your last force layout does anything.
Solution
There are a few solutions, I'm proposing one that adds two simple changes from your current code.
We need to get each force layout's nodes and links to the tick function. Currently we have all the force layout tick functions using the same node and link references. Ultimately, this is a variable scoping issue.
We can start by placing the tick function into the for loop. But, this still runs into the same problem by itself: node and link have a scope that isn't limited to the for loop (or the current iteration of the for loop) - each tick function will still use the same node and link references.
To fix this, we also need to use let when defining link and node (instead of var), now these variables have a block level scope, meaning each iteration's definitions of link and node won't overwrite the previous iterations.
By moving the tick function into the for loop and using let to define node and link, each time we call the tick function it will use the appropriate nodes and links.
Here's an example using a slightly modified example of the above code (removing some of the styling that relies on data properties and re-sizing the layouts for snippet view, but with the changes proposed above):
var data = {
"a":{
nodes:[{name:1},{name:2},{name:3}],
links:[
{source:1, target:2},
{source:2, target:0},
{source:0, target:1}
]
},
"b":{
nodes:[{name:"a"},{name:"b"},{name:"c"}],
links:[
{source:1, target:2},
{source:2, target:0},
{source:0, target:1}
]
}
}
// TREE DISPLAY
var width = 500, height = 100;
var color = d3.scale.category20();
var radius = d3.scale.sqrt().range([0, 6]);
var i = 0;
for (var key in data) {
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height);
var force = d3.layout.force()
.size([width, height])
.charge(-400)
.linkDistance(20);
var graph = data[key];
let link = svg.selectAll(".link")
.data(graph.links)
.enter().append("g")
.attr("class", "link");
link.append("line")
.style("stroke-width", 1)
.style("stroke","#ccc")
let node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node");
node.append("circle")
.attr("r", 5)
.attr("fill","#eee");
node.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.name;
});
force.nodes(graph.nodes)
.links(graph.links)
.on("tick", tick)
.start();
i++;
function tick() {
link.selectAll("line")
.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;
});
node.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>

JS- how to remove duplicate JSON nodes and add one link for all nodes that get merged

enter code here I have a JSON File from which I want to create a d3 directed
graph with arrows in the direction of higher influence score
{"nodes":[{"Name":"GJA","influenceScore":81.0,"type":10.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0},
{"Name":"SAG","influenceScore":89.0,"type":30.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0}],"links":
[{"source":0,"target":0,"type":"SA","value":1},
{"source":0,"target":1,"type":"SA","value":1},
{"source":0,"target":2,"type":"SA","value":1},
{"source":0,"target":3,"type":"SA","value":1}]}
I am a d3novice, so would like some help from experts here
My d3 code is here:
.link {
stroke: #ccc;
}
.node text {
pointer-events: none;
font: 12px sans-serif;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var width = 1200,
height = 900;
var color = d3.scale.category10();
var fill = d3.scale.category10();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(0.052)
.distance(350)
.charge(-20)
.size([width, height]);
d3.json("\\abc.json", function(error, json) {
if (error) throw error;
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link").style("stroke-width", function(d) { return
Math.sqrt(d.value); }).style("stroke", function(d) {return
fill(d.value);});
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", "node")
.attr("r", function(d) { return (d.influenceScore/10) + 10;
}).style("fill", function(d) { return color(d.type); });
node.append("text")
.attr("dx", -35)
.attr("dy", "4.5em").text(function(d) { return d.Name });
node.append("title").text(function(d) { return d.Name ;});
force.on("tick", function() {
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; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y +
")"; });
});
});
I am getting the following image
I would like the target node e.g JJZ here just to occur once ( currently it's occurring as many number of times as it is repeated in the JSON i.e 2 times in the given example) however the line joining the two nodes should increase in thickness depending on the number of times the nodes repeat. so the blue line linking JJZ with GJA should be thicker than GJA and SAG and if another node occurs 5 times that should be thicker than JJZ and GJA. Also how do I insert directed arrows in the direction of a higher influence score
Your question here has little to do with D3: you can manipulate your array with plain JavaScript.
This function looks for the objects on json.nodes based on the property Name. If it doesn't exist, it pushes the object into an array that I named filtered. If it already exists, it increases the value of count in that object:
var filtered = []
json.nodes.forEach(function(d) {
if (!this[d.Name]) {
d.count = 0;
this[d.Name] = d;
filtered.push(this[d.Name])
}
this[d.Name].count += 1
}, Object.create(null))
Here is the demo:
var json = {"nodes":[{"Name":"GJA","influenceScore":81.0,"type":10.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0},
{"Name":"SAG","influenceScore":89.0,"type":30.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0}],"links":
[{"source":0,"target":0,"type":"SA","value":1},
{"source":0,"target":1,"type":"SA","value":1},
{"source":0,"target":2,"type":"SA","value":1},
{"source":0,"target":3,"type":"SA","value":1}]};
var filtered = []
json.nodes.forEach(function(d){
if(!this[d.Name]){
d.count = 0;
this[d.Name] = d;
filtered.push(this[d.Name])
}
this[d.Name].count += 1
}, Object.create(null))
console.log(filtered)
Then, you just need to use the property count to set the stroke-width of your links.

Removing two nodes from a Force Layout fails while one succeeds

I'm trying to remove some nodes from a Force Layout.
The first part of the process is selecting one or more nodes. That's done with the click handler:
var nodeg = node.enter().append("g")
.attr("class", "node")
.on('click', function (n) {
if (n.dragging === true) {
return;
}
// select the clicked node
n.selected = !n.selected;
d3.select(this)
.transition()
.duration(500)
.ease('bounce')
.attr('fill', getNodeBackground(n))
.attr('transform', getNodeTransform(n));
})
.call(drag);
Note that I'm setting selected to true.
Next, if the user presses the delete key I remove the selected nodes:
function removeSelectedNodes() {
if (!confirm('Are you sure you want to delete the selected relationships?')) {
return;
}
var firstIndex = -1;
d3.selectAll('.node')
.each(function (n, i) {
if (n.selected !== true) {
return;
}
n.data.remove = true;
n.data.index = i;
if (firstIndex === -1) {
firstIndex = i;
}
});
var offset = 0;
_.each(_.where(scope.nodes, {data: {remove: true}}), function (n) {
var removeAt = n.index;
if (n.index > firstIndex) {
removeAt = n.index - 1 - offset;
offset++;
}
scope.nodes.splice(removeAt, 1);
scope.links.splice(removeAt - 1, 1);
});
renderGraph();
}
The entire renderGraph function looks like this:
function renderGraph() {
force
.nodes(scope.nodes)
.links(scope.links)
.start();
var link = svg.selectAll(".link")
.data(scope.links);
link.exit().remove();
link.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(scope.nodes);
node.exit().remove();
var nodeg = node.enter().append("g")
.attr("class", "node")
.on('click', function (n) {
if (n.dragging === true) {
return;
}
// select the clicked node
n.selected = !n.selected;
d3.select(this)
.transition()
.duration(500)
.ease('bounce')
.attr('fill', getNodeBackground(n))
.attr('transform', getNodeTransform(n));
})
.call(drag);
nodeg.append("image")
.attr("xlink:href", function (d) {
return d.avatar || 'https://github.com/favicon.ico'
})
.attr("x", -56)
.attr("y", -8)
.attr("width", 64)
.attr("height", 64);
nodeg.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.attr('class', 'name')
.text(function (d) {
return d.displayName;
});
nodeg.append("text")
.attr("dx", 12)
.attr("dy", "1.35em")
.text(function (d) {
return d.relationship;
});
force.on("tick", function () {
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;
});
node.attr("transform", function (d) {
return getNodeTransform(d);
});
});
}
This is where it starts going south. If I select one node, the graph is rendered correctly after splicing the node. However, if I remove two nodes the graph ends up persisting the first removed node (by index).
Let's say the first node (by index) was 'Bob' and the second node was 'Bill'. The second node will be removed, but the first one will persist. Interestingly, another one of the nodes, the current last node (by index), will be gone instead.
NOTE: the array looks good. The nodes I wanted removed are gone, and the remaining ones are correct.
What did I do wrong here?
UPDATE: I've tried not setting nodes and links after removing nodes, and just calling start:
force.start()
This didn't work.
The answer, thanks to Lars again, was to add a key function to the links and nodes:
var link = svg.selectAll(".link")
.data(scope.links, function (d) {
return d.source.data._id + '|' + d.target.data._id;
});
var node = svg.selectAll(".node")
.data(scope.nodes, function (d) {
return d.data._id;
});
Thanks Lars!

D3.js - Cannot set node labels after node click?

I have implemented a simple network visualization app in D3.js by adopting ideas from http://jsbin.com/omokap/8/edit?html,css,js,output.
This application reads node names (separated by line breaks) from a textarea in an html page and then constructs a network in which all nodes are connected to each other.
All my codes are included in the end of this message.
My problem is that I cannot set labels to nodes.
More specifically I get the following error message when loading the D3jNetVis.html on a web browser.
Uncaught TypeError: D3jNetVis.js:64
undefined is not a function
This error happens when I try to set labels to nodes in the following code snippet:
dataSet.nodes.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
This type of node label setter was suggested in D3.js, force-graph, cannot display text/label of nodes.
Any ideas why I get this error and how to fix it?
Google gave a hint that this could be related the importing order of the js-files in the html page but have been trying various combinations without success.
Thanks,
Erno Lindfors
D3jNetVis.html:
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Network Visualization Example - d3js</title>
<link rel="stylesheet" type="text/css" href="D3jNetVis.css">
</head>
<body>
<table>
<tr><td><div id="svgContent"></div></td></tr>
<tr><th align="left">Give node ids</th></tr>
<tr><td><textarea id="nodeIds" cols=5 rows=20></textarea></td></tr>
<tr><td><button type="button" onclick="constNet()">Construct Network</button></td></tr>
</table>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="D3jNetVis.js" charset="utf-8"></script>
</body>
</html>
D3jNetVis.js:
function constNet() {
var textArea = document.getElementById("nodeIds");
var nodeIdsArray = document.getElementById("nodeIds").value.split("\n");
var w = 500,
h = 500;
var svg = d3.select("#svgContent")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr('preserveAspectRatio', 'xMinYMin slice')
.append('g');
var nodesArray = [];
for (var i = 0; i < nodeIdsArray.length; i++) {
var nodeId = nodeIdsArray[i];
var newNode = {name: "Node" + nodeId, id:nodeId, fixed:false};
nodesArray[nodesArray.length] = newNode;
}
var edgesArray = [];
for (var i = 0; i < nodeIdsArray.length-1; i++) {
var sNodeId = nodeIdsArray[i];
for (var j = i+1; j < nodeIdsArray.length; j++) {
var tNodeId = nodeIdsArray[j];
edgesArray[edgesArray.length] = {source:sNodeId-1, target:tNodeId-1};
}
}
var dataSet = {
nodes: nodesArray,
edges: edgesArray
};
var force = self.force = d3.layout.force()
.nodes(dataSet.nodes)
.links(dataSet.edges)
.gravity(0.05)
.distance(100)
.charge(-100)
.size([w,h])
.start();
var link = svg.selectAll(".link")
.data(dataSet.edges)
.enter().append("line")
.attr("class", "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; });
var node_drag = d3.behavior.drag()
.on("dragstart", dragstart)
.on("drag", dragmove)
.on("dragend", dragend);
var node = svg.selectAll("circle")
.data(dataSet.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 4.5)
.call(node_drag);
/*
The "Uncaught TypeError" happens in the next line.
*/
dataSet.nodes.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
function dragstart(d, i) {
force.stop(); // stops the force auto positioning before you start dragging
}
function dragmove(d, i) {
d.px += d3.event.dx;
d.py += d3.event.dy;
d.x += d3.event.dx;
d.y += d3.event.dy;
tick();
}
function dragend(d, i) {
d.fixed = true; // of course set the node to fixed so the force doesn't include the node in its auto positioning stuff
tick();
force.resume();
}
force.on("tick", tick);
function tick() {
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; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
}
D3jNetVis.css:
line{
stroke: #cccccc;
stroke-width: 1;
}
circle{
fill: blue;
}
You can just append html elements to a selection of html elements (so the browser knows where to append in the DOM).
dataSet.nodes is not a selection of html elements, that's why you get the error message.
Write instead:
node.append("text").....

Reload d3.js graph on node click

I've had a d3 graph with a bunch of nodes based off items. When I click on one of those nodes, the graph is reloaded with data based off the clicked node.
I use a URL structure like so:
http://siteurl.com/index.html?item=
When a node is clicked, I have a function that runs the d3.json( function again with the new URL and then executes the update function again.
I've recently changed my code so that the node word appears below the node. Now I get an 'undefined is not a function' error on the line of code with node.exit().remove();
EDIT: Issue fixed from #Elijah's answer, but does not resolve my issue.
So when I click on a node, links get removed, then regenerated, but the nodes from the previous graph remain.
JSFiddle
Here's some of my JS
$wordToSearch = "bitter";
var w = 960,
h = 960,
node,
link,
root,
title;
var jsonURL = 'http://desolate-taiga-6759.herokuapp.com/word/' + $wordToSearch;
d3.json(jsonURL, function(json) {
root = json.words[0]; //set root node
root.fixed = true;
root.x = w / 2;
root.y = h / 2 - 80;
update();
});
var force = d3.layout.force()
.on("tick", tick)
.charge(-700)
.gravity(0.1)
.friction(0.9)
.linkDistance(50)
.size([w, h]);
var svg = d3.select(".graph").append("svg")
.attr("width", w)
.attr("height", h);
//Update the graph
function update() {
var nodes = flatten(root),
links = d3.layout.tree().links(nodes);
// Restart the force layout.
force
.nodes(nodes)
.links(links)
.start();
// Update the links…
link = svg.selectAll("line.link")
.data(links, function(d) { return d.target.id; });
// Enter any new links.
link.enter().insert("svg:line", ".node")
.attr("class", "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; });
// Exit any old links.
link.exit().remove();
// Update the nodes…
node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 10)
.on("click", click)
.style("fill", "red");
node.append("text")
.attr("dy", 10 + 15)
.attr("text-anchor", "middle")
.text(function(d) { return d.word });
svg.selectAll(".node").data(nodes).exit().remove();
}
function tick() {
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; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
/***********************
*** CUSTOM FUNCTIONS ***
***********************/
//Request extended JSON objects when clicking a clickable node
function click(d) {
$wordClicked = d.word;
var jsonURL = 'http://desolate-taiga-6759.herokuapp.com/word/' + $wordClicked;
console.log(jsonURL);
updateGraph(jsonURL);
}
// Returns a list of all nodes under the root.
function flatten(root) {
var nodes = [], i = 0;
function recurse(node) {
if (node.children) node.size = node.children.reduce(function(p, v) { return p + recurse(v); }, 0);
if (!node.id) node.id = ++i;
nodes.push(node);
return node.size;
}
root.size = recurse(root);
return nodes;
}
//Update graph with new extended JSON objects
function updateGraph(newURL) {
d3.json(newURL, function(json) {
root = json.words[0]; //set root node
root.fixed = true;
root.x = w / 2;
root.y = h / 2 - 80;
update();
});
}
function getUrlParameter(sParam)
{
var sPageURL = window.location.search.substring(1);
var sURLVariables = sPageURL.split('&');
for (var i = 0; i < sURLVariables.length; i++)
{
var sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] == sParam) {
return sParameterName[1];
}
}
}
Does anyone have any ideas why thats not working please?
EDIT: Updated my JS based from #Elijah's answer.
Handle the 3 states enter, exit and update, separate from each other:
node = svg.selectAll(".node")
.data(nodes); // base data selection, this is the update
var nodeE = node
.enter(); // handle the enter case
var nodeG = nodeE.append("g")
.attr("class", "node")
.call(force.drag); // add group ON ENTER
nodeG.append("circle")
.attr("r", 10)
.on("click", click)
.style("fill", "red"); // append circle to group ON ENTER
nodeG.append("text")
.attr("dy", 10 + 15)
.attr("text-anchor", "middle")
.text(function(d) { return d.word }); // append text to group ON ENTER
node.exit().remove(); // handle exit
Update fiddle here.
Your problem is that here:
node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
You're defining node as svg.selectAll(".node").enter() which means your variable now refers to the selection enter behavior and not the selection itself. So when you try to change exit behavior on it with: node.exit().remove();
..you're trying to access the .exit() behavior not of the selection but of the selection's .enter() behavior. Replace that with:
svg.selectAll(".node").data(nodes).exit().remove();
And that should fix your problem. There may be something else going on, but that's definitely going to cause issues.
Edited to add:
You should also update your tick function so that it doesn't reference node which is now assigned to the #selection.enter() and not the selection and instead reference the selection:
svg.selectAll("g.node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

Categories