How to move a node from one simulation to another - javascript

I'm trying to move nodes from one forceSimulation to another, and I'm expecting the nodes to leave one cluster and drift over to another. However, it seems that once the simulation has settled into a stable pattern the forces stop being applied.
Here's the codepen:
Migrating Nodes
And a snippet:
const width = 600;
const height = 200;
var nodes;
var simulations;
function loaded() {
d3.select("svg")
.attr("width", width)
.attr("height", height)
test();
}
function test() {
nodes = [];
nodes[0] = d3.range(50).map(function() {
return {
radius: 4,
color: "blue"
};
})
nodes[1] = d3.range(50).map(function() {
return {
radius: 4,
color: "red"
};
})
simulations = [null, null];
update(0);
update(1);
setTimeout(startMigration, 1000);
}
function update(index) {
simulations[index] = d3.forceSimulation(nodes[index])
.force('charge', d3.forceManyBody().strength(10))
.force('x', d3.forceX().x(function(d) {
return width * (index + 1) / 3;
}))
.force('y', d3.forceY().y(function(d) {
return height / 2;
}))
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius;
}))
.on('tick', ticked);
function ticked() {
var u = d3.select('svg')
.selectAll('.circle' + index)
.data(nodes[index])
.join('circle')
.attr('class', 'circle' + index)
.attr('r', function(d) {
return d.radius;
})
.style('fill', function(d) {
//return colorScale(d.category);
return d.color;
})
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
}
}
function startMigration() {
setInterval(function() {
if (nodes[1].length > 10) {
nodes[0].push(nodes[1].pop());
d3.select('svg')
.selectAll('.circle0')
.data(nodes[0]);
simulations[0].nodes(nodes[0]);
d3.select('svg')
.selectAll('.circle1')
.data(nodes[1]);
simulations[1].nodes(nodes[1]);
}
}, 250);
}
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body onload="loaded()">
<div id='layout'>
<div id='container'>
<svg id="graph" />
</div>
</div>
</body>
</html>
nodes is an array of two datasets, and simulations is an array of two simulations.
Here is where I create the forceSimulations:
function update(index) {
simulations[index] = d3.forceSimulation(nodes[index])
.force('charge', d3.forceManyBody().strength(10))
.force('x', d3.forceX().x(function(d) {
return width * (index + 1) / 3;
}))
.force('y', d3.forceY().y(function(d) {
return height/2;
}))
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius;
}))
.on('tick', ticked);
function ticked() {
var u = d3.select('svg')
.selectAll('.circle' + index)
.data(nodes[index])
.join('circle')
.attr('class', 'circle' + index)
.attr('r', function(d) {
return d.radius;
})
.style('fill', function(d) {
//return colorScale(d.category);
return d.color;
})
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
}
}
A this is the code that starts getting called periodically 1000ms after the simulation starts to move nodes from one simulation to the other:
nodes[0].push(nodes[1].pop());
d3.select('svg')
.selectAll('.circle0')
.data(nodes[0]);
simulations[0].nodes(nodes[0]);
d3.select('svg')
.selectAll('.circle1')
.data(nodes[1]);
simulations[1].nodes(nodes[1]);
The effect I was hoping for is that nodes would leave the red cluster and drift over to join the blue cluster. Why do they slow down and stop? The problem gets worse the longer I wait after the initial load. I get the feeling there's something fundamental about forceSimulation I'm not understanding. I would have thought that they would get as close as they could to the (forceX, forceY) of the simulation to which they belong.
A secondary question is whether all the steps in the code snippet above are necessary. Do I need to reassign data to both selections, and reassign nodes to both simulations?

