I have a sticky force layout : http://jsfiddle.net/smqsusdw/
I have this function that drags one node to a position :
function positionnodes(){
force.stop();
node.each(function(d, i){
if(i===1){
d.fixed = true;
d.x = 100;
d.y = 100;
}
}).transition().duration(1000).attr("cx", function(d){ return d.x }).attr("cy", function(d){ return d.y });
link.transition().duration(1000)
.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; });
}
Now when it does this I want it to look like I am dragging it with my mouse. But when I press the button only the chosen node moves. Is there anyway to simulate a mousedrag on the node so that the other related nodes seem to move with it ?
For example, I press the button, only one node moves and all the others stay put.
But when I drag one of the nodes to a position the related nodes kind of move with it due to the D3 force physics. Is there a way to simulate this movement
To choose the right approach it is important to know that in D3's force layout the calculations are decoupled from the actual rendering of any elements. d3.layout.force()
will take care of calculating movements and positions according to the specified parameters. The rendering will be done by the handler registered with .force("tick", renderingHandler)
. This function will get called by the force layout on every tick and render the elements based on the calculated positions.
With this in mind it becomes apparent, that your solution will not work as expected. Using transitions on the graphical elements will just move the nodes around without updating the data and without any involvement of the force layout. To get the desired behavior, you need to stick to the decoupling of calculations and rendering. This will free you from the need to implement a simulation of mouse events.
This could be done by using a d3.timer()
, which will repeatedly invoke a function setting the moving node's position to the interpolated values between its start and end values. After having set these values, the function will activate the force layout to do its work for the rest of the nodes and invoke the rendering handler .tick()
, which will update the entire layout.
function positionnodes(){
var move = graph.nodes[1], // the node to move around
duration = 1000, // duration of the movement
finalPos = { x: 100, y: 100 },
interpolateX = d3.interpolateNumber(move.x, finalPos.x),
interpolateY = d3.interpolateNumber(move.y, finalPos.y);
// We don't want the force layout to mess with our node.
move.fixed = true;
// Move the node by repeatedly determining its position.
d3.timer(function(elapsed) {
// Because the node should remain fixed, the previous position (.px, .py)
// needs to be set to the same value as the new position (.x, .y). This way
// the node will not have any inherent movement.
move.x = move.px = interpolateX(elapsed / duration);
move.y = move.py = interpolateY(elapsed / duration);
// Re-calculate the force layout. This will also invoke tick()
// which will take care of the rendering.
force.start();
// Terminate the timer when the desired duration has elapsed.
return elapsed >= duration;
});
}
Have a look at the following snippet or the updated JSFiddle for a working adaption of your code.
var graph ={
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
{"x": 477, "y": 248},
{"x": 425, "y": 207},
{"x": 402, "y": 155},
{"x": 369, "y": 196},
{"x": 350, "y": 148},
{"x": 539, "y": 222},
{"x": 594, "y": 235},
{"x": 582, "y": 185},
{"x": 633, "y": 200}
],
"links": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2},
{"source": 3, "target": 4},
{"source": 4, "target": 5},
{"source": 5, "target": 6},
{"source": 5, "target": 7},
{"source": 6, "target": 7},
{"source": 6, "target": 8},
{"source": 7, "target": 8},
{"source": 9, "target": 4},
{"source": 9, "target": 11},
{"source": 9, "target": 10},
{"source": 10, "target": 11},
{"source": 11, "target": 12},
{"source": 12, "target": 10}
]
}
var width = 960,
height = 500;
var force = d3.layout.force()
.size([width, height])
.charge(-400)
.linkDistance(40)
.on("tick", tick);
var drag = force.drag()
.on("dragstart", dragstart);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
//d3.json("graph.json", function(error, graph) {
// if (error) throw error;
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 12)
.on("dblclick", dblclick)
.call(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; });
}
function dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
}
function dragstart(d) {
d3.select(this).classed("fixed", d.fixed = true);
}
function positionnodes(){
var move = graph.nodes[1], // the node to move around
duration = 1000, // duration of the movement
finalPos = { x: 100, y: 100 },
interpolateX = d3.interpolateNumber(move.x, finalPos.x),
interpolateY = d3.interpolateNumber(move.y, finalPos.y);
// We don't want the force layout to mess with our node.
move.fixed = true;
// Move the node by repeatedly determining its position.
d3.timer(function(elapsed) {
// Because the node should remain fixed, the previous position (.px, .py)
// needs to be set to the same value as the new position (.x, .y). This way
// the node will not have any inherent movement.
move.x = move.px = interpolateX(elapsed / duration);
move.y = move.py = interpolateY(elapsed / duration);
// Re-calculate the force layout. This will also invoke tick()
// which will take care of the rendering.
force.start();
// Terminate the timer when the desired duration has elapsed.
return elapsed >= duration;
});
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
cursor: move;
fill: #ccc;
stroke: #000;
stroke-width: 1.5px;
}
.node.fixed {
fill: #f00;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<button onclick = 'positionnodes()'> click me</button>
I was playing around with this so I thought I may as well post it as well.
@altocumulus was too fast for me!
Here is a way to do a very similar thing but using a transition. This allows you to access easing, delays and chaining for free as well, so it's easy to generalise to a more complex set of movements.
px
and py
on the chosen data element, to transparently hook up with the transition, by returning the fake cx
and cy
attributes of the dummy node while they are transitioning.end
event of the transition, clean up by replacing the getters with the current value of the dummy node attributes.You can keep clicking the button and it sends the node to random locations.
If you generate the dummy nodes using d3 style data binding then you can easily generalise it to move any number of nodes in unison. In the following example they are filtered on the fixed
property.
var graph ={
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
{"x": 477, "y": 248},
{"x": 425, "y": 207},
{"x": 402, "y": 155},
{"x": 369, "y": 196},
{"x": 350, "y": 148},
{"x": 539, "y": 222},
{"x": 594, "y": 235},
{"x": 582, "y": 185},
{"x": 633, "y": 200}
],
"links": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2},
{"source": 3, "target": 4},
{"source": 4, "target": 5},
{"source": 5, "target": 6},
{"source": 5, "target": 7},
{"source": 6, "target": 7},
{"source": 6, "target": 8},
{"source": 7, "target": 8},
{"source": 9, "target": 4},
{"source": 9, "target": 11},
{"source": 9, "target": 10},
{"source": 10, "target": 11},
{"source": 11, "target": 12},
{"source": 12, "target": 10}
]
}
var width = 500,
height = 190,
steps = function(){return +d3.select("#steps-selector").property("value")};
var force = d3.layout.force()
.size([width, height])
.charge(-100)
.linkDistance(6)
.on("tick", tick);
var drag = force.drag()
.on("dragstart", dragstart);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
//d3.json("graph.json", function(error, graph) {
// if (error) throw error;
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 6)
.on("dblclick", dblclick)
.call(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; });
force.alpha(0.1)
}
function dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
}
function dragstart(d) {
d3.select(this).classed("fixed", d.fixed = true);
}
function positionnodes(){
var ns = "CB:emit/drag/transition/or-whatever-you-feel-like",
shadowNodes = d3.select("body").selectAll("emitDrag")
.data(graph.nodes.filter(function(d){return d.fixed})),
shadowedData = [];
shadowNodes.enter().append(function(){return document.createElementNS(ns, "emitDrag")});
shadowNodes.each(function(d, i){
var n = d3.select(this);
shadowedData[i] = d;
dragstart.call(node.filter(function(s){return s === d;}).node(), d);
d.fixed = true;
n.attr({cx: d.x, cy: d.y});
Object.defineProperties(d, {
px: {
get: function() {return +n.attr("cx")},
configurable: true
},
py: {
get: function() {return +n.attr("cy")},
configurable: true
}
});
});
force.start();
d3.range(steps()).reduce(function(o, s){
return o.transition().duration(750).ease("cubic")
.attr({
cx: function(){return (1+3*Math.random())*width*0.2},
cy: function(){return (1+3*Math.random())*height*0.2}
})
},shadowNodes)
.each("end", function(d, i){
var n = d3.select(this);
Object.defineProperties(shadowedData[i], {
px: {value: +n.attr("cx"), writable: true},
py: {value: +n.attr("cy"), writable: true}
});
});
}
body {
margin: 0;
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
cursor: move;
fill: #ccc;
stroke: #000;
stroke-width: 1.5px;
}
.node.fixed {
fill: #f00;
}
button, input {display: inline-block}
.input {
position: absolute;
top: 0;
left: 0;
/*white-space: pre;*/
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<div class="input">
<button onclick = 'positionnodes()'> select the nodes to include then click me</button>
steps <input id="steps-selector" onchange = 'positionnodes()' type="number" name="steps" value = 3 min="1" max="100"/>
</div>
Here are a few more possibilities, all due to the power of d3 transitions...
var graph ={
"nodes": [
{"x": 469, "y": 410, move: true},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
{"x": 477, "y": 248, move: true},
{"x": 425, "y": 207},
{"x": 402, "y": 155},
{"x": 369, "y": 196},
{"x": 350, "y": 148},
{"x": 539, "y": 222},
{"x": 594, "y": 235},
{"x": 582, "y": 185},
{"x": 633, "y": 200, move: true}
],
"links": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2},
{"source": 3, "target": 4},
{"source": 4, "target": 5},
{"source": 5, "target": 6},
{"source": 5, "target": 7},
{"source": 6, "target": 7},
{"source": 6, "target": 8},
{"source": 7, "target": 8},
{"source": 9, "target": 4},
{"source": 9, "target": 11},
{"source": 9, "target": 10},
{"source": 10, "target": 11},
{"source": 11, "target": 12},
{"source": 12, "target": 10}
]
}
var width = 500,
height = 190,
steps = function(){return +d3.select("#steps-selector").property("value")};
var inputDiv = d3.select("#input-div"),
tooltip = (function tooTip() {
var tt = d3.select("body").append("div")
.attr("id", "tool-tip")
.style({
position: "absolute",
color: "black",
background: "rgba(0,0,0,0)",
display: "none"
});
return function(message) {
return message ?
function() {
var rect = this.getBoundingClientRect();
tt
.style({
top: (rect.bottom + 6) + "px",
left: (rect.right + rect.left) / 2 + "px",
width: "10px",
padding: "0 1em 0 1em",
background: "#ccc",
'border-radius': "2px",
display: "inline-block"
})
.text(message)
}:
function() {
tt
.style({
display: "none"
})
}
}
})(),
easeings = ["linear", "quad", "cubic", "sin", "exp", "circle", "elastic", "back", "bounce"],
xEase = d3.ui.select({
base: d3.select("#input-div"),
oninput: positionnodes,
data: easeings,
initial: "bounce",
onmouseover: tooltip("x"),
onmouseout: tooltip()
}),
yEase = d3.ui.select({
base: d3.select("#input-div"),
oninput: positionnodes,
data: easeings,
initial: "circle",
onmouseover: tooltip("y"),
onmouseout: tooltip()
}),
t = (function(){
var s = d3.select("#input-div").selectAll(".time")
.data([{name: "tx", value: 0.75}, {name: "ty", value: 1.6}])
.enter().append("input")
.attr({
id: function(d){return d.name + "-selector"},
type: "number",
name: function(d){return d.name},
value: function(d){return d.value},
min: "0.1", max: "5", step: 0.5
})
.on("change", positionnodes)
.each(function(d){
d3.select(this).on("mouseover", tooltip(d.name))
})
.on("mouseout", tooltip());
return function(){
var values = [];
s.each(function(){
values.push(d3.select(this).property("value") * 1000);
});
return values;
}
})();
var force = d3.layout.force()
.size([width, height])
.charge(-100)
.linkDistance(6)
.on("tick", tick);
var drag = force.drag()
.on("dragstart", dragstart);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
//d3.json("graph.json", function(error, graph) {
// if (error) throw error;
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 6)
.on("dblclick", dblclick)
.call(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; });
force.alpha(0.1)
}
function dblclick(d) {
d3.select(this).classed("fixed", d.move = false);
}
function dragstart(d) {
d3.select(this).classed("fixed", d.move = true);
}
function positionnodes(){
var ns = "CB:emit/drag/transition/or-whatever-you-feel-like",
transitions = d3.select("body").selectAll("transitions")
.data([graph.nodes.filter(function(d){return d.move})]),
transitionsEnter = transitions.enter().append(function(){
return document.createElementNS(ns, "transitions")
}),
shadowNodes = transitions.selectAll("emitDrag")
.data(function(d){return d}),
shadowedData = [];
shadowNodes.enter().append(function(){
return document.createElementNS(ns, "emitDrag")
});
shadowNodes.each(function(d, i){
var n = d3.select(this);
shadowedData[i] = d;
dragstart.call(node.filter(function(s){return s === d;}).node(), d),
endAll = d3.cbTransition.endAll();
n.attr({cx: d.x, cy: d.y});
Object.defineProperties(d, {
px: {
get: function() {return d.x = +n.attr("cx")},
configurable: true
},
py: {
get: function() {return d.y = +n.attr("cy")},
configurable: true
}
});
});
force.start();
d3.range(steps()).reduce(function(o){
return (o.transition("cx").duration(t()[0]).ease(xEase.value())
.attr({
cx: function(d){
// return d.x + (Math.random() - 0.5) * width/5
return (1+3*Math.random())*width*0.2
}
}))
},shadowNodes)
.call(cleanUp, "px", "cx");
d3.range(steps()).reduce(function(o){
return (o.transition("cy").duration(t()[1]).ease(yEase.value())
.attr({
cy: function(d){
// return d.y + (Math.random() - 0.5) * height/5
return (1+3*Math.random())*height*0.2
}
}))
},shadowNodes)
.call(cleanUp, "py", "cy");
function cleanUp(selection, getter, attribute){
selection.each("end.each", function(d, i){
var n = d3.select(this);
Object.defineProperty(shadowedData[i], getter, {
value: +n.attr(attribute), writable: true
});
})
.call(endAll, function(){
transitions.remove();
}, "move-node");
}
}
positionnodes()
body {
margin: 0;
position: relative;
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
cursor: move;
fill: #ccc;
stroke: #000;
stroke-width: 1.5px;
}
.node.fixed {
fill: #f00;
}
button, input {display: inline-block}
.input-div {
position: absolute;
top: 0;
left: 0;
/*white-space: pre;*/
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://rawgit.com/cool-Blue/d3-lib/master/transitions/end-all/1.0.0/endAll.js" charset="UTF-8"></script>
<script src="https://rawgit.com/cool-Blue/d3-lib/master/inputs/select/select.js" charset="UTF-8"></script>
<div id="input-div">
<button onclick = 'positionnodes()'> select the nodes to include then click me</button>
steps <input id="steps-selector" onchange = 'positionnodes()' type="number" name="steps" value = 10 min="1" max="100"/>
</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With