Displaying Node label in a dynamic forcelayout graph in D3.js - javascript
I'm trying to visualize a graph using D3.js via web sockets. I'm trying to display node label ( can be seen in the code below ), but it does not seem to appear at all. Please see the function start(). What is wrong here?
<script>
var width = 1900,
height = 1080;
var color = d3.scale.category10();
var nodes = [],
links = [];
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.charge(-100)
.gravity(0.1)
.linkDistance(100)
.size([width, height])
.on("tick", tick);
var svg = d3.select("body").append("svg").attr("width",width).attr("height", height);
var node = svg.selectAll(".node"),
link = svg.selectAll(".link");
function tick() {
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; });
}
// Add and remove elements on the graph object
function addNode(id) {
nodes.push({"name":id, "id":id});
start();
}
function addEdge(edgeId,sourceId,targetId) {
var sourceNode = findNode(sourceId);
var targetNode = findNode(targetId);
if((sourceNode !== undefined) && (targetNode !== undefined)) {
links.push({"edgeId":edgeId, "source": sourceNode, "target": targetNode});
start();
}
}
function removeEdge(edgeId) {
for (var i = 0; i < links.length; i++) {
if (links[i].edgeId == edgeId) {
links.splice(i, 1);
break;
}
}
start();
}
var findNode = function (id) {
for (var i=0; i < nodes.length; i++) {
if (nodes[i].id === id)
return nodes[i]
};
}
function start() {
var drag = force.drag().origin(function(d) { return d; }).on("dragstart", dragstarted).on("drag", dragged).on("dragend", dragended);
link = link.data(force.links(), function(d) { return d.source.id + "-" + d.target.id; });
link.enter().insert("line", ".node").attr("class", "link");
link.exit().remove();
node = node.data(force.nodes(), function(d) { return d.id;});
node.enter().append("circle").attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(drag);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
node.exit().remove();
force.start();
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
var socket = new WebSocket('ws://localhost:8887');
socket.onopen = function(){
console.log("Connection established, handle with function");
};
socket.onmessage = function(evt){
var obj = JSON.parse(evt.data);
if(obj.operation == "nodeAdded")
{
addNode( obj.nodeId );
}
if(obj.operation == "edgeAdded")
{
addEdge(obj.edgeId,obj.fromNodeId,obj.toNodeId);
}
if(obj.operation == "edgeRemoved")
{
removeEdge(obj.edgeId);
}
}
</script>
You will have to group the circles and corresponding labels for each node. Try this way.
node = node.data(force.nodes(), function(d) { return d.id;})
.enter().append("g")
.attr("class", "node")
.call(drag);
node.append("circle")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); });
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
Also update the tick function as shown below.
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 + ")"; });
}
By default text elements are of the white color. Add .style("fill", "black"), so you can see them.
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.style("fill", "black")
.text(function(d) { return d.name });
Also your node is the <circle> element. You can't append <text> element to the <circle> element ( it's not a container ). Use a <g> element and append <circle> and <text> to it or append <text> element to the <svg> element.
Related
d3 Node Labels and collapsing
I currently have a D3 that displays a force directed network and will collapse its children on click. I would like to to also display the names of the nodes on hover. Something like this: https://bl.ocks.org/mbostock/1212215 Below is my code so far. .node { cursor: pointer; stroke: #3182bd; stroke-width: 1.5px; } .link { fill: none; stroke: #9ecae1; stroke-width: 1.5px; } </style> <body> <script src="http://d3js.org/d3.v3.min.js"></script> <script> var width = 960, height = 500, root; var force = d3.layout.force() .size([width, height]) .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("test.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; } // for (var i=0; i<root.nodes.length; i++){ // var node = root.nodes[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; }) // 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(); } </script> My issue is with the update function. I was able to get it to display, but when I clicked to collapse, it wouldn't show the labels after. Im not sure if I should be tweeting the tick or what. thanks!
Group the circle and text labels using SVG group element as shown below. var groupNodes = node.enter().append("g") .attr("class", "node") .attr("transform", function(d) { return "translate("+d.x+","+d.y+")"; }); .on("click", click) .on("mouseover", function(){ d3.select(this).select("text").style("display","block"); }) .on("mouseout", function(){ d3.select(this).select("text").style("display","none"); }) .call(force.drag); groupNodes.append("circle") .attr("r", function(d) { return Math.sqrt(d.size) / 10 || 4.5; }) .style("fill", color); var label = groupNodes.append("text") .attr("dy", ".35em") .style("display", "none) .text(function(d) { return d.name; //Use the key which holds the name value }); The above code replaces following part of the code in the update function. 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); and now change the tick function as follows 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+")"; }); } Now all other functionalities should work as expected.
D3 V4 Draggable network with Force Directed Layout
What am trying to do is Have user select a bunch of nodes Let the user drag those nodes Use forcesimulation to manage the layout. Something similar to http://jsfiddle.net/pkerpedjiev/29majy5c/2/ But I need this in V4. var graph = {"nodes":[{"x":444,"y":275},{"x":378,"y":324},{"x":478,"y":278},{"x":471,"y":256},{"x":382,"y":269},{"x":371,"y":247},{"x":359,"y":276},{"x":364,"y":302},{"x":400,"y":330},{"x":388,"y":298},{"x":524,"y":296},{"x":570,"y":243},{"x":552,"y":159},{"x":502,"y":287},{"x":511,"y":313},{"x":513,"y":265},{"x":602,"y":132},{"x":610,"y":90},{"x":592,"y":91},{"x":575,"y":89},{"x":607,"y":73},{"x":591,"y":68},{"x":574,"y":73},{"x":589,"y":149},{"x":620,"y":205},{"x":621,"y":230},{"x":589,"y":234},{"x":602,"y":223},{"x":548,"y":188},{"x":532,"y":196},{"x":548,"y":114},{"x":575,"y":174},{"x":497,"y":250},{"x":576,"y":196},{"x":504,"y":201},{"x":494,"y":186},{"x":482,"y":199},{"x":505,"y":219},{"x":486,"y":216},{"x":590,"y":306},{"x":677,"y":169},{"x":657,"y":258},{"x":667,"y":205},{"x":552,"y":227},{"x":518,"y":173},{"x":473,"y":125},{"x":796,"y":260},{"x":731,"y":272},{"x":642,"y":288},{"x":576,"y":269},{"x":605,"y":187},{"x":559,"y":289},{"x":544,"y":356},{"x":505,"y":365},{"x":579,"y":289},{"x":619,"y":282},{"x":574,"y":329},{"x":664,"y":306},{"x":627,"y":304},{"x":643,"y":327},{"x":664,"y":348},{"x":665,"y":327},{"x":653,"y":317},{"x":650,"y":338},{"x":622,"y":321},{"x":633,"y":338},{"x":647,"y":357},{"x":718,"y":362},{"x":636,"y":240},{"x":640,"y":227},{"x":617,"y":249},{"x":631,"y":254},{"x":566,"y":213},{"x":713,"y":322},{"x":716,"y":298},{"x":666,"y":241},{"x":627,"y":355}],"links":[{"source":1,"target":0},{"source":2,"target":0},{"source":3,"target":0},{"source":3,"target":2},{"source":4,"target":0},{"source":5,"target":0},{"source":6,"target":0},{"source":7,"target":0},{"source":8,"target":0},{"source":9,"target":0},{"source":11,"target":10},{"source":11,"target":3},{"source":11,"target":2},{"source":11,"target":0},{"source":12,"target":11},{"source":13,"target":11},{"source":14,"target":11},{"source":15,"target":11},{"source":17,"target":16},{"source":18,"target":16},{"source":18,"target":17},{"source":19,"target":16},{"source":19,"target":17},{"source":19,"target":18},{"source":20,"target":16},{"source":20,"target":17},{"source":20,"target":18},{"source":20,"target":19},{"source":21,"target":16},{"source":21,"target":17},{"source":21,"target":18},{"source":21,"target":19},{"source":21,"target":20},{"source":22,"target":16},{"source":22,"target":17},{"source":22,"target":18},{"source":22,"target":19},{"source":22,"target":20},{"source":22,"target":21},{"source":23,"target":16},{"source":23,"target":17},{"source":23,"target":18},{"source":23,"target":19},{"source":23,"target":20},{"source":23,"target":21},{"source":23,"target":22},{"source":23,"target":12},{"source":23,"target":11},{"source":24,"target":23},{"source":24,"target":11},{"source":25,"target":24},{"source":25,"target":23},{"source":25,"target":11},{"source":26,"target":24},{"source":26,"target":11},{"source":26,"target":16},{"source":26,"target":25},{"source":27,"target":11},{"source":27,"target":23},{"source":27,"target":25},{"source":27,"target":24},{"source":27,"target":26},{"source":28,"target":11},{"source":28,"target":27},{"source":29,"target":23},{"source":29,"target":27},{"source":29,"target":11},{"source":30,"target":23},{"source":31,"target":30},{"source":31,"target":11},{"source":31,"target":23},{"source":31,"target":27},{"source":32,"target":11},{"source":33,"target":11},{"source":33,"target":27},{"source":34,"target":11},{"source":34,"target":29},{"source":35,"target":11},{"source":35,"target":34},{"source":35,"target":29},{"source":36,"target":34},{"source":36,"target":35},{"source":36,"target":11},{"source":36,"target":29},{"source":37,"target":34},{"source":37,"target":35},{"source":37,"target":36},{"source":37,"target":11},{"source":37,"target":29},{"source":38,"target":34},{"source":38,"target":35},{"source":38,"target":36},{"source":38,"target":37},{"source":38,"target":11},{"source":38,"target":29},{"source":39,"target":25},{"source":40,"target":25},{"source":41,"target":24},{"source":41,"target":25},{"source":42,"target":41},{"source":42,"target":25},{"source":42,"target":24},{"source":43,"target":11},{"source":43,"target":26},{"source":43,"target":27},{"source":44,"target":28},{"source":44,"target":11},{"source":45,"target":28},{"source":47,"target":46},{"source":48,"target":47},{"source":48,"target":25},{"source":48,"target":27},{"source":48,"target":11},{"source":49,"target":26},{"source":49,"target":11},{"source":50,"target":49},{"source":50,"target":24},{"source":51,"target":49},{"source":51,"target":26},{"source":51,"target":11},{"source":52,"target":51},{"source":52,"target":39},{"source":53,"target":51},{"source":54,"target":51},{"source":54,"target":49},{"source":54,"target":26},{"source":55,"target":51},{"source":55,"target":49},{"source":55,"target":39},{"source":55,"target":54},{"source":55,"target":26},{"source":55,"target":11},{"source":55,"target":16},{"source":55,"target":25},{"source":55,"target":41},{"source":55,"target":48},{"source":56,"target":49},{"source":56,"target":55},{"source":57,"target":55},{"source":57,"target":41},{"source":57,"target":48},{"source":58,"target":55},{"source":58,"target":48},{"source":58,"target":27},{"source":58,"target":57},{"source":58,"target":11},{"source":59,"target":58},{"source":59,"target":55},{"source":59,"target":48},{"source":59,"target":57},{"source":60,"target":48},{"source":60,"target":58},{"source":60,"target":59},{"source":61,"target":48},{"source":61,"target":58},{"source":61,"target":60},{"source":61,"target":59},{"source":61,"target":57},{"source":61,"target":55},{"source":62,"target":55},{"source":62,"target":58},{"source":62,"target":59},{"source":62,"target":48},{"source":62,"target":57},{"source":62,"target":41},{"source":62,"target":61},{"source":62,"target":60},{"source":63,"target":59},{"source":63,"target":48},{"source":63,"target":62},{"source":63,"target":57},{"source":63,"target":58},{"source":63,"target":61},{"source":63,"target":60},{"source":63,"target":55},{"source":64,"target":55},{"source":64,"target":62},{"source":64,"target":48},{"source":64,"target":63},{"source":64,"target":58},{"source":64,"target":61},{"source":64,"target":60},{"source":64,"target":59},{"source":64,"target":57},{"source":64,"target":11},{"source":65,"target":63},{"source":65,"target":64},{"source":65,"target":48},{"source":65,"target":62},{"source":65,"target":58},{"source":65,"target":61},{"source":65,"target":60},{"source":65,"target":59},{"source":65,"target":57},{"source":65,"target":55},{"source":66,"target":64},{"source":66,"target":58},{"source":66,"target":59},{"source":66,"target":62},{"source":66,"target":65},{"source":66,"target":48},{"source":66,"target":63},{"source":66,"target":61},{"source":66,"target":60},{"source":67,"target":57},{"source":68,"target":25},{"source":68,"target":11},{"source":68,"target":24},{"source":68,"target":27},{"source":68,"target":48},{"source":68,"target":41},{"source":69,"target":25},{"source":69,"target":68},{"source":69,"target":11},{"source":69,"target":24},{"source":69,"target":27},{"source":69,"target":48},{"source":69,"target":41},{"source":70,"target":25},{"source":70,"target":69},{"source":70,"target":68},{"source":70,"target":11},{"source":70,"target":24},{"source":70,"target":27},{"source":70,"target":41},{"source":70,"target":58},{"source":71,"target":27},{"source":71,"target":69},{"source":71,"target":68},{"source":71,"target":70},{"source":71,"target":11},{"source":71,"target":48},{"source":71,"target":41},{"source":71,"target":25},{"source":72,"target":26},{"source":72,"target":27},{"source":72,"target":11},{"source":73,"target":48},{"source":74,"target":48},{"source":74,"target":73},{"source":75,"target":69},{"source":75,"target":68},{"source":75,"target":25},{"source":75,"target":48},{"source":75,"target":41},{"source":75,"target":70},{"source":75,"target":71},{"source":76,"target":64},{"source":76,"target":65},{"source":76,"target":66},{"source":76,"target":63},{"source":76,"target":62},{"source":76,"target":48},{"source":76,"target":58}]} var width = 400, height = 500, shiftKey; var svg = d3.select("body") .attr("tabindex", 1) .on("keydown.brush", keydown) .on("keyup.brush", keyup) .each(function() { this.focus(); }) .append("svg") .attr("width", width) .attr("height", height); var link = svg.append("g") .attr("class", "link") .selectAll("line"); var brush = svg.append("g") .datum(function() { return {selected: false, previouslySelected: false}; }) .attr("class", "brush"); var node = svg.append("g") .attr("class", "node") .selectAll("circle"); graph.links.forEach(function(d) { d.source = graph.nodes[d.source]; d.target = graph.nodes[d.target]; }); link = link.data(graph.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; }); brush.call(d3.svg.brush() .x(d3.scale.identity().domain([0, width])) .y(d3.scale.identity().domain([0, height])) .on("brushstart", function(d) { node.each(function(d) { d.previouslySelected = shiftKey && d.selected; }); }) .on("brush", function() { var extent = d3.event.target.extent(); node.classed("selected", function(d) { return d.selected = d.previouslySelected ^ (extent[0][0] <= d.x && d.x < extent[1][0] && extent[0][1] <= d.y && d.y < extent[1][1]); }); }) .on("brushend", function() { d3.event.target.clear(); d3.select(this).call(d3.event.target); })); var force = d3.layout.force() .charge(-120) .linkDistance(30) .nodes(graph.nodes) .links(graph.links) .size([width, height]) .start(); node = node.data(graph.nodes).enter().append("circle") .attr("r", 4) .attr("cx", function(d) { return d.x; }) .attr("cy", function(d) { return d.y; }) .on("mousedown", function(d) { if (!d.selected) { // Don't deselect on shift-drag. if (!shiftKey) node.classed("selected", function(p) { return p.selected = d === p; }); else d3.select(this).classed("selected", d.selected = true); } }) .on("mouseup", function(d) { if (d.selected && shiftKey) d3.select(this).classed("selected", d.selected = false); }) .call(d3.behavior.drag() .on("dragstart", function(d1) { node.filter(function(d) { return d.selected; }) .each(function(d) { d.fixed |= 2; }) }) .on("drag", function(d1) { node.filter(function(d) { return d.selected; }) .each(function(d) { d.x += d3.event.dx; d.y += d3.event.dy; d.px += d3.event.dx; d.py += d3.event.dy; }) force.resume(); }) .on("dragend", function(d) { node.filter(function(d) { return d.selected; }) .each(function(d) { d.fixed &= ~6; }) })); 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; }); }; force.on("tick", tick); function keydown() { shiftKey = d3.event.shiftKey || d3.event.metaKey; } function keyup() { shiftKey = d3.event.shiftKey || d3.event.metaKey; } .node { stroke: #fff; stroke-width: 1.5px; } .node .selected { stroke: red; } .link { stroke: #999; } .brush .extent { fill-opacity: .1; stroke: #fff; shape-rendering: crispEdges; } <!DOCTYPE html> <style> </style> <body> <script src="http://d3js.org/d3.v3.min.js"></script> <script> </script>
This D3 block contains a v4 selectable Force-Layout Graph. It is based on the example you gave but using the syntax introduced in v4. Hope it helps.
making d3 force layout work with labeled data
I've been trying to glue ideas from various d3 examples into what i need, starting with the basic example using miserable.json data and then adding: use of a key function for the data joins modifying the underlying graph ala this example making the links' linkDistance() function depend on an attribute of the graph's links' adding labels to the nodes ala this example So far i'm only 3 out of 4: something about using the g elements -- using code taken directly from the Mike's "Labeled Force Layout" example -- breaks things and the nodes aren't drawn. I can make it work if i join circle elements directly, but not if I interpose g elements with attached circles and text elements. The code below is my best effort at a minimal example. This example works, but does not if I replace the .enter().append("circle") line with the .enter().append("g") lines. Does anyone know why? var Width = 200; var Height = 200; var Pix2Len = 10; var color = d3.scale.category10(); var svg = d3.select("body").append("svg") .attr("width", Width) .attr("height", Height); var force = d3.layout.force() .size([Width, Height]) var graph = { "nodes":[ {"name":"Myriel","idx":0}, {"name":"Napoleon","idx":1}, {"name":"Mlle.Baptistine","idx":2}, {"name":"Mme.Magloire","idx":3} ], "links":[ {"source":1,"target":0,"len":1,"idx":"1-0"}, {"source":2,"target":1,"len":4,"idx":"2-1"}, {"source":2,"target":0,"len":8,"idx":"2-0"}, {"source":3,"target":0,"len":10,"idx":"3-0"}, {"source":3,"target":1,"len":4,"idx":"3-1"}, {"source":3,"target":2,"len":6,"idx":"3-2"} ] } console.log("data loaded. nnode="+graph.nodes.length+" nlinks="+graph.links.length); force .nodes(graph.nodes) .links(graph.links) .size([Width, Height]) .linkDistance(function(link) { // console.log("link: "+link.source.name+' '+link.target.name+' '+link.idx+' '+link.len) return link.len * Pix2Len}) .on("tick", tick); var link = svg.selectAll(".link") .data(graph.links, function(d) {return d.idx; }) .enter().append("line") .attr("class", "link"); var node = svg.selectAll(".node") .data(graph.nodes, function(d) {return d.idx; }) // THIS WORKS .enter().append("circle").attr("r", 8).style("fill", function(d) { return color(0); }); // BUT THIS DOES NOT // modeled after http://bl.ocks.org/mbostock/950642 // //.enter().append("g") //.attr("class", "node") //.attr("cx", function(d) { return d.x; }) //.attr("cy", function(d) { return d.y; }); // //node.append("circle") //.attr("r", 10) //.style("fill", function(d) { return color(0); }); // //node.append("text") //.attr("dx", 12) //.attr("dy", ".35em") //.text(function(d) { return d.name }); // 1. Begin with graph from JSON data setTimeout(function() { start(); }, 0); // 2. Change graph topology setTimeout(function() { var new4 = {"name":"CountessdeLo","idx":4} var new5 = {"name":"Geborand","idx":5} graph.nodes.push(new4,new5); var link40 = {"source":4,"target":0,"len":1,"idx":"4-0"}; var link43 = {"source":4,"target":3,"len":4,"idx":"4-3"}; var link50 = {"source":5,"target":0,"len":1,"idx":"5-0"}; var link52 = {"source":5,"target":2,"len":4,"idx":"5-2"}; graph.links.push(link40,link43,link50,link52); start(); }, 3000); //3. Change some link lengths setTimeout(function() { // force.links().forEach(function(link) { graph.links.forEach(function(link) { if (link.idx == '1-0') {link.len=10; } else if (link.idx == '3-0') {link.len=2; } else if (link.idx == '5-0') {link.len=10; }; }); // eo-forEach start(); }, 6000); function start() { link = link.data(force.links(), function(d) { return d.idx; }); link.enter().insert("line", ".node").attr("class", "link"); link.exit().remove(); node = node.data(force.nodes(), function(d) { return d.idx;}); node.enter().append("circle").attr("class", function(d) { // tried with the <g> version above // node.enter().append("g").attr("class", function(d) { console.log('start:'+' '+d.name); return d.idx; }).attr("r", 5).style("fill", function(d) { return color(1); }); node.exit().remove(); force.start(); } function tick() { node.attr("cx", function(d) { // console.log('tick:'+' '+d.name); 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; }); } //} // eo-ready()
In your code you are setting cx and cy attributes to the g element. g elements does not support any position attributes like x, y or cx, cy. To move the contents of a g element you will have to use the transform attribute. Your code var node = svg.selectAll(".node") .data(graph.nodes, function(d) {return d.idx; }) .enter().append("g") .attr("class", "node") .attr("cx", function(d) { return d.x; }) //will not work .attr("cy", function(d) { return d.y; }); //will not work Solution var node = svg.selectAll(".node") .data(graph.nodes, function(d) {return d.idx; }) .enter().append("g") .attr("class", "node"); node.append("circle") .attr("r", 10) .style("fill", function(d) { return color(0); }); node.append("text") .attr("dx", 12) .attr("dy", ".35em") .text(function(d) { return d.name }); Use translate function as below to move group elements. function tick() { //Moving <g> elements using transform attribute node.attr("transform", function(d) { 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; }); } JSFiddle
Implementing fisheye for force directed graph on Rails 4 (using d3.js)
I am trying to implement fisheye distortion on a force directed graph using d3.js. For some reason, the distortion works on the links but not the nodes. This might be caused by the nodes being g elements, but I am not sure. Any help would be appreciated! var svg, node, link; function make_graph() { // Clear out the div first $("#display_graph svg:first").remove(); // Sets size var width = 960, height = 500; // Maps groups to colors var color = d3.scale.category20(); // Select the div and append a svg svg = d3.select("#display_graph").append("svg") .attr("width", width) .attr("height", height); // Pull json from graph var json_from_db = $('.graph_json').data('json'); var json_nodes = json_from_db.nodes var json_links = json_from_db.links // Sets up the force directed graph var charge_val = $('#charge').val(); var link_distance_val = $('#link_distance').val(); var force = d3.layout.force() .charge(charge_val) .linkDistance(link_distance_val) .size([width, height]) .nodes(json_nodes) .links(json_links) .start(); link = svg.selectAll(".link") .data(json_links) .enter().append("line") .attr("class", "link") .style("stroke-width", function(d) { return Math.sqrt(d.value); }); var font_size_px = $('#font_size').val(); node = svg.selectAll(".node") .data(json_nodes) .enter().append("g") .attr("class", "node") .style("font-size", font_size_px) .call(force.drag); var radius = $('#radius').val(); node.append("circle") .attr("r", radius) .style("fill", function(d) { return color(d.group); }); 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 + ")"; }); }); $('#fisheye_btn').click( function() { var glyph_color = $(this).css('color'); // original color is white orig_color = 'rgb(255, 255, 255)'; if (glyph_color == orig_color) { $(this).css('color', 'orange'); } else { $(this).css('color', orig_color); }; }); }; function make_fisheye() { var fisheye = d3.fisheye.circular() .radius(200) .distortion(2); svg.on("mousemove", function() { fisheye.focus(d3.mouse(this)); node.each(function(d) { d.fisheye = fisheye(d); }) .attr("cx", function(d) { return d.fisheye.x; }) .attr("cy", function(d) { return d.fisheye.y; }) .attr("r", function(d) { return d.fisheye.z * 4.5; }); link.attr("x1", function(d) { return d.source.fisheye.x; }) .attr("y1", function(d) { return d.source.fisheye.y; }) .attr("x2", function(d) { return d.target.fisheye.x; }) .attr("y2", function(d) { return d.target.fisheye.y; }); }); } Eventually, I want the user to be able to turn the distortion on or off by clicking a button. Thanks for your help! I updated the code. I can turn the distortion on or off with a button click now. // Make force directed graph on button click $(document).ready( function() { $("#refresh_btn").click( function() { make_graph(); }); }); // Turn on and off fisheye distortion $(document).ready( function() { $('#fisheye_btn').click( function() { var glyph_color = $(this).css('color'); // original color is white orig_color = 'rgb(255, 255, 255)'; if (glyph_color == orig_color) { $(this).css('color', 'orange'); make_graph(); make_fisheye(); } else { $(this).css('color', orig_color); make_graph(); }; }); }); var svg, node, link; function make_graph() { // Clear out the div first $("#display_graph svg:first").remove(); // Sets size var width = 960, height = 500; // Maps groups to colors var color = d3.scale.category20(); // Select the div and append a svg svg = d3.select("#display_graph").append("svg") .attr("width", width) .attr("height", height); // Pull json from graph var json_from_db = $('.graph_json').data('json'); var json_nodes = json_from_db.nodes var json_links = json_from_db.links // Sets up the force directed graph var charge_val = $('#charge').val(); var link_distance_val = $('#link_distance').val(); var force = d3.layout.force() .charge(charge_val) .linkDistance(link_distance_val) .size([width, height]) .nodes(json_nodes) .links(json_links) .start(); link = svg.selectAll(".link") .data(json_links) .enter().append("line") .attr("class", "link") .style("stroke-width", function(d) { return Math.sqrt(d.value); }); var font_size_px = $('#font_size').val(); node = svg.selectAll(".node") .data(json_nodes) .enter().append("g") .attr("class", "node") .style("font-size", font_size_px) .call(force.drag); var radius = $('#radius').val(); node.append("circle") .attr("r", radius) .style("fill", function(d) { return color(d.group); }); 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 + ")"; }); }); }; function make_fisheye() { var fisheye = d3.fisheye.circular() .radius(200) .distortion(2); svg.on("mousemove", function() { fisheye.focus(d3.mouse(this)); node.each(function(d) { d.fisheye = fisheye(d); }) .attr("cx", function(d) { return d.fisheye.x; }) .attr("cy", function(d) { return d.fisheye.y; }) .attr("r", function(d) { return d.fisheye.z * 4.5; }); link.attr("x1", function(d) { return d.source.fisheye.x; }) .attr("y1", function(d) { return d.source.fisheye.y; }) .attr("x2", function(d) { return d.target.fisheye.x; }) .attr("y2", function(d) { return d.target.fisheye.y; }); }); } Unfortunately, I am still having issues with just the links being distorted. The nodes are actually g elements, which I am becoming more and more convinced might be the problem. Is there a way to select elements inside the g element using d3.js? I am surprised that the fisheye distortion is not just passed on to the elements inside the g element. Maybe my guess is wrong? New Code Now both the nodes and links are distorted on a button click! Here is the code: // Global variables var svg, node, link; var fisheye_on = false; // Make force directed graph on button click $(document).ready( function() { $("#refresh_btn").click( function() { // Make sure fisheye is turned off $('#fisheye_btn').css('color', 'white'); fisheye_on = false; make_graph(); }); }); // Turn on and off fisheye distortion $(document).ready( function() { $('#fisheye_btn').click( function() { var glyph_color = $(this).css('color'); // original color is white orig_color = 'rgb(255, 255, 255)'; if (glyph_color == orig_color) { $(this).css('color', 'orange'); fisheye_on = true; make_graph(); } else { $(this).css('color', orig_color); fisheye_on = false; make_graph(); }; }); }); function make_graph() { // Clear out the div first $("#display_graph svg:first").remove(); // Sets size var width = 960, height = 500; // Maps groups to colors var color = d3.scale.category20(); // Select the div and append a svg svg = d3.select("#display_graph").append("svg") .attr("width", width) .attr("height", height); // Pull json from graph var json_from_db = $('.graph_json').data('json'); var json_nodes = json_from_db.nodes var json_links = json_from_db.links // Sets up the force directed graph var charge_val = $('#charge').val(); var link_distance_val = $('#link_distance').val(); var force = d3.layout.force() .charge(charge_val) .linkDistance(link_distance_val) .size([width, height]) .nodes(json_nodes) .links(json_links) .start(); link = svg.selectAll(".link") .data(json_links) .enter().append("line") .attr("class", "link") .style("stroke-width", function(d) { return Math.sqrt(d.value); }); var font_size_px = $('#font_size').val(); node = svg.selectAll(".node") .data(json_nodes) .enter().append("g") .attr("class", "node") .style("font-size", font_size_px) .call(force.drag); var radius = $('#radius').val(); node.append("circle") .attr("r", radius) .style("fill", function(d) { return color(d.group); }); node.append("text") .attr("dx", 12) .attr("dy", ".35em") .text(function(d) { return d.name; }); if (fisheye_on == false) { 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 + ")"; }); }); } else { var fisheye = d3.fisheye.circular() .radius(200) .distortion(2); svg.on("mousemove", function() { fisheye.focus(d3.mouse(this)); node.each(function(d) { d.fisheye = fisheye(d); }); node.selectAll("circle") .attr("cx", function(d) { return d.fisheye.x - d.x; }) .attr("cy", function(d) { return d.fisheye.y - d.y; }) .attr("r", function(d) { return d.fisheye.z * 4.5; }); node.selectAll("text") .attr("dx", function(d) { return d.fisheye.x - d.x; }) .attr("dy", function(d) { return d.fisheye.y - d.y; }); link.attr("x1", function(d) { return d.source.fisheye.x; }) .attr("y1", function(d) { return d.source.fisheye.y; }) .attr("x2", function(d) { return d.target.fisheye.x; }) .attr("y2", function(d) { return d.target.fisheye.y; }); }); }; }; Unfortunately, I have come across another issue. There seems to be an initial lag when the distortion is applied. The nodes get distorted first and then the links get distorted. Not sure what is causing this. Any hints? Public JSFiddle I managed to replicated the issue in a public JSFiddle: http://jsfiddle.net/cspears2002/vVL99/ In order to make the graph, just click "Make Graph". Clicking "Fisheye" will turn the fisheye distortion on and off. The issue is that the nodes are distorted first and then the links follow. I want nodes and links to be distorted at the same time. Any help would be appreciated!
D3 force directed graph node - text is being duplicated when expading
I'm trying to implement the expand / collapse functionality as i saw on the D3 site, and what i get is that some nodes get to have multiple labels when i run this functionality. var width = 960, height = 500; var node, path, root, nodes, links, link; var svg = d3.select("body").append("svg") .attr("width", width) .attr("height", height); var force = d3.layout.force(); var resource = $.getJSON('js/nodes.json', function(data) { root = restructure(data); render(); }) function restructure(data) { var root = data[0]; data.splice(0,1); root.children = data; _.each(root.children, function(child) { child._children = child.links; }) return root; } function flatten(root) { // return root; var nodes = []; var links = []; function rec(node) { var sourceNode = addNode(nodes, node); if (node.children) { //sourceNode.children = []; node.children.forEach(function(child) { var targetNode = rec(child); links.push({source:sourceNode, target:targetNode}); //sourceNode.children.push(targetNode); }) } return sourceNode; } rec(root); return {nodes:d3.values(nodes), links:links}; } function addNode(collection, node) { if(collection[node.name] != null) return collection[node.name]; collection[node.name] = node; return node; } function render() { var flat = flatten(root); var nodes = flat.nodes; var links = flat.links; nodes = tree.nodes(elements.nodes).reverse(); links = tree.links(nodes);*/ force.nodes(nodes) .links(links) .size([width, height]) .linkDistance(160) .charge(-1500) .on("tick", tick) .start(); var drag = force.drag() .on("dragstart", dragstart); function dragstart(d) { d.fixed = true; d3.select(this).classed("fixed", true); } function isRoot(node) { return node.id == root.id } /* link = svg.selectAll(".link") .data(force.links()) .enter().append("line") .attr("class", "link");*/ // Update the links… link = svg.selectAll("line.link") .data(force.links(), function(d) { return d.target.id; }); // 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; }) .style("stroke-width",function(l) { console.log(l.source.rank + ':' + l.target.rank); var val = Math.min(10,Math.max(1,Math.log(Math.max(Math.abs(l.source.rank),Math.abs(l.target.rank))))); console.log('width: ' + val); return val; }); // Exit any old links. link.exit().remove(); node = svg.selectAll(".node") .data(force.nodes()); node.exit().remove(); node.enter().append("g") .attr("class", "node") .attr("id", function(d) { return d.id; }) .on("click", click) .on("dblclick", doubleclick) .on("mouseover", mouseover) .on("mouseout", mouseout) .call(force.drag); node.append("image") .attr("xlink:href", function (d) { return "../img/icon-location.png"; }) .attr("x", -8) .attr("y", -8) .attr("width", 16) .attr("height", 16); /* node.append("circle") .attr("r", function(d) { return isRoot(d)? 14:8; }) .style("fill",function(d) { return isRoot(d)? "steelblue":""; });*/ var center = svg.select('#node_' + nodes[0].id) center.append("circle") .attr("r", "14") .style("fill","steelblue"); node.append("text") .attr("x", 12) .attr("dy", ".35em") .text(function(d) { return d.name; }); node.transition() .attr("r", function(d) { return d.children ? 4.5 : Math.sqrt(d.size) / 10; }); // Exit any old nodes. node.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 + ")"; }); } // Toggle children on click. function doubleclick(d) { alert(d); } // Toggle children on click. function click(d) { if (d.children) { d._children = d.children; d.children = null; } else { d.children = d._children; d._children = null; } render(); } function mouseover() { d3.select(this).select("circle").transition() .duration(750) .attr("r", 16); } function mouseout() { d3.select(this).select("circle").transition() .duration(750) .attr("r", 8); } however, what i get (after expanding) looks like that: <g class="node fixed" id="1063" transform="translate(329.44373878271944,118.27604414379978)" r="NaN"><image xlink:href="../img/icon-location.png" x="-8" y="-8" width="16" height="16"></image><text x="12" dy=".35em">PRO</text><image xlink:href="../img/icon-location.png" x="-8" y="-8" width="16" height="16"></image><text x="12" dy=".35em">Dropbox</text></g> any help?
You want to make sure that updated nodes are correctly matched to the existing DOM elements, which you're already doing for the edges. You'll pass an identity function as the second parameter to the node data() call. Perhaps: node = svg.selectAll(".node") .data(force.nodes(), function(d) { return d.id; }); The one thing you need to look out for is that this function can be called both on new data elements as well as the stored data on nodes being updated. For more information see http://bost.ocks.org/mike/selection/