Lots of force simulations with user interaction 'reheat' the simulation during drag operations with simulation.alpha(n).restart(). See this observable for example.
You can do this without user interaction in setMigration function to keep the simulation animation updated (keep it 'warm' so to speak) until sufficient circles have moved from 'red' to 'blue'. The two lines to add are:
simulations[0].alpha(0.15).restart();
simulations[1].alpha(0.15).restart();
If you increase 0.15 (upto 1) you will see that both simulations are more 'excitable' as the migration occurs. 0.15 seemed like a pleasant visual experience for me. This seems to work without the .restart() so you can play around with that too likely depending on how long you wait to start setMigration.
For your second question - since each simulation was initialised to nodes[0] and nodes[1] then the d3.select('svg').selectAll('.circleN') can be commented out because you already changed the array content with nodes[0].push(nodes[1].pop()).
Working example below:
const width = 600;
const height = 200;
var nodes;
var simulations;
function loaded() {
d3.select("svg")
.attr("width", width)
.attr("height", height)
test();
}
function test() {
nodes = [];
nodes[0] = d3.range(50).map(function() {
return {
radius: 4,
color: "blue"
};
})
nodes[1] = d3.range(50).map(function() {
return {
radius: 4,
color: "red"
};
})
simulations = [null, null];
update(0);
update(1);
setTimeout(startMigration, 1000);
}
function update(index) {
simulations[index] = d3.forceSimulation(nodes[index])
.force('charge', d3.forceManyBody().strength(10))
.force('x', d3.forceX().x(function(d) {
return width * (index + 1) / 3;
}))
.force('y', d3.forceY().y(function(d) {
return height / 2;
}))
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius;
}))
.on('tick', ticked);
function ticked() {
var u = d3.select('svg')
.selectAll('.circle' + index)
.data(nodes[index])
.join('circle')
.attr('class', 'circle' + index)
.attr('r', function(d) {
return d.radius;
})
.style('fill', function(d) {
//return colorScale(d.category);
return d.color;
})
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
}
}
function startMigration() {
setInterval(function() {
if (nodes[1].length > 10) {
nodes[0].push(nodes[1].pop());
//d3.select('svg')
// .selectAll('.circle0')
// .data(nodes[0]);
simulations[0].nodes(nodes[0]);
//d3.select('svg')
// .selectAll('.circle1')
// .data(nodes[1]);
simulations[1].nodes(nodes[1]);
// reheat the simulations
simulations[0].alpha(0.15).restart();
simulations[1].alpha(0.15).restart();
}
}, 250);
}
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body onload="loaded()">
<div id='layout'>
<div id='container'>
<svg id="graph" />
</div>
</div>
</body>
</html>

Related

d3js v7: animate projectile between two nodes in forceSimulation

I have the following implementation of two nodes in a d3 v7 forceSimulation:
function anim() {
d3.select("#test").attr("class", "link anim");
setTimeout(() => {
d3.select("#test").attr("class", "link");
}, 1000);
}
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
graph = {
nodes: [],
links: [],
}
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter(width / 2, height / 2).strength(0.01))
.nodes(graph.nodes)
.force("link", d3.forceLink(graph.links).distance(100))
.on("tick", function() {
svg.selectAll('.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 })
svg.selectAll('.node')
.attr("cx", function (d) { return d.x })
.attr("cy", function (d) { return d.y })
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
})
}).alphaDecay(0.0002)
function update() {
// update links
var link = svg.selectAll('.link').data(graph.links);
link.enter()
.insert('line', '.node')
.attr('class', 'link')
.attr('id', "test")
.style('stroke', '#d9d9d9');
link
.exit()
.remove()
// update nodes
var node = svg.selectAll('.node').data(graph.nodes);
var g = node.enter()
.append('g')
.attr('class', 'node');
g.append('circle')
.attr("r", 20)
.style("fill", "#d9d9d9")
.attr("cursor", "move");
g.append('text')
.attr("class", "text")
.attr("dy", -25)
.text(function (d) { return d.name });
var drag = d3.drag()
.on("drag", function(event, d) {
d.fx = event.x;
d.fy = event.y;
simulation.alpha(0.3).restart();
})
.on("end", function(event, d) {
d.fx = null;
d.fy = null;
});
node.call(drag);
node
.exit()
.remove();
// update simulation
simulation
.nodes(graph.nodes)
.force("link", d3.forceLink(graph.links).distance(100))
.force("charge", d3.forceManyBody().strength(-200))
.restart()
};
function addNode(node) {
graph.nodes.push(node);
update();
};
function connectNodes(source, target) {
graph.links.push({
source: source,
target: target,
});
update();
};
addNode({
id: "you",
name: "you",
});
addNode({
id: "other",
name: "other"
});
connectNodes(0, 1);
.anim {
animation: dash 1s linear forwards;
}
#keyframes dash {
from {
stroke-dasharray: 100;
stroke-dashoffset: 100;
}
to {
stroke-dasharray: 100;
stroke-dashoffset: 0;
}
}
<html>
<head>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<svg width="400" height="200"></svg>
<button onclick="anim()">Animate</button>
</body>
</html>
When the user clicks the "Animate" button, the line between the nodes gets redrawn. This solution is based of an article on how to animate lines being drawn. However, 1) I would like the line to stay static, and have the animation draw a projectile between the nodes instead - like a bullet being fired from "you" to "other". I couldn't figure out how do this with d3.
And 2) is there a better solution than removing the anim class after three seconds inside the anim() function ? I believe one could reset the css animation instead as suggested here: https://css-tricks.com/restart-css-animation/, but I am not sure how this would work in the d3 context. A problem with my solution is that spamming the Animate button does not reset the animation properly. With 1) implemented I would like the spamming effect two draw multiple bullets in succession (it does not need to wait for the animation to finish).

