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>
Related
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);
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.
I've been trying to implement a D3 force directed graph function to give a visual representation to my data, So far I've been successful in getting the nodes to display on the screen and have the individual nodes draggable, however I have been unsuccessful in getting the whole network to pan and zoom.
I've looked at countless examples online, but haven't really been able to figure out what I'm doing wrong.
Could someone point me in the right direction please, using d3 version 4
function selectableForceDirectedGraph(){
var width = d3.select('svg').attr('width');
var height = d3.select('svg').attr('height');
var color = d3.scaleOrdinal(d3.schemeCategory20);
var svg = d3.select("svg")
.attr('width',width)
.attr('height',height);
var container = svg.append("g")
.on("zoom",zoomed)
.on("start",dragstarted)
.on("drag",dragged)
.on("end",dragended);
var json_nodes = _dict['one']['nodes'];
var json_links = _dict['one']['links'];
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(json_links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); })
.style("marker-end","url(#suit)");
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(json_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(json_nodes)
.on("tick", ticked);
simulation.force("link")
.links(json_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;
}
//Zoom functions
function zoomed(){
container.attr("transform","translate(" + d3.event.translate + ")scale(" +d3.event.scale + ")");
}
}
The HTML doc that this function is called from has an SVG instantiated (hence why the SVG is selected, not appended like in most other examples)
The two variables "json_nodes" and "json_links" take formatted nodes and links (like what you'd see in a JSON file) from a text file and passes them as the data (wanted to do this offline so when I go overseas). The format for the data is like so:
nodes:[{"id": "Name", "group": integer},...],
links:[{"source": "Name", "target": "Name", "value": integer},...]
I apologise if this is a repeated question, I haven't been able to find any really intuitive help with this.
After some intense staring at the screen for a couple hours, I realised a number of little housekeeping tips to make mine, and hopefully to any other programmers new to D3, applications easier to understand and less prone to errors.
As it turns out from my last example, I was trying to perform zoom functions when the zoom variable containing the D3 .zoom() method hadn't been added to the links variable (as I have pointed out below). Once I had done this, everything worked perfectly.
I've also added some comments as to make the improve the readability of the code from the original question, these changes make it easier to understand and much easier to build upon (much like the inheritance with python classes that I'm familiar with).
So hopefully my little moment of frustration is useful to someone in the future, until the next question, happy debugging :)
Mr Incompetent.
function selectableForceDirectedGraph(){
var width = d3.select('svg').attr('width');
var height = d3.select('svg').attr('height');
var color = d3.scaleOrdinal(d3.schemeCategory20);
//As the height and width have already been set, no need to reset them.
var svg = d3.select("svg");
//This is the container group for the zoom
var container = svg.append("g")
.attr("class","everything");
//see the above question for explanation for purpose of these variable.
var json_nodes = _dict['one']['nodes'];
var json_links = _dict['one']['links'];
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));
//drawing lines for the links
var link = container.append("g")
.attr("class", "links")
.selectAll("line")
.data(json_links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); })
.style("marker-end","url(#suit)");
//draw the circles for the nodes
var node = container.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(json_nodes)
.enter().append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); });
//HOUSE KEEPING NOTE: add handlers for drag and zoom as to prevent DRY
var drag_controls = d3.drag()
.on("start",dragstarted)
.on("drag",dragged)
.on("end",dragended);
drag_controls(node); //adding the drag event handlers to the nodes
var zoom_controls = d3.zoom()
.on("zoom",zoomed);
zoom_controls(svg); //adding the zoom event handler to the svg container
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(json_nodes)
.on("tick", ticked);
simulation.force("link")
.links(json_links);
function ticked() {
link //updates the link positions
.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 //update the node positions
.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;
}
//Zoom functions
function zoomed(){
container.attr("transform",d3.event.transform)
}
}
I've been trying to make D3 like Force-Directed Graph (example: https://bl.ocks.org/mbostock/4062045) with PNGs (meaning the dots should be pictures).
Here is a visual idea:
A different approach I tried is to map each graphic element into parallax.js (http://matthew.wagerfield.com/parallax/) and draw a line between the center of each graphic element, but I do not know how to do that, just yet.
Since you didn't post your code to create the force, I'll provide a general answer. You may have to adapt it according to your specific code.
The basic idea here is appending group elements for each node, and appending both the circles and the images to those groups. Here I'm using 40x40 PNGs:
var node = svg.selectAll("foo")
.data(nodes)
.enter()
.append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var nodeCircle = node.append("circle")
.attr("r", 20)
.attr("stroke", "gray")
.attr("fill", "none");
var nodeImage = node.append("image")
.attr("xlink:href", d => d.image)
.attr("height", "40")
.attr("width", "40")
.attr("x", -20)
.attr("y", -20);
The url of each image is in the data array of the nodes:
var nodes = [{
"id": "foo",
"image": "https://icons.iconarchive.com/icons/google/chrome/48/Google-Chrome-icon.png"
}, {
"id": "bar",
"image": "https://icons.iconarchive.com/icons/carlosjj/mozilla/48/Firefox-icon.png"
}, {
"id": "baz",
"image": "https://icons.iconarchive.com/icons/johanchalibert/mac-osx-yosemite/48/safari-icon.png"
}, {
"id": "barbaz",
"image": "https://icons.iconarchive.com/icons/ampeross/smooth/48/Opera-icon.png"
}];
Here is a demo:
var width = 300;
var height = 200;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var nodes = [{
"id": "Chrome",
"image": "https://icons.iconarchive.com/icons/google/chrome/48/Google-Chrome-icon.png"
}, {
"id": "Firefox",
"image": "https://icons.iconarchive.com/icons/carlosjj/mozilla/48/Firefox-icon.png"
}, {
"id": "Safari",
"image": "https://icons.iconarchive.com/icons/johanchalibert/mac-osx-yosemite/48/safari-icon.png"
}, {
"id": "Opera",
"image": "https://icons.iconarchive.com/icons/ampeross/smooth/48/Opera-icon.png"
}];
var edges = [{
"source": 0,
"target": 1
}, {
"source": 0,
"target": 2
}, {
"source": 0,
"target": 3
}];
var simulation = d3.forceSimulation()
.force("link", d3.forceLink())
.force("charge", d3.forceManyBody().strength(-1000))
.force("center", d3.forceCenter(width / 2, height / 2));
var links = svg.selectAll("foo")
.data(edges)
.enter()
.append("line")
.style("stroke", "#ccc")
.style("stroke-width", 1);
var color = d3.scaleOrdinal(d3.schemeCategory20);
var node = svg.selectAll("foo")
.data(nodes)
.enter()
.append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var nodeCircle = node.append("circle")
.attr("r", 20)
.attr("stroke", "gray")
.attr("stroke-width", "2px")
.attr("fill", "white");
var nodeImage = node.append("image")
.attr("xlink:href", d => d.image)
.attr("height", "40")
.attr("width", "40")
.attr("x", -20)
.attr("y", -20)
var texts = node.append("text")
.style("fill", "black")
.attr("dx", 20)
.attr("dy", 8)
.text(function(d) {
return d.id;
});
simulation.nodes(nodes);
simulation.force("link")
.links(edges);
simulation.on("tick", function() {
links.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", (d) => "translate(" + d.x + "," + 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;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
I want some of the nodes in my force-directed layout to ignore all forces and stay in fixed positions based on an attribute of the node, while still being able to be dragged and exert repulsion on other nodes and maintain their link lines.
I thought it would be as simple as this:
force.on("tick", function() {
vis.selectAll("g.node")
.attr("transform", function(d) {
return (d.someAttribute == true) ?
"translate(" + d.xcoordFromAttribute + "," + d.ycoordFromAttribute +")" :
"translate(" + d.x + "," + d.y + ")"
});
});
I have also tried to manually set the node's x and y attributes each tick, but then the links continue to float out to where the node would be if it was affected by the force.
Obviously I have a basic misunderstanding of how this is supposed to work. How can I fix nodes in a position, while keeping links and still allowing for them to be draggable?
Set d.fixed on the desired nodes to true, and initialize d.x and d.y to the desired position. These nodes will then still be part of the simulation, and you can use the normal display code (e.g., setting a transform attribute); however, because they are marked as fixed, they can only be moved by dragging and not by the simulation.
See the force layout documentation for more details (v3 docs, current docs), and also see how the root node is positioned in this example.
Fixed nodes in force layout for d3v4 and d4v5
In d3v3 d.fixed will fix nodes at d.x and d.y; however, in d3v4/5 this method no longer is supported. The d3 documentation states:
To fix a node in a given position, you may specify two additional
properties:
fx - the node’s fixed x-position
fy - the node’s fixed y-position
At the end of each tick, after the application of any forces, a node
with a defined node.fx has node.x reset to this value and node.vx set
to zero; likewise, a node with a defined node.fy has node.y reset to
this value and node.vy set to zero. To unfix a node that was
previously fixed, set node.fx and node.fy to null, or delete these
properties.
You can set fx and fy attributes for the force nodes in your data source, or you can add and remove fx and fy values dynamically. The snippet below sets these properties at the end of drag events, just drag a node to fix its position:
var data ={
"nodes":
[{"id": "A"},{"id": "B"},{"id": "C"},{"id":"D"}],
"links":
[{"source": "A", "target": "B"},
{"source": "B", "target": "C"},
{"source": "C", "target": "A"},
{"source": "D", "target": "A"}]
}
var height = 250;
var width = 400;
var svg = d3.select("body").append("svg")
.attr("width",width)
.attr("height",height);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(50))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var link = svg.append("g")
.selectAll("line")
.data(data.links)
.enter().append("line")
.attr("stroke","black");
var node = svg.append("g")
.selectAll("circle")
.data(data.nodes)
.enter().append("circle")
.attr("r", 5)
.call(d3.drag()
.on("drag", dragged)
.on("end", dragended));
simulation
.nodes(data.nodes)
.on("tick", ticked)
.alphaDecay(0);
simulation.force("link")
.links(data.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 dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.6.0/d3.min.js"></script>
d3v6 changes to event listners
In the above snippet, the drag events use the form
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
Where d is the datum of the node being dragged. In d3v6, the form is now:
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
or:
function dragged(event,d) {
d.fx = event.x;
d.fy = event.y;
}
The event is now passed directly to the listener, the second parameter passed to the event listener is the datum. Here's the canonical example on Observable.