chart = {
const svg = d3
.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height]);
let link = svg.append("g").selectAll("line");
let node = svg.append("g").selectAll("circle");
let title = svg.append("g").selectAll("text");
let simulation = d3.forceSimulation();
let collisionForce = d3.forceCollide();
let linkForce = d3.forceLink().id(d => d.id);
function restartSimulation() {
if (simulation.alpha() <= 0.3) {
simulation.alpha(0.3);
simulation.restart();
}
}
function getDistance(linkDatum) {
let source = linkDatum.source;
let target = linkDatum.target;
const childrenCount = target.item.children.length;
const childrenVisible =
childrenCount > 0 && target.item.children.some(item => item.visible);
const ratio = childrenCount * .2;
const factor =
configuration.childItemDistanceFactor +
(childrenVisible ? Math.max(1, ratio) : 0);
return factor * (source.radius + target.radius);
}
// Link Distanz Gewichtung.
function getStrength(linkDatum) {
return 1;
}
// Kollisionsradius
function getCollisionRadius(node) {
if (node.item.parent == null) {
return node.radius + 30;
}
return node.radius + 10;
}
// Funktion, die bei jedem Tick des Force-Renderings aufgerufen wird
function updateLinksAndNodes() {
if (linkForce) {
linkForce.distance(getDistance);
linkForce.strength(getStrength);
}
if (collisionForce) {
collisionForce.radius(getCollisionRadius);
}
simulation.force(
"manybody",
d3.forceManyBody().strength(node => {
return node.radius * configuration.manyBodyStrength;
})
);
node.attr("cx", d => d.x).attr("cy", d => d.y);
title.attr("x", d => d.x).attr("y", d => d.y);
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);
}
// Funktion, um Nodes und force zu initialisieren
function updateNodeList() {
validateData();
const nodes = getNodes(data);
const links = getLinks(data);
simulation.nodes(nodes);
simulation.force(
"charge",
d3.forceManyBody().strength(configuration.manyBodyStrength)
);
linkForce.links(links);
simulation.force('link', linkForce);
simulation.force(
'x',
d3.forceX().strength((configuration.centerStrength * 100) / width)
);
simulation.force(
'y',
d3.forceY().strength((configuration.centerStrength * 100) / height)
);
collisionForce = d3.forceCollide();
simulation.force('collision', collisionForce);
simulation.velocityDecay(.6);
node = node
.data(nodes, d => d.id)
.join(
enter =>
enter
.append("circle")
.attr('opacity', 0)
.attr("stroke-width", "0")
.call(g =>
g
.transition()
.ease(d3.easeCubicOut)
.delay(0)
.duration(configuration.transitionDuration)
.attr('opacity', 1)
.attr("stroke-width", "10px")
),
update => update,
exit =>
exit
.transition()
.ease(d3.easeCubicOut)
.duration(50)
.attr("r", Math.E / 2)
.remove()
)
.attr('r', d => d.radius)
.attr('cx', d => d.x)
.attr("cy", d => d.y)
.attr('fill', d => d.fill)
.attr("stroke-width", "10px")
.attr("stroke", d => d.fill + 60)
.attr('cursor', d => (d.item.children.length > 0 ? 'pointer' : ''))
.on("click", (e, d) => {
if (d.item.children.length > 0) {
clickItem(d.id);
updateNodeList();
restartSimulation();
}
});
title = title
.data(nodes, d => d.id)
.join(
enter =>
enter
.append("text")
.style('opacity', 0)
.call(g =>
g
.transition()
.ease(d3.easeCubicOut)
.delay(0)
.duration(configuration.transitionDuration)
.style('opacity', 1)
),
update => update,
exit => exit.remove()
)
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => d.item.name)
.attr("text-anchor", "middle")
.attr("fill", "#fff")
.attr(
"font-family",
"-apple-system, system-ui, avenir, helvetica, roboto, noto, arial"
)
.attr("font-weight", "700")
.attr("dy", "2px")
.attr("font-size", "12px")
.style("pointer-events", "none");
link = link
.data(links, d => d.target.id)
.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.target.fill)
.call(enter =>
enter
.transition()
.ease(d3.easeCubicOut)
.delay(0)
.duration(configuration.transitionDuration)
.attr("stroke-opacity", 1)
)
);
}
simulation.on('tick', () => {
updateLinksAndNodes();
});
// helps to avoid initial scatter
for (let i = 0; i < 10; i++) {
simulation.tick();
}
simulation.alphaDecay(1 - Math.pow(0.001, 1 / 600));
updateNodeList();
return svg.node();
}