Add node specific links to forceSimulation force-directed graph in d3.js

I have this force graph. I want to have each circle contain a link to a different page, such that when clicked the viewer is redirected.
Is there a way to do this?
Further, it would be nice to have text attached (inside) each node too, but this is much less of a priority.
var numNodes = 12
var nodes = d3.range(numNodes).map(function(d) {
return {
radius: Math.random() * 20 + 40
}
})
var simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(4))
.force('center', d3.forceCenter(400, 250))
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius
}))
.on('tick', ticked);
function orb() {
var u = d3.select('svg')
.selectAll('circle')
.data(nodes)
u.enter()
.append('circle')
.attr('r', function(d) {
return d.radius
})
.merge(u)
.attr('cx', function(d) {
return d.x
})
.attr('cy', function(d) {
return d.y
})
u.exit().remove()
}
function ticked() {
orb()
}
circle:hover {
fill: red;
stroke: #444;
stroke-width: 0.5;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.5.0/d3.min.js"></script>
<svg width="800" height="800" class=''>
<g>
</g>
</svg>
See if this answers both your questions. I've commented it well; let me know if anything is unclear:
<!DOCTYPE html>
<html>
<head>
<style>
circle:hover {
fill: red;
stroke: #444;
stroke-width: 0.5;
}
text {
fill: #fff;
}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.5.0/d3.min.js"></script>
<svg width="800" height="800" class="">
<g></g>
</svg>
<script>
var numNodes = 12;
var nodes = d3.range(numNodes).map(function (d) {
return {
radius: Math.random() * 20 + 40,
link:
Math.random() < 0.5
? 'https://www.wikipedia.org'
: 'https://www.google.com',
text: Math.random() < 0.5 ? 'text' : 'string'
};
});
var simulation = d3
.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(4))
.force('center', d3.forceCenter(400, 250))
.force(
'collision',
d3.forceCollide().radius(function (d) {
return d.radius;
})
)
.on('tick', ticked);
function orb() {
// update selection
let g = d3.select('svg')
.selectAll('g')
.data(nodes);
// enter selection
let ge =
g.enter()
.append('g');
// on enter append `a`
// around the circle
ge
.append('a')
.attr('href', function (d) {
return d.link;
})
.attr('target', '_blank')
.append('circle')
.attr('r', function (d) {
return d.radius;
});
// on enter create text element
// for each circle
ge
.append('text')
.attr('text-anchor', 'middle')
.text(function(d){
return d.text;
})
// merge update and enter
g = ge.merge(g);
// position circle
g
.selectAll('circle')
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
// position text "in" circle
g.selectAll('text')
.attr('transform', function(d){
return 'translate(' + d.x + ',' + d.y + ')';
})
g.exit().remove();
}
function ticked() {
orb();
}
</script>
</body>
</html>

D3js Force-directed graph link intersections avoid

