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").....
Related
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.
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 + ")"; });
I am completely stuck adding labels to the force directed tree graph found here http://bl.ocks.org/mbostock/1138500
I have attempted to synthesize the force directed tree with other examples that include labels as well as following the answer to Add text label to d3 node in Force directed Graph and resize on hover but the graph always seems to break.
This code works for the force directed graph with labels and pictures
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
stroke: #ccc;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([width, height]);
d3.json("graph.json", function(error, json) {
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("image")
.attr("xlink:href", "https://github.com/favicon.ico")
.attr("x", -8)
.attr("y", -8)
.attr("width", 16)
.attr("height", 16);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.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 + ")"; });
});
});
</script>
However when I attempt to modify this to form a tree structure from Mike's example my code looks like this but does not work.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
stroke: #ccc;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([width, height]);
d3.json("test.json", function(error, json) {
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("image")
.attr("xlink:href", "https://github.com/favicon.ico")
.attr("x", -8)
.attr("y", -8)
.attr("width", 16)
.attr("height", 16);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
force
.nodes(json.nodes)
.links(json.links)
.on("tick", tick)
.start();
function tick(e) {
// Push sources up and targets down to form a weak tree.
var k = 6 * e.alpha;
json.links.forEach(function(d, i) {
d.source.y -= k;
d.target.y += k;
});
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return 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; });
}
});
</script>
I have tried and tried to resolve this but cannot combine the labels with the force directed graph, any assistance would be greatly appreciated, I've been beating my head against the wall on this for some time now...
Thanks!
The part missing from your sample code is the addition to the tick callback that decreases the y value of the source, and increases that of the target by a small amount each time.
Here's a jsfiddle example which I think does what you're after.
The key portion is the addition of a parameter, called e here, to the tick function, along with the lines
var k = 6 * e.alpha;
json.links.forEach(function(d, i) {
d.source.y -= k;
d.target.y += k;
});
The result looks like this, once you also increase the magnitude of the charge to push the nodes a little further away from each other:
I have some data I am trying to display with the D3 force layout. Apologies if this is a naive question, or if the terminology i employ in the question title is not accurate. I couldn't see an answer quite what i was looking for.
I made a fiddle with a sample showing what I am on about here :
http://jsfiddle.net/stevendwood/f3GJT/8/
In the example I have one node (0) which has lots of links. Another node (16) has a smaller amount of links, 0 and 16 are both connected to 15.
So what i would like is for 0 and 16 to be little clusters with their connected nodes appearing in a nice circle around them.
I vainly tried to customise the charge based on the number of links, but I think what i want to do is somehow make nodes more attracted to nodes they are connected to and less attracted to nodes that they are not connected to.
I would like something like this if possible :
var w = 500,
h = 500,
nodes = [],
links = [];
/* Fake up some data */
for (var i=0; i<20; i++) {
nodes.push({
name: ""+i
});
}
for (i=0; i<16; i++) {
links.push({
source: nodes[i],
target: nodes[0]
});
}
links.push({
source: nodes[16],
target: nodes[15]
});
for (i=17; i<20; i++) {
links.push({
source: nodes[i],
target: nodes[16]
});
}
var countLinks = function(n) {
var count = 0;
links.forEach(function(l) {
if (l.source === n || l.target === n) {
count++;
}
});
return count;
}
/////////////////////////////////////////////
var vis = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.nodes(nodes)
.links([])
.gravity(0.05)
.charge(function(d) {
return countLinks(d) * -50;
})
.linkDistance(300)
.size([w, h]);
var link = vis.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link")
.attr("stroke", "#CCC")
.attr("fill", "none");
var node = vis.selectAll("circle.node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("svg:circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 14)
.style("fill", "#CCC")
.style("stroke", "#AAA")
.style("stroke-width", 1.5)
node.append("text").text(function(d) { return d.name; })
.attr("x", -6)
.attr("y", 6);
force.on("tick", function(e) {
node.attr("transform", function(d, i) {
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; })
});
force.start();
Why did you leave out the links when declaring the force layout? If you add them back in, it looks much closer to what you wanted:
var force = d3.layout.force()
.nodes(nodes)
//.links([])
.links(links)
.gravity(0.1)
.charge(-400)
.linkDistance(75)
.size([w, h]);
http://jsfiddle.net/f3GJT/11/
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>