so, here I am with another of my questions:
I have a network set up in D3. Its nodes are all unfixed but one. All unfixed can be dragged wherever you want, but within the svg. To provide this I test the new coordinates at the end of the script. But if I move the fixed node all the other are dragged together with this one.
My problem is, that the nodes seem to get their coordinates relative to the parent object, which, in this case, is a g-Elements that provides the group-dragging. So after dragging they still have the restrictions, but they are shifted together with the group. Any useful hints welcome :)
And here are parts of my code:
var node = group.selectAll(".node")
.data(reqNodes)
.enter().append("g")
.attr("transform", function(d) { return "translate(0,0)"; })
.attr("class", function(d) {
return d.fixed ? 'node fixed' : 'node not-fixed';
})
var CurrentGTransformX = 0;
var CurrentGTransformY = 0;
group.selectAll('.not-fixed')
.call(force.drag);
// attach a different drag handler to the fixed node
var groupDrag = d3.behavior.drag()
.on("drag", function(d) {
// mouse pos offset by starting node pos
var x = window.event.clientX - 420,
y = window.event.clientY - 260;
group.attr("transform", function(d) { return "translate(" + x + "," + y + ")"; });
//that was the line getting in conflict with the part in the end
CurrentGTransformX = x;
CurrentGTransformY = y;
});
group.call(groupDrag);
force.on("tick", function() {
node.attr("cx", function(d) { return d.x = Math.max(15, Math.min(width - 15, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(15, Math.min(height - 15, 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; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
edit: the transformation of the group is the reason why the restrictions in the end get other data than displayed. Any idea how to serve both functions in the proper way?
Related
Let me preface this by saying I'm not even 100% sure that this should work. I totally apologize for ignorance in this subject; it's something I'm very new to.
I am using a Force Layout like this:
var force = d3.layout.force()
.charge(-1500)
.linkDistance(250)
.size([parseInt(svg.style('width')), parseInt(svg.style('height'))]);
Later on down the road I'm binding nodes like this:
var node = svg.selectAll(".node")
.data(scope.nodes);
var nodeg = node.enter().append("g")
.attr("class", "node")
.on('click', function (n) {
d3.select(this)
.attr('transform', 'translate(' + n.x + ', ' + n.y + ') scale(1.5)')
})
.call(force.drag);
The code I'm trying to implement is this:
.on('click', function (n) {
d3.select(this)
.attr('transform', 'translate(' + n.x + ', ' + n.y + ') scale(1.5)')
})
I thought, based on some research I've done, that this would cause the node to scale or zoom when clicked. However, even though the code executes, nothing is happening. When looking at the Elements page I don't even see the transform added.
I wonder if this is because of the constantly changing transform applied by the Force Layout? It's effectively overriden maybe?
At any rate, the goal is to get this node to scale. It has an image and a couple text objects inside of it added like this:
nodeg.append("image")
.attr("xlink:href", function (d) {
return d.avatar || 'https://github.com/favicon.ico'
})
.attr("x", -56)
.attr("y", -8)
.attr("width", 64)
.attr("height", 64);
nodeg.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.attr('class', 'name')
.text(function (d) {
var name;
if (d._id === scope.user.profile._id) {
name = 'You';
} else if (d.firstName) {
name = d.firstName;
if (d.lastName) {
name += ' ' + d.lastName;
}
} else if (d.lastName) {
name = d.lastName;
}
return name;
});
nodeg.append("text")
.attr("dx", 12)
.attr("dy", "1.35em")
.text(function (d) {
return d.relationship;
});
tick function
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 + ")";
});
});
Thanks to Lars, once again, I've got a beautiful solution! I added a new function that builds the appropriate transform:
function getNodeTransform(n) {
var transform = 'translate(' + n.x + ', ' + n.y + ')';
if (n.fixed === true) {
console.log('Adding scale to node:', n);
transform += ' scale(1.4)';
}
return transform;
}
The next step was to use that in both the click and the tick:
.on('click', function (n) {
n.fixed = !n.fixed;
d3.select(this)
.transition('bounce')
.attr('transform', getNodeTransform(n));
})
force.on("tick", function () {
link.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
node.attr("transform", function (d) {
return getNodeTransform(d);
});
});
This allows me to select nodes even while the layout is settling.
I have a force network something similar to this :
http://jsfiddle.net/Brb29/7/
On the button press i translate the nodes to the same position :
function positionnodes(){
force.stop();
nodes.each(function(d){
d.fixed = true;
d.x = 100;
d.y = 100;
}).transition().duration(1000).attr("transform", function(d)
{
//console.log(d.hasRelationship);
//console.log(d.y);
return "translate(" + 100 + "," + 100 + ")";
});
edges.transition().duration(1000).attr("x1", function (d) {
console.log(d.source.x)
return d.source.x;
})
.attr("y1", function (d) {console.log(d.source.y)
return d.source.y;
})
.attr("x2", function (d) {console.log(d.target.x)
return d.target.x;
})
.attr("y2", function (d) {console.log(d.target.y)
return d.target.y;
});
//setTimeout(function(){ force.start();},1000);
}
My problem is, after i do this and i go to drag the nodes, the node position jumps back to its previous even though i have set it. It's as if the d.x/d.y hasn't updated.
Any ideas ?
The drag event will trigger force.start and then (x,y) of all nodes will be re-computed. To avoid triggering force.start, you need overwrite the event handler for force.drag, see
here
OK. Here is a working example: Working Example. The core part is here:
var drag = force.drag()
.origin(function(d) {
var t =
d3.transform(d3.select(this).attr("transform")).translate;
return {x: t[0], y: t[1]};
})
.on("drag.force", function(d) {
var cord = [0,0];
cord = d3.mouse(this);
d.x = cord[0];
d.y = cord[1];
tick();
});
I'm trying to apply the fisheye effect to a force layout with collision detection, node dragging, zoom etc but am struggling to integrate a fisheye with my tick function.
The closest existing example I found was http://bl.ocks.org/bentwonk/2514276 but it doesn't really contain a custom tick function or enable the usual things like dragging a node while fisheye is applied. For the sake of clarity, here's my tick function regardless of fisheye...
function tick(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.each(gravity(.2 * e.alpha))
.each(collide(jitter))
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
And here's what I'm hoping to use on my svg listener to trigger fisheye...
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 * 8; });
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; });
});
But when using both together I notice no fisheye effect - presumably tick() is overriding any changes from my svg listener (although even if I add force.stop() to my svg listener I still see no fisheye on the page or any console errors).
And of course if I replace the tick function, then the whole node layout won't compute properly to start with.
Apologies that I don't have a more specific question, but does anyone have any thoughts on the best way to approach combining these 2 behaviours so that I can use a fisheye effect without compromising on other force layout functionality?
Thanks!
EDIT:
My only thought so far is to simply use the svg listener to trigger the mouse position...
svg.on("mousemove", function() {
//force.stop();
fisheye.focus(d3.mouse(this));
});
And then have some kind of coordinate addition in the tick function...
node.each(gravity(.2 * e.alpha))
.each(collide(jitter))
.each(function(d) { d.fisheye = fisheye(d); })
.attr("r", function(d) { return d.fisheye.z * 8; })
.attr("transform", function(d) {
return "translate(" + d.x + d.fisheye.x + "," + d.y + d.fisheye.y + ")";
});
(the above doesn't work though - just produces invalid coordinates so all nodes end up at the top-left of the screen)
I am just starting on D3, so if anyone has any general suggestions on thing I might not be doing correctly/optimally, please let me know :)
I am trying to create a Force Directed graph with the nodes spaced out evenly (or close enough) around the center root node (noted by the larger size).
Here's an example of the layout I'm trying to achieve (I understand it won't be the same every time):
I have the following graph:
var width = $("#theVizness").width(),
height = $("#theVizness").height();
var color = d3.scale.ordinal().range(["#ff0000", "#fff000", "#ff4900"]);
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("#theVizness").append("svg")
.attr("width", width)
.attr("height", height);
var loading = svg.append("text")
.attr("class", "loading")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Loading...");
/*
ForceDirectData.json
{
"nodes":[
{"name":"File1.exe","colorGroup":0},
{"name":"File2.exe","colorGroup":0},
{"name":"File3.exe","colorGroup":0},
{"name":"File4.exe","colorGroup":0},
{"name":"File5.exe","colorGroup":0},
{"name":"File6.exe","colorGroup":0},
{"name":"File7.exe","colorGroup":0},
{"name":"File8.exe","colorGroup":0},
{"name":"File8.exe","colorGroup":0},
{"name":"File9.exe","colorGroup":0}
],
"links":[
{"source":1,"target":0,"value":10},
{"source":2,"target":0,"value":35},
{"source":3,"target":0,"value":50},
{"source":4,"target":0,"value":50},
{"source":5,"target":0,"value":65},
{"source":6,"target":0,"value":65},
{"source":7,"target":0,"value":81},
{"source":8,"target":0,"value":98},
{"source":9,"target":0,"value":100}
]
}
*/
d3.json("https://dl.dropboxusercontent.com/u/5772230/ForceDirectData.json", function (error, json) {
var nodes = json.nodes;
force.nodes(nodes)
.links(json.links)
.linkDistance(function (d) {
return d.value * 1.5;
})
.charge(function(d){
var charge = -500;
if (d.index === 0) charge = 0;
return charge;
})
.friction(0.4);
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", 1);
var files = svg.selectAll(".file")
.data(json.nodes)
.enter().append("circle")
.attr("class", "file")
.attr("r", 10)
.attr("fill", function (d) {
return color(d.colorGroup);
});
var totalNodes = files[0].length;
files.append("title")
.text(function (d) { return d.name; });
force.start();
for (var i = totalNodes * totalNodes; i > 0; --i) force.tick();
nodes[0].x = width / 2;
nodes[0].y = height / 2;
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; });
files.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("class", function(d){
var classString = "file"
if (d.index === 0) classString += " rootFile";
return classString;
})
.attr("r", function(d){
var radius = 10;
if (d.index === 0) radius = radius * 2;
return radius;
});
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; });
files.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
loading.remove();
});
JSFiddle
I have already tried getting close to this with the charge() method. I thought giving every node besides the root node a higher charge would accomplish this, but it did not.
What can I do to have the child nodes evenly spaced around the root node?
Yes, force layout is a perfect tool for situations like yours.
You just need to change a little initialization of the layout, like this
force.nodes(nodes)
.links(json.links)
.charge(function(d){
var charge = -500;
if (d.index === 0) charge = 10 * charge;
return charge;
});
and voila
Explanation. I had to remove settings for friction and linkDistance since they affected placement in a bad way. The charge for root node is 10 times larger so that all other nodes are dominantly pushed away from the root. Other nodes also repel each other, and the perfect symmetry is achieved at the end, as a result.
Jsfiddle is here.
I see from your code that you attempted to affect distance from the root node and other nodes by utilizing linkDistance that is dependant on data. However, it might be better (although counter-intuitive) to use linkStrength for that purpose, like this
force.nodes(nodes)
.links(json.links)
.linkStrength(function (d) {
return d.value / 100.0;
})
.charge(function(d){
var charge = -500;
if (d.index === 0) charge = 10 * charge;
return charge;
});
but you need to experiment.
For centering and fixing the root node, you can use this
nodes[0].fixed = true;
nodes[0].x = width / 2;
nodes[0].y = height / 2;
but before initialization of layout, like in this Jsfiddle.
I've been working with d3 for a while now in an attempt to create an interactive ecosystem explorer tool that maps out relationships between species. Recently I've tried adding a feature that lets users show or hide species (nodes) in the force directed graph. I've tried following other examples and although the code works - it only works inconsistently.
For some reason, when I add back a node, it sometimes isn't visible. The graph moves as if the node is added but then it doesn't show up. I have the feeling that it is adding it but then the node is being hidden again in the force.on("tick") code but have no idea why. I've posted the relevant code below and would really appreciate any ideas! The toggleNode function determines whether a node should be shown or hidden - basically just splicing or adding to the nodes array. I keep the data in an array called dataset that stores a flag to indicate whether a node is visible or not.
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);
var nodes = force.nodes(), links = force.links(); // arrays to hold data
var vis = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h);
force.on("tick", function() {
vis.selectAll("circle.node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
vis.selectAll("line.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; });
});
function restart() {
// UPDATE nodes
var node = vis.selectAll("circle.node")
.data(nodes, function(d) { return d.id;});
// ENTER new nodes
var nodeEnter = node.enter().append("svg:circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.style("fill", function(d) { return groupColors[(d.group)-1]; })
.style("stroke","fff")
.style("stroke-width",1)
.on("mousedown", function(d) {
clickNode(d);
})
.call(force.drag);
// REMOVE deleted nodes
var nodeExit = node.exit().remove();
force.start();
}
// Add the nodes and links to the vis
function createVis(json) {
dataset = json; // store data in global
for (var i = 0; i < dataset['nodes'].length; i++) {
// fill node info
nodes.push(dataset['nodes'][i]);
}
restart();
}
// Remove node and associated links.
function toggleNode(nodeKey,eol_id) {
console.log(nodeKey + ': ' + eol_id);
var tLabel; // value for toggle label
if ( dataset['nodes'][nodeKey]['isHidden'] == 0 ) {
// node is visible, so hide it
tLabel = 'Show';
for( var k=0; k<nodes.length; k++ ) {
if ( nodes[k]['eol_id'] == eol_id ) {
nodes.splice(k, 1); // remove this node
break;
}
}
dataset['nodes'][nodeKey]['isHidden'] = 1;
console.log('node removed: ' + nodeKey);
} else {
dataset['nodes'][nodeKey]['isHidden'] = 0;
nodes.push(dataset['nodes'][nodeKey]);
tLabel = 'Hide';
}
$('#primary_title_toggle').html(' ' + tLabel + '<br>');
restart();
}