There is an example of force-directed graph i've tried to draw with the help of the d3.js.
I have 3 big questions at all. And this is the code (you can run code snippet below, it might works):
function getRandomInt(max, min = 0) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function fdSortShit(g, nodeDimensions) {
const gNodes = [];
const gLinks = [];
g.children().forEach(child => {
gNodes.push({
id: child,
w: nodeDimensions[child].w,
h: nodeDimensions[child].h,
radius:
Math.sqrt(
nodeDimensions[child].w * nodeDimensions[child].w + nodeDimensions[child].h * nodeDimensions[child].h
) / 2
});
});
g.edges().forEach(edge => {
gLinks.push({ source: edge.v, target: edge.w });
});
const data = {
nodes: gNodes,
links: gLinks
};
const nodes = data.nodes;
const links = data.links;
const linkNodeRad = 5;
const linkNodes = [];
links.forEach((link, idx) => {
if (link.source != link.target) {
linkNodes.push({
id: `link-node-${idx}`,
source: nodes.filter(e => {
return e.id == link.source;
})[0],
target: nodes.filter(e => {
return e.id == link.target;
})[0],
radius: linkNodeRad
});
}
});
const width = 800;
const height = 600;
var svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", "-400, -300, 800, 600");
function forceSimulation(nodes, links) {
return d3
.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter())
.force(
"collision",
d3.forceCollide().radius(function(d) {
return d.radius;
})
);
}
var link = svg
.selectAll(".link")
.attr("stroke", "#fff")
.data(links)
.enter()
.append("line")
.attr("class", "link");
var node = svg
.append("g")
.selectAll("g")
.data(nodes)
.enter()
.append("g");
var circles = node
.append("circle")
.attr("class", "node")
.attr("r", node => {
return node.radius;
});
var text = node
.append("text")
.text(d => {
return d.id;
})
.attr("class", "node-caption")
.attr("x", 0)
.attr("y", 0);
var linkNode = svg
.selectAll(".link-node")
.data(linkNodes)
.enter()
.append("circle")
.attr("class", "link-node")
.attr("r", linkNodeRad);
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
linkNode
.attr("cx", function(d) {
return (d.x = (d.source.x + d.target.x) * 0.5);
})
.attr("cy", function(d) {
return (d.y = (d.source.y + d.target.y) * 0.5);
});
}
forceSimulation(nodes.concat(linkNodes), links)
.on("tick", ticked)
.on("end", () => {
console.warn("END");
});
}
const coords = {};
const size = { min: 10, max: 30 };
const dotStr = "graph g { a--a;a--b;a--b;a--c;a--d;a--e;b--b1;c--c1;c--c2;d--d1;d--d2;d--d3;d--d4;e--e1;v--w;v--x;v--y;w--z;w--w1;x--x1;x--x2;y--y1;y--y2;y--y3;y--y4;z--z1;v--a; }";
const g = graphlibDot.read(dotStr);
g.children().forEach(child => {
const x = getRandomInt(1024 - 10, 10);
const y = getRandomInt(768 - 10, 10);
coords[child] = {
x: x,
y: y,
w: getRandomInt(size.max, size.min),
h: getRandomInt(size.max, size.min)
};
});
fdSortShit(g, coords);
svg {
background-color: lightgray;
}
circle.node {
fill: lightcoral;
}
circle.link-node {
fill: rgba(0, 0, 255, 0.2);
/* fill: transparent; */
}
line.link {
stroke: lightseagreen;
}
text.node-caption {
font: normal 10px courier new;
}
<script src="https://cdn.jsdelivr.net/npm/graphlib-dot#0.6.2/dist/graphlib-dot.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The image looks like this:
The first question is: What about to avoid this intersections?
I know that I can't dodge all of edge intersections but I want to minimize them. This example is a tree-graph with no cycles. I know that there is a way to build it without edge crossings. But I don't know how to do it with this algorithm.
But still annoying intersection.
The second question is: What about NOT to simulate forces in-time (I need no animation) but just to draw final result? When I use forceSimulation.on("end", cb) it is great, but delay between start and stop is big.. but this is graph is just a small example. I can't wait so long on a bigger once.
And the third question is.. how to apply force-derected settings? Force energy, stiffness, repulsion, damping etc.? Can't find them on d3#5
The final result my project lead wants is:
no node overlap;
minimize edge-edge intersections;
minimize edge-node intersections.
I'm ready for dialog.
I solved this issue by playing with the forceCollide and forceLink distance parameters :
var simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(d => d.id).distance(100).strength(1))
.force('charge', d3.forceManyBody()) // ^ change this value
.force('collide', d3.forceCollide(110)) // change this value
.force('center', d3.forceCenter(width / 2, height / 2));
The idea is to make unrelated nodes to repel each other, while keeping the link distance short.
It works very well in my case, but my node graph is much simpler than yours.
You apply the force settings in the initialization portion. Here is an example -
var simulation = d3.forceSimulation() //Modify link distance/strength here
.force("link", d3.forceLink().id(function (d) { return d.id; }).distance(80).strength(1))
.force("charge", d3.forceManyBody().strength(-15)) //Charge strength is here
.force("center", d3.forceCenter(width / 2, height / 2));
This can be used to solve one of your problems...if you set the "charge" strength to some large negative number like -150, the nodes will be strongly repelled such that they don't overlap, nor do any of their links. If you aren't looking to drag the graph at all then this should be all you need to avoid overlaps.
A side effect of a highly negative charge is that the graph settles quite quickly, and as you don't want to simulate the forces in real time after the initial display, you can call simulation.stop() to freeze or stop the simulation.

