I am new to d3-force
, I find that it is difficult to change the angle of lines and nodes,you can run my code here.
Anyway, my code is very easy:
const width = 800;
const height = 400;
const centerX = width / 2;
const centerY = height / 2;
const graph = ({
nodes: Array.from({length:8}, () => ({})),
links: [
{source: 1, target: 0},
{source: 2, target: 0},
{source: 3, target: 0},
{source: 4, target: 0},
{source: 5, target: 0},
{source: 6, target: 0},
{source: 7, target: 0},
]
});
const svg = d3.select("svg").attr("viewBox", [0, 0, width, height]),
link = svg
.selectAll(".link")
.data(graph.links)
.join("line"),
node = svg
.selectAll(".node")
.data(graph.nodes)
.join("g");
node.append("circle")
.attr("r", 12)
.attr("cursor", "move")
.attr("fill", "#ccc")
.attr("stroke", "#000")
.attr("stroke-width", "1.5px");
node.append("text").attr("dy", 25).text(function(d) {return d.index})
const simulation = d3
.forceSimulation()
.nodes(graph.nodes)
.force("link", d3.forceLink(graph.links).distance(100))
.force("charge", d3.forceManyBody().strength(-400))
.force("center", d3.forceCenter(width / 2, height / 2))
.stop();
for (let i = 0, n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())); i < n; ++i) {
simulation.tick();
}
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)
.attr("stroke", "#000")
.attr("stroke-width", "1.5px")
node
.attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>
I dig the source of d3-force
here, find that it adds Math.PI * (3 - Math.sqrt(5))
every time.
Here is what I want:
n
nodes, it will be Math.PI / (n-1)
Math.PI / (n-1) / 2
I wouldn't use d3-force, but just calculate the positions yourself, using some basic trigonometry:
const width = 600;
const height = 400;
const centerX = width / 2;
const centerY = height / 2;
const graph = ({
nodes: d3.range(8).map(i => ({ id: i })),
links: [{
source: 0,
target: 1
},
{
source: 0,
target: 2
},
{
source: 0,
target: 3
},
{
source: 0,
target: 4
},
{
source: 1,
target: 5
},
{
source: 1,
target: 6
},
{
source: 1,
target: 7
},
]
});
graph.root = graph.nodes[0];
graph.nodes.forEach(n => {
n.children = [];
});
// Replace ID's with references to the nodes themselves
graph.links.forEach(l => {
l.source = graph.nodes.find(n => n.id === l.source);
l.target = graph.nodes.find(n => n.id === l.target);
// Register the target as a child of the source
l.source.children.push(l.target);
l.target.parent = l.source;
});
// Place the nodes
graph.nodes.forEach(n => {
if(n.parent === undefined) {
// root
n.x = centerX;
n.y = centerY;
n.level = 0;
return;
}
const parent = n.parent;
n.level = parent.level + 1;
const nSiblings = parent.children.length;
const ithSibling = parent.children.indexOf(n);
// Position the node
const angle = 2 * Math.PI / nSiblings; // in radians
const startAngle = - angle / 2;
const radius = 200 - 60 * n.level;
console.log(angle, startAngle);
n.x = parent.x + radius * Math.cos((ithSibling + 1) * angle + startAngle);
// Use a plus to keep the order clockwise, - for counterclockwise
n.y = parent.y + radius * Math.sin((ithSibling + 1) * angle + startAngle);
});
const svg = d3.select("svg").attr("viewBox", [0, 0, width, height]),
link = svg
.selectAll(".link")
.data(graph.links)
.join("line"),
node = svg
.selectAll(".node")
.data(graph.nodes)
.join("g");
node.append("circle")
.attr("r", 12)
.attr("cursor", "move")
.attr("fill", "#ccc")
.attr("stroke", "#000")
.attr("stroke-width", "1.5px");
node.append("text").attr("dy", 25).text(function(d) {
return d.id
});
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)
.attr("stroke", "#000")
.attr("stroke-width", "1.5px")
node
.attr("transform", function(d) {
return "translate(" + d.x + ", " + d.y + ")";
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>