I've added a new peice of javascript to an old script I had to add a highlighting functionality to a force network layout. I get the information for the diagram from generated json in a rails app. The original code I've been using is here:
var width = 960,
height = 960;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-100)
.linkDistance(530)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var endpoint = window.location.href+".json"
d3.json(endpoint, function(graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("marker-end", "url(#suit)");
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 10)
.style("fill", function(d) { return color(d.group); })
.call(force.drag)
.on('dblclick', connectedNodes);
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
svg.append("defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 25)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
.style("stroke", "#4679BD")
.style("opacity", "0.6");
//APPENDED CODE ADDED HERE
//Toggle stores whether the highlighting is on
var toggle = 0;
//Create an array logging what is connected to what
var linkedByIndex = {};
for (i = 0; i < graph.nodes.length; i++) {
linkedByIndex[i + "," + i] = 1;
};
graph.links.forEach(function (d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
//This function looks up whether a pair are neighbours
function neighboring(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
function connectedNodes() {
if (toggle == 0) {
//Reduce the opacity of all but the neighbouring nodes
d = d3.select(this).node().__data__;
node.style("opacity", function (o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
});
link.style("opacity", function (o) {
return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1;
});
//Reduce the op
toggle = 1;
} else {
//Put them back to opacity=1
node.style("opacity", 1);
link.style("opacity", 1);
toggle = 0;
}
}
I then tried to append further code as suggested here and simply added the following to the bottom of the script above where it is marked in capital letters
Could have been so simple.... The script worked but the added functionlity (to add highlights between nodes) didn't. An error message says:
Uncaught ReferenceError: graph is not defined
My susipicion is that it relates to the line
d3.json(endpoint, function(graph) {
and the fact that the subsequent }); is in the wrong place to encompass the new code but I've played with it and I'm not sure how to correct it
UPDATE
I've solved this. The problem was simply that I was declaring graph inside a function and the other functions couldn't access it. The solution is to put the other functions inside the function that delares it which in effect means moving the last
});
from the line
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
to the very last line. Works fine now
The answer is now given in the UPDATE section of the question
Related
I'm trying to upgrade the working D3.js v3 collapsible graph code below to work under D3.js v5. I've changed layout.force() to use the new forceSimulation function as shown further below.
var width = 960,
height = 500,
root;
var force = d3.layout.force()
.size([width, height])
.linkDistance(100)
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
//Added markers to indicate that this is a directed graph
svg.append("defs").selectAll("marker")
.data(["arrow"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5");
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("graph2.json", function(json) {
root = json;
//Give nodes ids and initialize variables
for(var i=0; i<root.nodes.length; i++) {
var node = root.nodes[i];
node.id = i;
node.collapsing = 0;
node.collapsed = false;
}
//Give links ids and initialize variables
for(var i=0; i<root.links.length; i++) {
var link = root.links[i];
link.source = root.nodes[link.source];
link.target = root.nodes[link.target];
link.id = i;
}
update();
});
function update() {
//Keep only the visible nodes
var nodes = root.nodes.filter(function(d) {
return d.collapsing == 0;
});
var links = root.links;
//Keep only the visible links
links = root.links.filter(function(d) {
return d.source.collapsing == 0 && d.target.collapsing == 0;
});
force
.nodes(nodes)
.links(links)
.start();
// Update the links…
link = link.data(links, function(d) { return d.id; });
// Exit any old links.
link.exit().remove();
// Enter any new links.
link.enter().insert("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; })
.attr("marker-end", "url(#arrow)");
// Update the nodes…
node = node.data(nodes, function(d){ return d.id; }).style("fill", color);
// Exit any old nodes.
node.exit().remove();
// Enter any new nodes.
node.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return Math.sqrt(d.size) / 10 || 4.5; })
.style("fill", color)
.on("click", click)
.call(force.drag);
}
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; });
}
// Color leaf nodes orange, and packages white or blue.
function color(d) {
return d.collapsed ? "#3182bd" : d.children ? "#c6dbef" : "#fd8d3c";
}
// Toggle children on click.
function click(d) {
if (!d3.event.defaultPrevented) {
//check if link is from this node, and if so, collapse
root.links.forEach(function(l) {
if(l.source.id == d.id) {
if(d.collapsed){
l.target.collapsing--;
} else {
l.target.collapsing++;
}
}
});
d.collapsed = !d.collapsed;
}
update();
}
I've changed the force block to:-
var force = d3.forceSimulation()
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.force("link", d3.forceLink().id(function (d) { return d.id))
.on("tick", tick);
but can't figure out what else needs to change. It doesn't draw anything.
Thanks.
I am trying to add a threshold to a force directed graph, where I will only include edges between vertices that that are above some threshold I store in a map. My slider is partly working, and the edges successfully get removed.
However, after the edges are removed, the graph stops animating, and there is an error in the console when calling force.start(). Do I really need to add unique ids? In the JSFiddle I linked, he does not do that, and his slider works with no problems.
Thanks!
There are multiple answers on StackOverflow to similar questions, and I have used them to fix my obvious errors (such as d3.js: "Cannot read property 'weight' of undefined" when manually defining both nodes and links for force layout), but I am down to this one. I am using this example: http://jsfiddle.net/simonraper/TdHgx/?utm_source=website&utm_medium=embed&utm_campaign=TdHgx from this website: http://www.coppelia.io/2014/07/an-a-to-z-of-extra-features-for-the-d3-force-layout/
I will send the relevant JS.
function draw(occurence) {
var ratio = window.devicePixelRatio || 1;
var width = Math.min(700,0.8*$(window).width()), height = 700;
var color = d3.scale.category20();
var svg = d3.select("#" + occurence).append("svg")
.attr("width", width)
.attr("height", height)
.attr("id", "#" + occurence + "svg");
d3.json(occurence + ".json", function(error, graph) {
if (error) throw error;
var force = d3.layout.force()
.charge(-120)
.linkDistance(50)
.size([width, height]);
force
.nodes(graph.nodes)
.links(graph.links)
.start();
if (occurence == "occurrences3") {
console.log(graph.nodes);
console.log(graph.links);
}
var graphRec=JSON.parse(JSON.stringify(graph)); //Add this line
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
node.on("dblclick", function(d) {
$("#uncuratedGraphsModal").modal();
var d = d3.select(this).node().__data__;
var name = d.name;
$("#uncuratedGraphsModalHeader").text("Node " + name);
$("#uncuratedGraphsModalBody").empty();
var edge_by_person = edge_by_person_per_threshold[occurence + "edge_by_person"];
var edges = edge_by_person[name];
edges.sort(function(a,b) {
return a.dest - b.dest;
});
$.each(edges, function(edge) {
var new_edge = $("<li>");
new_edge.text("Neighbor: " + edges[edge].dest + ", Token: " + edges[edge].token);
$("#uncuratedGraphsModalBody").append(new_edge);
});
});
$("#" + occurence + "thresholdSlider").on('input', function(thresh) {
threshold($("#" + occurence + "thresholdSlider").val());
});
//adjust threshold
function threshold(thresh) {
var edge_by_person = edge_by_person_per_threshold_unweighted[occurence + "edge_by_person_unweighted"];
graph.links.splice(0, graph.links.length);
for (var i = 0; i < graphRec.links.length; i++) {
var source = graphRec.links[i].source.name;
var dest = graphRec.links[i].target.name;
var value = -1;
var edges = edge_by_person[source];
var found = false;
for (var edge = 0; !found && edge < edges.length; edge++) {
if (dest == edges[edge].dest) {
value = edges[edge].token;
found = true;
}
}
if (value >= thresh) {
graph.links.push({
source: graphRec.links[i].source.name,
target: graphRec.links[i].target.name,
value: graphRec.links[i].value
});
}
}
restart();
}
//Restart the visualisation after any node and link changes
function restart() {
link = link.data(graph.links);
link.exit().remove();
link.enter().insert("line", ".node").attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
node = node.data(graph.nodes);
node.enter().insert("circle", ".cursor").attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
force
.nodes(node)
.links(link)
.start();
}
});
}
This is the JS that he added from the JsFiddle I linked:
This is my understanding: If you have the same nodes, and you just remove a few edges from graph.links, can't you just do a force.start()? When I directly copy his restart() function, all links are broken, and none get redrawn, even though doing a console.log(graph.links) shows the correct links to-redraw.
I believe that it's talking to the wrong nodes at this point, maybe the wrong SVG. I have multiple SVGs on the same page.
I am having some issues with d3js and I can't figure out what is going on. The idea is to draw initial graph from some endpoint data (first img), that's fine works well. Each node is clickable, on click ajax call is made for that node and data is returned, based on some criteria at that point nodes.push(xx), links.push(xx) happens to add new nodes and restart() is called to draw new nodes and links. The issue is that the main graph is doing the correct thing (Not showed on screenshots as I had to put fake data on the first graph i.e. calling an endpoint /record/id/first doesn't return a data) but there are bunch of random nodes showing up in the right bottom corner.
You can also see on the example below, even if the data doesn't change after clicking on first/second/third something wrong goes with node.enter() after restart() with the same data passed in...
JS FIDDLE: http://jsfiddle.net/5754j86e/
var w = 1200,
h = 1200;
var nodes = [];
var links = [];
var node;
var link;
var texts;
var ids = [];
var circleWidth = 10;
var initialIdentifier = "marcin";
nodes = initialBuildNodes(initialIdentifier, sparql);
links = initialBuildLinks(sparql);
//Add SVG
var svg = d3.select('#chart').append('svg')
.attr('width', w)
.attr('height', h);
var linkGroup = svg.append("svg:g").attr("id", "link-group");
var nodeGroup = svg.append("svg:g").attr("id", "node-group");
var textGroup = svg.append("svg:g").attr("id", "text-group");
//Add Force Layout
var force = d3.layout.force()
.size([w, h])
.gravity(.05)
.charge(-1040);
force.linkDistance(120);
restart();
function restart() {
force.links(links)
console.log("LINKS ARE: ", links)
link = linkGroup.selectAll(".link").data (links);
link.enter().append('line')
.attr("class", "link");
link.exit().remove();
force.nodes(nodes)
console.log("NODES ARE: ", nodes)
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
})
.on("click", function(d) {
nodeClicked (d);
})
.on('mouseenter', function(d){
nodeMouseEnter(d)
})
.on('mouseout', function(d){
nodeMouseOut(d)
});
node.exit().remove();
var annotation = textGroup.selectAll(".annotation").data (nodes);
annotation.enter().append("svg:g")
.attr("class", "annotation")
.append("text")
.attr("x", function(d) { return d.radius + 4 })
.attr("y", ".31em")
.attr("class", "label")
.text(function(d) { return d.name; });
annotation.exit().remove();
force.start();
}
function nodeClicked (d) {
// AJAX CALL happens here and bunch of nodes.push({name: "new name"}) happen
}
force.on('tick', function(e) {
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, i) {
return 'translate('+ d.x +', '+ d.y +')';
})
svg.selectAll(".annotation").attr("transform", function(d) {
var labelx = d.x + 13;
return "translate(" + labelx + "," + d.y + ")";
})
});
Okay I got it, based on the docs (https://github.com/mbostock/d3/wiki/Selections#enter):
var update_sel = svg.selectAll("circle").data(data)
update_sel.attr(/* operate on old elements only */)
update_sel.enter().append("circle").attr(/* operate on new elements only */)
update_sel.attr(/* operate on old and new elements */)
update_sel.exit().remove() /* complete the enter-update-exit pattern */
From my code you can see I do enter() and then once again I add circle on node in a separate statement.
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});
Adding circle should be within the scope of enter() otherwise it happens to all nodes not only the new nodes therefore it should be :
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag)
.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});
I'm not an expert in d3 (as many of you know) however I am attempting to modify a working force network so that the nodes are fixed. So far I have been able to get the nodes to work but haven't been able to get the lines between them to work.
I've created a fiddle at http://jsfiddle.net/PatriciaW/Dnnve/
Any help will be very welcome.
var width = 960,
height = 700,
n = 100;
var categoryColour = {
"community group": "red",
"volunteer": "blue",
"organization": "green",
"air": "transparent"
};
var json2 = {"nodes":[{"node":{"name":"TCAN","x2":1,"y2":2,"description":"A network of organizations in Toronto devoted to climate change mitigation and adaptation.","category":"organization","size":3,"URL":"http:\/\/localhost:8888\/mydrupal\/groups\/tcan"}},{"node":{"name":"Rita ","x2":5,"y2":3,"description":"Rita is devoted to mitigating climate change and participates in many organizations.","category":"volunteer","size":2,"URL":"http:\/\/localhost:8888\/mydrupal\/groups\/rita"}},{"node":{"name":"Green 13","x2":5,"y2":4,"description":"Green 13","category":"community group","size":2,"URL":"http:\/\/localhost:8888\/mydrupal\/groups\/green-13"}},{"node":{"name":"ZCO","x2":3,"y2":1,"description":"Zero Carbon Ontario","category":"organization","size":2,"URL":"http:\/\/localhost:8888\/mydrupal\/groups\/zco"}},{"node":{"name":"Resilient Toronto","x2":3,"y2":5,"description":"","category":"organization","size":3,"URL":"http:\/\/localhost:8888\/mydrupal\/groups\/resilient-toronto"}},{"node":{"name":"City of Toronto","x2":3,"y2":3,"description":"","category":"organization","size":5,"URL":"http:\/\/localhost:8888\/mydrupal\/groups\/city-toronto"}}]};
var nodes=json2.nodes.map(function(json2) {
return json2.node;
});
var i = 0;
while (i < nodes.length) {
nodes[i].fixed=true;
nodes[i].x = (nodes[i].x2)*100;
nodes[i].y = (nodes[i].y2)*100;
i = i+ 1;
}
var json = {"connections":[{"connection":{"source":"Rita","target":"Resilient Toronto"}},{"connection":{"source":"TCAN","target":"Resilient Toronto"}},{"connection":{"source":"Resilient Toronto","target":"City of Toronto"}},{"connection":{"source":"Rita","target":"ZCO"}},{"connection":{"source":"Rita","target":"Green 13"}},{"connection":{"source":"Green 13","target":"TCAN"}},{"connection":{"source":"ZCO","target":"TCAN"}}]};
var links=json.connections.map(function(json) {
return json.connection;
});
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height])
.linkDistance(60)
.charge(-300)
.on("tick", tick)
.start();
svg = d3.select("#network")
.append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle") // this is OK if use nodes or links below but defaults to links
.attr("r", 8)
.style("fill", function(nodes) {
return categoryColour [nodes.category];
})
node.append("text") // OK
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name; });
function tick() {
link
.attr("x1", function(d) {
// console.log("d"); console.log(d); has source and target but not .x and y values?
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 + ")";
});
};
// Use a timeout to allow the rest of the page to load first.
setTimeout(function() {
force.tick();
console.log("timeout nodes "); console.log(nodes); //
svg.selectAll("line")
.data(links)
.enter().append("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; });
svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 4.5);
}, 10);
I'm not an expert either but I think you are having issues because the source and target values in your connections should be referencing the respective positional array index for the node (not the name of the node).
A simple example, in your fiddle just change the first connection from...
{"connection":{"source":"Rita","target":"Resilient Toronto"}}
to...
{"connection":{"source":1,"target":4}}
1 and 4 being the index of the elements in the nodes array. Hope this is what you are looking for.
I updated the jsfiddle with the working code. Needs improvement but demonstrates the principle.
I'm working on the example located on jsfiddle, here.
It appears that I have everything structured properly, as the children are properly associated with their parents and the proper text is displayed.
The problems I've run into are as follows...
The main node (in the center) is not collapsible. The overall behavior of the graph is somewhat glitchy when compared to the example located here.
Colors do not change when nodes are collapsed, children of the parent node are displayed when the parent is collapsed. After several clicks on various nodes, children and parents seem to get switched.
My question is what section of code could be causing this and why?
Here's the code that I'm using to generate the chart. Data is missing, but is provided by the jsfiddle. Any help is appreciated, thanks in advance.
var width = 960,
height = 500,
root;
var force = d3.layout.force()
.charge(-220)
.size([width, height])
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link");
function update() {
var nodes = flatten(root);
var links = d3.layout.tree().links(nodes);
console.log(nodes);
// Restart the force layout.
force.nodes(nodes)
.links(links)
.linkDistance(55)
.start();
var link = svg.selectAll(".link")
.data(links, function(d) { return d.target.id; });
link.enter().append("line")
.attr("class", "link");
link.exit().remove();
var node = svg.selectAll("g.node")
.data(nodes)
var groups = node.enter().append("g")
.attr("class", "node")
.attr("id", function (d) {
return d.id
})
.on('click', click)
.call(force.drag);
groups.append("circle")
.attr("class","node")
.attr("x", -8)
.attr("y",-8)
.attr("r", function(d) { return d.children ? 4.5 : 10 })
.style("fill", color)
.on("click", click)
.call(force.drag);
groups.append("text")
.attr("dx", 12)
.attr("dy", "0.35em")
.style("font-size", "10px")
.style("color", "#000000")
.style("font-family", "Arial")
.text(function (d) {
console.log(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 + ")";
});
});
}
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 + ")";
});
}
// Color leaf nodes orange, and packages white or blue.
function color(d) {
return d._children ? "#3182bd" // collapsed package
: d.children ? "#c6dbef" // expanded package
: "#fd8d3c"; // leaf node
}
// Toggle children on click.
function click(d) {
if (!d3.event.defaultPrevented) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update();
}
}
// Returns a list of all nodes under the root.
function flatten(root) {
var nodes = [], i = 0;
function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
nodes.push(node);
}
recurse(root);
return nodes;
}
Ok, there are two things going on here.
First, by default, d3 uses the index of each datum as its ID (to determine when the item enters/exits the selection). This is your issue with parent/children moving around, when the element at index X is replaced by a new element, d3 thinks they are the same. You need to provide a function to return the id:
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.id; });
Second, you're only setting the fill color of the circle when an item enters the selection. You should extract the bit that is assigning the style out of the enter() block, so it will be executed each time you call update().
node.selectAll('circle').style('fill', color);
I copied and hacked together your fiddle into plunker, because jsFiddle was running really slow for me:
http://plnkr.co/edit/7AJlQub6uCGQ3VSvq4pa?p=preview