D3 force simulation - Bring nodes closer to a point in same angle

This question is based on Move d3 circles away from center circle - force layout where when size of a node N1 is changed and I ran simulation on the nodes, the nodes around the N1 will move away in the same angle but the question I have here is to bring the nodes back closer to the N1 when its size is changed back to original. How can I achieve that?
I have used below but the nodes are not coming closer
simulation.nodes(nodes);
simulation.restart();
for (var i = 0; i < 300; ++i) simulation.tick();
Also, if I try with force simulation then the position of the other nodes are completely changing, please see the video here http://recordit.co/797i1E8ocT
d3.forceSimulation(nodes)
.force('x', d3.forceX(plot.x))
.force('y', d3.forceY(plot.y))
Thanks in advance.
Nodes are not coming closer because your simulation "speed", which is called alpha value, is exhausted (decayed). Just replace
simulation.restart();
which you don't need with
simulation.alpha(1);
and the nodes will behave properly.
However, as you have noted, after several expansions and collapses nodes can move significantly from their initial locations. This problem can be addressed in several ways. You can use some kind of determenistic algorithm to compute nodes locations such as tree layout, but it can be difficult to achieve smooth transitions when nodes are being expanded and collapsed. Another way is to "pin" nodes to their initial locations using additional forces, that attract each node to its first calculated position.
To implement it you can save the initial positions after the simulation initialization and its first run:
for (let d of data.children) {
d.initialX = d.x;
d.initialY = d.y;
}
And then replace x and y forces with forces which attract each node to its initial position:
simulation
.force("x", d3.forceX(d => d.initialX).strength(0.2))
.force("y", d3.forceY(d => d.initialY).strength(0.2));
The strength determines the balance between collision force and pinning. The larger the strength, the more aggressively the nodes will try to occupy their initial positions.
It may also be desirable to use point attraction instead of the sum of x and y ones — take a look at d3-force-attract package.
The following snippet illustrates the described approach.
var w = 650,
h = 650;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var color = d3.scaleOrdinal(d3.schemeCategory10)
var data = {
name: "root",
children: [{
label: 'RED1',
size: 20,
color: 'red'
}, {
label: 'RAD2',
size: 20,
color: '#c99700'
}, {
label: 'BIL3',
size: 20,
color: 'blue'
}, {
label: 'EEN4',
size: 10,
color: '#007377'
}, {
label: 'INO5',
size: 40,
color: '#b4975a'
}, {
label: 'RAD6',
size: 40,
color: '#c99700'
}, {
label: 'BIL7',
size: 30,
color: '#008ce6'
}, {
label: 'INO8',
size: 30,
color: '#b4975a'
}, {
label: 'INO9',
size: 40,
color: '#b4975a'
}, {
label: 'RAD10',
size: 40,
color: '#c99700'
}, {
label: 'BIL11',
size: 30,
color: '#008ce6'
}, {
label: 'INO12',
size: 30,
color: '#b4975a'
}]
};
var render = function() {
var simulation = d3.forceSimulation(data.children)
.force("x", d3.forceX(w / 2))
.force("y", d3.forceY(h / 2))
.force("collide", d3.forceCollide(function(d) {
return d.size + 20
}))
.stop();
for (var i = 0; i < 100; ++i) simulation.tick();
for (let d of data.children) {
d.initialX = d.x;
d.initialY = d.y;
}
simulation
.force("x", d3.forceX(d => d.initialX).strength(0.2))
.force("y", d3.forceY(d => d.initialY).strength(0.2));
let nodeLevel1 = svg.selectAll('circle')
.data(data.children, (d) => {
// Attaching key for uniqueness
return d.label;
});
nodeLevel1.exit().remove();
let nodeLevel1Enter = nodeLevel1
.enter()
.append("circle")
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
.attr("r", function(d) {
return d.size
})
.style("fill", function(d) {
return d.color;
})
nodeLevel1Enter = nodeLevel1Enter
.merge(nodeLevel1)
nodeLevel1Enter
.transition()
.duration(1600)
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
.attr("r", function(d) {
return d.size
})
.style("fill", function(d) {
return d.color;
})
d3.select('#updatesize').on('click', function() {
add();
})
d3.select('#updatebluesize').on('click', function() {
addblue();
})
d3.select('#resetsize').on('click', function() {
reset();
})
d3.select('#resetall').on('click', function() {
resetall();
})
var add = function() {
data.children[0].size = 140;
move();
}
var addblue = function() {
data.children[2].size = 100;
move();
}
var reset = function() {
data.children[0].size = 20;
move();
}
var resetall = function() {
data.children[0].size = 20;
data.children[2].size = 20;
move();
}
function move() {
simulation.nodes(data.children);
simulation.alpha(1);
for (var i = 0; i < 300; ++i) simulation.tick();
nodeLevel1Enter
.transition()
.duration(1600)
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
.attr("r", function(d) {
return d.size
});
}
}
render();
<script src="https://d3js.org/d3.v4.min.js"></script>
<a href='javascript:;' id='updatesize'>Update red resource size</a> |
<a href='javascript:;' id='updatebluesize'>Update blue resource size</a> |
<a href='javascript:;' id='resetsize'>Reset red resource size</a> |
<a href='javascript:;' id='resetall'>Reset all</a>

