chart = {
const nodes = [];
const links = [];
const manyBodyStrength = -15;
const centerStrength = 0.08;
const svg = d3
.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height]);
const simulation = d3
.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(manyBodyStrength))
.force("x", d3.forceX().strength((centerStrength * 100) / width))
.force("y", d3.forceY().strength((centerStrength * 100) / height))
.force("collision", d3.forceCollide().radius(d => d.radius + 3))
.force(
"link",
d3
.forceLink(links)
.distance(57)
.strength(0.8)
)
.on("tick", ticked);
simulation.alphaDecay(1 - Math.pow(0.001, 1 / 600));
let link = svg.append("g").selectAll("line");
let node = svg.append("g").selectAll("circle");
function ticked() {
node
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("fill", d => d.color);
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", d => d.source.color);
}
function update() {
node = node.data(nodes).join(enter =>
enter
.append("circle")
.attr("r", 0)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("fill", d => d.color)
.attr("stroke-width", "0")
.attr("stroke", "#000")
.attr('cursor', d => (d.type === 'top' ? 'pointer' : ''))
.on("click", (e, d) => (d.type === 'top' ? click(d) : ''))
.call(enter =>
enter
.transition()
.attr("r", d => d.radius)
.attr("stroke-width", "2px")
)
);
link = link.data(links).join(enter =>
enter
.append("line")
.attr("stroke-dasharray", "0 5")
.attr("stroke-opacity", "0")
.attr("stroke-linecap", "round")
.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-width", 4)
.attr("stroke", d => d.source.color)
.call(enter => enter.transition().attr("stroke-opacity", 1))
);
simulation.nodes(nodes);
if (simulation.alpha() <= 1) {
simulation.alpha(1);
simulation.restart();
}
/*for (let i = 0; i < 4; i++) {
simulation.tick();
}*/
}
function addNodes(parent) {
const childCount = parent.childCount;
const children = createNodes(parent, 'child', childCount);
children.forEach(child => {
const link = { source: parent, target: child };
nodes.push(child);
links.push(link);
});
}
function removeNodes(parent) {
const nodes2remove = nodes.filter(n => n.parent === parent);
const links2remove = links.filter(l => l.source === parent);
nodes2remove.forEach(n => {
const index = nodes.indexOf(n);
nodes.splice(index, 1);
});
links2remove.forEach(l => {
const index = links.indexOf(l);
links.splice(index, 1);
});
}
function click(d) {
if (!d.active) {
addNodes(d);
d.active = true;
} else {
removeNodes(d);
d.active = false;
}
update();
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function generateColor(count) {
// returns a function which takes the current index
// in the range from 1 to count as argument
return d3
.scaleLinear()
.domain([1, count])
.range(["#008bc7", "#edcf6d"]);
}
function createNode(parent, color, type) {
return {
id: uuid(),
color,
radius: type === 'child' ? radius : radius + 7,
type,
parent
};
}
function createNodes(parent, type, count) {
// function to generate colors - if parent available just take parent color
const getColor = parent ? () => parent.color : generateColor(count);
return Array.from({ length: count }, (_, x) => x).map((_, i) => {
const angle = (360 / count) * i;
const distance = parent ? 50 : 150;
const parentX = parent ? parent.x : 0;
const parentY = parent ? parent.y : 0;
const x = parentX + distance * Math.cos(angle * (Math.PI / 180));
const y = parentY + distance * Math.sin(angle * (Math.PI / 180));
const n = createNode(parent, getColor(i), type);
return { ...n, x, y };
});
}
// single fixed node in the center
const user = createNode(null, '#000', 'user');
user.fx = 0;
user.fy = 0;
nodes.push(user);
// ten topElements with children around the center
const topElements = createNodes(null, 'top', 12).forEach(topElement => {
const childCount = randomInt(2, 5);
topElement.childCount = childCount;
nodes.push(topElement);
});
update();
return svg.node();
}