I'm trying to reproduce with d3.js the behaviour of a pull cord light switch.
You can see the code running here or below in the snippet (best to view full screen).
My question is how can I set the distance between nodes to be always the same (as it is in a real cord)?
The only link that should stretch is the one between the two green nodes
I tried to add more strength to the forces but doesn't look good.
//create somewhere to put the force directed graph
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
const nodes_data = [
{ name: "c1" },
{ name: "c2" },
{ name: "c3" },
{ name: "c4" },
{ name: "c5" },
{ name: "c6" },
];
const links_data = [
{ source: "c1", target: "c2" },
{ source: "c2", target: "c3" },
{ source: "c3", target: "c4" },
{ source: "c4", target: "c5" },
{ source: "c5", target: "c6" },
];
//set up the simulation
const simulation = d3.forceSimulation().nodes(nodes_data);
//add forces
simulation.force(
"manyBody",
d3.forceManyBody().distanceMin(20).distanceMax(21)
);
const link_force = d3.forceLink(links_data).distance(40).strength(1);
link_force.id(function (d) {
return d.name;
});
simulation.force("links", link_force);
simulation.force("centerx", d3.forceX(width / 2).strength(0.3));
simulation.force(
"centery",
d3
.forceY()
.y(function (d, i) {
return height / 10 + i * 35;
})
.strength(function (d, i) {
return 0.4;
})
);
//draw circles for the nodes
const node = svg
.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes_data)
.enter()
.append("circle")
.attr("r", 10)
.attr("fill", "red")
.attr("draggable", "true");
const circles = d3.selectAll("circle")._groups[0];
const firstCircle = d3.select(circles[0]);
const secondCircle = d3.select(circles[1]);
const lastCircle = d3.select(circles[circles.length - 1]);
firstCircle.attr("fill", "green").text(function (d) {
d.fx = width / 2;
d.fy = height / 10;
console.log(d.fx, d.fy);
});
secondCircle.attr("fill", "green");
lastCircle.attr("fill", "blue");
//draw lines for the links
const link = svg
.append("g")
.attr("class", "links")
.selectAll("line")
.data(links_data)
.enter()
.append("line")
.attr("stroke-width", 2);
// The complete tickActions() function
function tickActions() {
//update circle positions each tick of the simulation
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
//update link positions
//simply tells one end of the line to follow one node around
//and the other end of the line to follow the other node around
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;
});
}
simulation.on("tick", tickActions);
const drag_handler = d3
.drag()
.on("start", drag_start)
.on("drag", drag_drag)
.on("end", drag_end);
function drag_start(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function drag_drag(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function drag_end(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
d3.forceY().strength(0.1);
document.body.style.background == "black"
? (document.body.style.background = "white")
: (document.body.style.background = "black");
console.log(document.body.style.background == "black");
}
drag_handler(lastCircle);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg width="400" height="400"></svg>
thanks
D3 isn't likely to create a perfect solution without modifying how the force layout works. Staying within the bounds of D3, I have a solution that achieves the desired result (with a minimal bit of elasticity, which may be acceptable).
As I noted in the comment, d3 is balancing a bunch of forces while the simulation runs. As a consequence, the resulting layout is a compromise between the forces. The solution I linked to in my comment gets links of a specified link by dialing down all the other forces as the simulation cools, allowing the other forces to influence the general layout, while the link distance force tweaks the result to ensure links are the proper length.
The same principle can be applied here, but without the benefit of multiple cycles to nudge the nodes to the precise location required.
First we declare all our forces, as usual:
var manybody = d3.forceManyBody().distanceMin(20).distanceMax(21);
var x = d3.forceX(width / 6).strength(0.3)
var y = d3.forceY().y(function (d, i) { return height / 10 + i * 35; })
.strength(0.4)
var distance = d3.forceLink(links_data.filter(function(d,i) { return i; }))
.distance(35)
.id(function(d) { return d.name; })
.strength(1);
Then we apply them:
simulation
.force("centerx",x)
.force("centery",y)
.force("link", distance)
.force("many", manybody);
Then in the drag start function, we remove all forces except for the link distance function. We also up the alpha and eliminate alpha decay to allow the force to move the nodes as close as possible in a single tick to their intended place:
function drag_start(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
// Disable other forces:
simulation.force("centerx",null)
.force("centery",null)
.force("many",null);
// Juice the alpha:
simulation.alpha(1)
.alphaDecay(0)
}
At the end of the drag, we undo the changes we made on drag start by reapplying the forces, decreasing alpha, and increasing alpha decay:
function drag_end(event, d) {
// Reapply forces:
simulation.force("centerx",x)
.force("centery",y)
.force("many",manybody);
// De-juice the alpha:
simulation.alpha(0.2)
.alphaDecay(0.0228)
...
There are a few idiosyncrasies in the code as compared with canonical D3, but I've just implemented the changes from above:
//create somewhere to put the force directed graph
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
const nodes_data = [
{ name: "c1" },
{ name: "c2" },
{ name: "c3" },
{ name: "c4" },
{ name: "c5" },
{ name: "c6" },
];
const links_data = [
{ source: "c1", target: "c2" },
{ source: "c2", target: "c3" },
{ source: "c3", target: "c4" },
{ source: "c4", target: "c5" },
{ source: "c5", target: "c6" },
];
//set up the simulation
const simulation = d3.forceSimulation().nodes(nodes_data);
////////////////////////
// Changes start: (1/2)
// Set up forces:
var manybody = d3.forceManyBody().distanceMin(15).distanceMax(15);
var x = d3.forceX(width / 6).strength(0.3)
var y = d3.forceY().y(function (d, i) { return 0 + i * 35; })
.strength(0.4)
var distance = d3.forceLink(links_data.filter(function(d,i) { return i; }))
.distance(35)
.id(function(d) { return d.name; })
.strength(1);
simulation
.force("centerx",x)
.force("centery",y)
.force("link", distance)
.force("many", manybody);
// End Changes (1/2)
/////////////////////////
//draw circles for the nodes
const node = svg
.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes_data)
.enter()
.append("circle")
.attr("r", 10)
.attr("fill", "red")
.attr("draggable", "true");
const circles = d3.selectAll("circle")._groups[0];
const firstCircle = d3.select(circles[0]);
const secondCircle = d3.select(circles[1]);
const lastCircle = d3.select(circles[circles.length - 1]);
firstCircle.attr("fill", "green").text(function (d) {
d.fx = width / 6;
d.fy = 0;
console.log(d.fx, d.fy);
});
secondCircle.attr("fill", "green");
lastCircle.attr("fill", "blue");
//draw lines for the links
const link = svg
.append("g")
.attr("class", "links")
.selectAll("line")
.data(links_data)
.enter()
.append("line")
.attr("stroke-width", 2);
// The complete tickActions() function
function tickActions() {
//update circle positions each tick of the simulation
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
//update link positions
//simply tells one end of the line to follow one node around
//and the other end of the line to follow the other node around
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;
});
}
simulation.on("tick", tickActions);
const drag_handler = d3
.drag()
.on("start", drag_start)
.on("drag", drag_drag)
.on("end", drag_end);
////////////////////////
// Start changes (2/2)
function drag_start(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
// Disable other forces:
simulation.force("centerx",null)
.force("centery",null)
.force("many",null);
// Juice the alpha:
simulation.alpha(1)
.alphaDecay(0)
}
function drag_drag(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function drag_end(event, d) {
// Reapply forces:
simulation.force("centerx",x)
.force("centery",y)
.force("many",manybody);
// De-juice the alpha:
simulation.alpha(0.2)
.alphaDecay(0.0228)
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
d3.forceY().strength(0.1);
document.body.style.background == "black"
? (document.body.style.background = "white")
: (document.body.style.background = "black");
}
// End changes (2/2)
////////////////////////
drag_handler(lastCircle);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg width=400 height=200></svg>
Optional Addition
I haven't empirically tested it, but it appeared to make a slight improvement: the first parameter for simulation.force() is just a name, so that you can replace or remove individual forces, you could potentially apply a force several times if you applied with different names. In the case of link distance, this could nudge links a bit closer each tick:
var distance = d3.forceLink(links_data.filter(function(d,i) { return i; }))
.distance(35)
.id(function(d) { return d.name; })
.strength(1);
simulation.force("a", distance);
simulation.force("b", distance);
simulation.force("c", distance);
Related
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).
I am trying to create bubble chart.(I'm new to D3.js).
First, I tried to change the code by referring to this site(https://www.d3-graph-gallery.com/graph/circularpacking_drag.html) to make a bubble chart.
the following code is original one.
// set the dimensions and margins of the graph
var width = 450
var height = 450
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", 450)
.attr("height", 450)
// create dummy data -> just one element per circle
var data = [{
"name": "A"
}, {
"name": "B"
}, {
"name": "C"
}, {
"name": "D"
}, {
"name": "E"
}, {
"name": "F"
}, {
"name": "G"
}, {
"name": "H"
}]
// Initialize the circle: all located at the center of the svg area
var node = svg.append("g")
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r", 25)
.attr("cx", width / 2)
.attr("cy", height / 2)
.style("fill", "#19d3a2")
.style("fill-opacity", 0.3)
.attr("stroke", "#b3a2c8")
.style("stroke-width", 4)
.call(d3.drag() // call specific function when circle is dragged
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Features of the forces applied to the nodes:
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter().x(width / 2).y(height / 2)) // Attraction to the center of the svg area
.force("charge", d3.forceManyBody().strength(1)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.1).radius(30).iterations(1)) // Force that avoids circle overlapping
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(data)
.on("tick", function(d) {
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
});
// What happens when a circle is dragged?
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(.03).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(.03);
d.fx = null;
d.fy = null;
}
<div id="my_dataviz"></div>
<script src="https://d3js.org/d3.v5.js"></script>
And in this chart, since there was no attraction function(attract to center),
I tried to add the following code. (I referred to the following site https://blockbuilder.org/ericsoco/d2d49d95d2f75552ac64f0125440b35e)
.force('attract', d3.forceAttract()
.target([width/2, height/2])
.strength(0.01))
However, it is not working.And it changes like the following image.
Could anyone advice me why this happen?
The image you get from d3.forceAttract not existing in d3 v5, as you can see from the console. You can use something like d3.forceRadial, however to add an attraction towards the center:
// set the dimensions and margins of the graph
var width = 450
var height = 450
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", 450)
.attr("height", 450)
// create dummy data -> just one element per circle
var data = [{
"name": "A"
}, {
"name": "B"
}, {
"name": "C"
}, {
"name": "D"
}, {
"name": "E"
}, {
"name": "F"
}, {
"name": "G"
}, {
"name": "H"
}]
// Initialize the circle: all located at the center of the svg area
var node = svg.append("g")
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r", 25)
.attr("cx", width / 2)
.attr("cy", height / 2)
.style("fill", "#19d3a2")
.style("fill-opacity", 0.3)
.attr("stroke", "#b3a2c8")
.style("stroke-width", 4)
.call(d3.drag() // call specific function when circle is dragged
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Features of the forces applied to the nodes:
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter().x(width / 2).y(height / 2)) // Attraction to the center of the svg area
.force("charge", d3.forceManyBody().strength(1)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.1).radius(30).iterations(1)) // Force that avoids circle overlapping
.force('attract', d3.forceRadial(0, width / 2, height / 2).strength(0.05))
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(data)
.on("tick", function(d) {
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
});
// What happens when a circle is dragged?
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(.03).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(.03);
d.fx = null;
d.fy = null;
}
<div id="my_dataviz"></div>
<script src="https://d3js.org/d3.v5.js"></script>
I am Trying to add rectangle and circle nodes in d3v4, the graph works although the nodes are all grouped together in one corner and their positions are not being updated. I can't work out what i'm doing wrong?
I have tried looking for examples online but cant seem to find any that are using d3v4 specifically
<!DOCTYPE html>
<meta charset="UTF-8">
<style>
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// Properties
var width = 800;
var height = 600;
var nominal_stroke = 4;
// Simulation
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody().strength(-400))
.force("center", d3.forceCenter(width / 2, height / 2));
// Create SVG window
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g");
svg.style("cursor", "move");
// Load JSON data
d3.json("./network.json", function(error, graph) {
console.log(graph);
if (error) throw error;
// Draw links
var link = g.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", nominal_stroke)
.style("stroke", "#999")
.style("stroke-opacity", 0.6);
// Draw nodes
var node = g.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Setup node properties
var circle = node.append("path")
.attr("d", d3.symbol()
.type(function (d) {
if
(d.shape == "rect") {
return d3.symbolSquare;
} else if
(d.shape == "circle") {
return d3.symbolCircle;
}
})
.size(400))
.style("stroke", "#999")
.style("stroke-opacity", 0.6)
.style("fill", function (d) {
return "blue"
});
// Add titles
node.append("title")
.text(function (d) {return d.id;});
// Start Simulation
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
// Refresh page
function ticked() {
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; });
}
// Zoom handler
svg.call(d3.zoom()
.scaleExtent([1 / 2, 8])
.on("zoom", zoomed));
function zoomed() {
node.attr("transform", d3.event.transform);
link.attr("transform", d3.event.transform);
}
});
// Functions
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = d.x;
d.fy = d.y;
}
function openLink() {
return function (d) {
var url = "";
if (d.url != "") {
url = d.url
}
window.open(url)
}
}
</script>
</body>
You should not use cx and cy in the ticked function, since you're dealing with <path>s, not <circle>s. You should use translate instead.
Therefore, it has to be:
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
Here is your code with that change (I'm using fake data here):
var width = 600;
var height = 400;
var nominal_stroke = 4;
// Simulation
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody().strength(-400))
.force("center", d3.forceCenter(width / 2, height / 2));
// Create SVG window
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g");
svg.style("cursor", "move");
graph = {
nodes: [{
id: 1,
shape: "rect"
}, {
id: 2,
shape: "circle"
}, {
id: 3,
shape: "rect"
}, {
id: 4,
shape: "rect"
}, {
id: 5,
shape: "circle"
}, {
id: 6,
shape: "circle"
}, {
id: 7,
shape: "circle"
}],
links: [{
source: 1,
target: 2
}, {
source: 1,
target: 3
}, {
source: 1,
target: 4
}, {
source: 1,
target: 5
}, {
source: 3,
target: 6
}, {
source: 3,
target: 7
}]
}
// Draw links
var link = g.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", nominal_stroke)
.style("stroke", "#999")
.style("stroke-opacity", 0.6);
// Draw nodes
var node = g.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Setup node properties
var circle = node.append("path")
.attr("d", d3.symbol()
.type(function(d) {
if (d.shape == "rect") {
return d3.symbolSquare;
} else if (d.shape == "circle") {
return d3.symbolCircle;
}
})
.size(400))
.style("stroke", "#999")
.style("stroke-opacity", 0.6)
.style("fill", function(d) {
return "blue"
});
// Add titles
node.append("title")
.text(function(d) {
return d.id;
});
// Start Simulation
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
// Refresh page
function ticked() {
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 + ")";
});
}
// Zoom handler
svg.call(d3.zoom()
.scaleExtent([1 / 2, 8])
.on("zoom", zoomed));
function zoomed() {
g.attr("transform", d3.event.transform);
}
// Functions
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = d.x;
d.fy = d.y;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
PS: Your zoom function is not working, which is a different problem. I also fixed it.
Below is how my json data looks in my html page:
var IDData = JSON.stringify([["node/105173", "node/38180995", "Agent", "Customer", "1379644.0", 1, 264.0, "1374903"], ["node/1061", "node/21373542", "Agent", "Customer", "530848.0", 1, 3000.0, "529502"]....]
The length of the array of array varies but but positioning of elements inside it is always the same.
Below is my d3.js code to render a force directed graph:
function createNodes (IDData) {
var nodes = [{group:1, group: 1}];
var links = [];
IDData.forEach(function(item){
nodes.push({id: item, group: 1})
links.push({source: item, target: item, value: 1}) // ;
});
var d3GraphData = {
nodes: nodes,
links: links
}
return d3GraphData;
};
function makeGraph (selector, d3GraphData) {
var svg = d3.select(selector),
width = +svg.attr("width"),
height = +svg.attr("height");
var color = d3.scaleOrdinal(d3.schemeCategory20);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(d3GraphData.links)
.enter()
.append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(d3GraphData.nodes)
.enter()
.append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(d3GraphData.nodes)
.on("tick", ticked);
simulation.force("link")
.links(d3GraphData.links);
function ticked() {
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 dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
$(document ).ready(function() {
console.log(IDData);
var galData = JSON.parse(IDData);
var startnodes = [];
var endnodes = [];
var nodetype1 = [];
var nodetype2 = [];
var PayTime = [];
var TXN_COUNT = [];
var Total_Amt = [];
var SendTime = [];
galData.map(function(e,i){
startnodes.push(e[0]);
endnodes.push(e[1]);
nodetype1.push(e[2]);
nodetype1.push(e[3]);
PayTime.push(e[4]);
TXN_COUNT.push(e[5]);
Total_Amt.push(e[6]);
SendTime.push(e[7]);
});
var final_nodes = createNodes(startnodes,endnodes);
makeGraph("#Network_graph",final_nodes);
});
Also , I need var startnodes and var endnodes which are the node numbers ; var nodetype1 and var nodetype2 which are node types as node attributes. This needs to be displayed as texts on node and var PayTime , var TXN_COUNT, var Total_Amt, var SendTime as link attributes .
Right now I only see nodes and no links . Never used d3.js before. More of a back end guy.
Apologies if too much code is pasted.
Your problem lies in how you convert your data into the format required by D3. Right now, you're trying to use the createNodes function to do this for you by passing in equal length lists of start/end (i.e. source/target) nodes for each link. But it's not quite working right since you are only taking the start nodes, so each link is connecting the same node and therefore is not visible.
I rewrote createNodes to create the nodes/links as D3 expects them. The only real trick is using d3.set() to create the list of nodes.
function createNodes (start_nodes, end_nodes) {
var node_set = d3.set();
var links = [];
start_nodes.forEach(function(src, i){
var tgt = end_nodes[i];
node_set.add(src);
node_set.add(tgt);
links.push({source: src, target: tgt, value: 1});
});
var d3GraphData = {
nodes: node_set.values().map(function(d){ return {id: d, group: 1} }),
links: links
}
return d3GraphData;
};
This is what you get in the output.
(Note that there are a few typos in the code that you have to fix before it will compile.)
I am using a force directed graph to draw entity relationships for a company. What I would like to do is use my built in index value for the "index" of the node instead of the array index. How do I override the d3 index value that gets set? Fiddle - http://jsfiddle.net/thielcole/ed9noqw1/
var forceLinks = [];
var forceData = [];
d3.csv('AmazonExampleforiCharts.csv', function(d) {
return { // This is the index I would like to use
index: d.Node,
parent: d.ParentNode,
color: d.NodeColor,
level: d.Hierarchy_code,
business: d.Business_Name,
power: d.Decisionpower,
hover: d.HoverOverValue,
link: d.LinkVALUE
};
}, function(error, rows) {
forceData = rows;
$.each(forceData, function(i, d) {
// console.log(d.parent);
if (d.parent != "" && d.parent !== undefined) { // generating link information here, I have to subtract one initially to match the array index
forceLinks.push({source: parseInt(d.parent , 10) - 1 , target: parseInt(d.index , 10) - 1, value: parseInt(d.level , 10)});
}
console.log(d);
});
$.each(forceLinks, function(i, d) {
// console.log(d);
});
initiateChart();
});
function initiateChart() {
var height = 1000,
width = 1400;
var graphData = [];
var graphLinks = [];
graphData = forceData;
graphLinks = forceLinks;
var color = d3.scale.category20();
var svg = d3.select('.svg')
.attr('width', width)
.attr('height', height);
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
// this is where it gets set
force.nodes(graphData)
.links(graphLinks)
.start();
var link = svg.selectAll('.link')
.data(graphLinks)
.enter().append('line')
.attr('class', 'link')
.style('stroke-width', function (d) {
return Math.sqrt(d.value);
});
var node = svg.selectAll('.node')
.data(graphData)
.enter().append('circle')
.attr('class', 'node')
.attr('r', 8)
.style('fill', function (d) {
return color(d.level);
})
.on('mouseover', function(d , i) {
d3.select(this).attr('r', 12);
var tooltip = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('position','absolute')
.style('top', (d3.event.pageY - 10) + 'px')
.style('left' , (d3.event.pageX) + 'px' )
.style('z-index' , '10')
.text(d.hover);
console.log(d)
})
.on('click', function(d , i) {
showChildren(d);
})
.on('mouseout', function(d, i) {
d3.select(this).attr('r', 8);
d3.select('body').selectAll('.tooltip')
.remove();
})
.call(force.drag);
//Now we are giving the SVGs co-ordinates - the force layout is generating the co-ordinates which this code is using to update the attributes of the SVG elements
force.on("tick", function () {
link.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
node.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
})
});
}
var node = svg.selectAll('.node')
.data(graphData)
.enter().append('circle')
.attr('class', 'node')
.attr('r', 8)
.attr('id', function(d){return(d.index);})// use the data 'index' property as node 'id' attribute
.style('fill', function (d) {
return color(d.level);
})
.call(force.drag);
The 'index'-es will be stored in circles 'id' attributes as strings (not numbers):
You can add as much attributes as you want and recall them later. Actually, the id tag is not the best choice. The much better alternatives are: data-index, data-id, data-company etc.
PS:
The notation .attr('class', 'node') is not a mistake, but d3.js have special selection.classed(name[, value]) method for classes. I recommend you for attributes use native js arrays:
var styles = {
node: {stroke: "white", "stroke-width": 1.5, r: 8},
link: {stroke: "#999", "stroke-width": 0.6}
};
...
var node = svg.selectAll('.node')
...
.attr(styles.node)
...
By the way, in SVG fill is attribute.
DEMO: http://jsfiddle.net/3gw8vxa3/