Keep position of D3 graph after updating data

I have the following problem. I created a graph in D3. I added a dropdown menu where the nodes of the graph get resized according to the chosen category. To do that I wrote a function that re-runs the javascript code every time I choose an option in the dropdown menu.
However, the updated graph appears at the end of my website. Is there a way to keep the graph at its original position ?
I tried to put it in a div and fix the position, but that didn't work out. I hope my problem is clear.
Find in the following a toy example of my code.
Cheers!
<html lang="en">
<head>
<meta charset="utf-8">
<script type="text/javascript" src="http://d3js.org/d3.v3.js"></script>
<style type="text/css">
</style>
</head>
<body>
<select onchange="change(this)">
<option value="rad1">Option 1</option>
<option value="rad2">Option 2</option>
</select>
<p> This is some text above the plot </p>
<script type="text/javascript">
change('rad1')
function change(dd) {
update(dd.value)
}
function update(rad) {
var current = rad;
d3.selectAll('svg').remove();
var w = 500;
var h = 300;
var dataset = {
nodes: [
{ name:'Node 1',rad1: 1.31, rad2: 2.32 },
{ name:'Node 2',rad1: 2.12, rad2: 5.00 },
{ name:'Node 3',rad1: 40.30, rad2: 20.40 }
],
edges: [
{ source:0, target:0,rad1: 3.31 },
{ source:0, target:1,rad1: 3.31},
{ source:1, target:2,rad1: 20.31}
]
};
var force = d3.layout.force()
.nodes(dataset.nodes)
.links(dataset.edges)
.size([w, h])
.linkDistance([100])
.charge([-300])
.start();
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var edges = svg.selectAll("line")
.data(dataset.edges)
.enter()
.append("line")
.style("stroke", "#808080")
.style("stroke-width", 1)
.style("stroke-opacity", 0.1);
var nodes = svg.selectAll("circle")
.data(dataset.nodes)
.enter()
.append("circle")
.attr("r", function(d) {
if( current == "rad1") {
return d.rad1;
} else {
return d.rad2;
}
})
.style("fill", function(d, i) {
return '#000000';
})
.call(force.drag)
.on("click", function(d) {
console.log("Name: " + d.name);
});
force.on("tick", function() {
edges.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; });
nodes.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
}
</script>
<p> Here is more text below the plot </p>
</body>
</html>
The problem is that you are appending the SVG to the body, thus it will always show at the very end of the page. The correct way is creating a div (or anything else) with an ID (which are unique):
<div id="svghere"></div>
And then appending the SVG:
var svg = d3.select("#svghere")
.append("svg")
.//the rest of the code

Categories

Resources