chart = {
const svg = d3
.create("svg")
.attr("viewBox", [-width, -height, 2 * width, 2 * height]);
const zoom_g = svg.append("g");
function zoomed({ transform }) {
zoom_g.attr("transform", transform);
}
svg.call(
d3
.zoom()
.scaleExtent([0.2, 6])
.on("zoom", zoomed)
);
let link = zoom_g.append("g").selectAll("line");
let node = zoom_g.append("g").selectAll("circle");
let plus = zoom_g.append("g").selectAll("g");
let title = zoom_g.append("g").selectAll("text");
let simulation = d3.forceSimulation().on('tick', updateLinksAndNodes);
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 getCollisionRadius(node) {
if (node.item.parentId == null) return node.radius + 30;
return node.radius + 10;
}
////////////////////////////////
// TICK-FUNKTION (wird bei jedem Tick des Force-Renderings aufgerufen)
function updateLinksAndNodes() {
if (linkForce) {
linkForce.distance(getDistance);
linkForce.strength(config.linkStrength);
}
if (collisionForce) {
collisionForce.radius(getCollisionRadius);
}
simulation.force(
"charge",
d3.forceManyBody().strength(node => node.radius * config.manyBodyStrength)
);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.call(drag(simulation, updateNodeList));
title
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => d.item.title)
.call(wrap);
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);
plus.attr("transform", d => {
const x = (d.target.x - d.source.x) / 2 + d.source.x;
const y = (d.target.y - d.source.y) / 2 + d.source.y;
return `translate(${x}, ${y})`;
});
}
////////////////////////////////
// Funktion, um Nodes und force zu initialisieren
function updateNodeList() {
validateData();
const nodes = getNodes(data);
const links = filterVisibleLinks(getLinks(data), nodes);
simulation
.nodes(nodes)
.force("charge", d3.forceManyBody().strength(config.manyBodyStrength))
.force('link', linkForce.links(links))
.force('attractCenteredNodes', attractCenter().strength(0.8)) // Einbinden der Custom force
.force('x', d3.forceX().strength((config.centerStrength * 100) / width))
.force('y', d3.forceY().strength((config.centerStrength * 100) / height))
.force('collision', d3.forceCollide())
.velocityDecay(.25);
node = node
.data(nodes, d => d.id)
.join(
enter =>
enter.append("circle").call(g =>
g
.transition()
.ease(d3.easeCubicOut)
.duration(config.transitionDuration)
.attr("r", d => d.radius)
),
update => update,
exit =>
exit
.transition()
.ease(d3.easeCubicOut)
.duration(config.transitionDuration)
.attr("opacity", 0.5)
.attr("r", Math.E / 4)
.attr("cx", d =>
d.item.parent.visible
? d.item.parent.node.x
: getRootNode(d).node.x
)
.attr("cy", d =>
d.item.parent.visible
? d.item.parent.node.y
: getRootNode(d).node.y
)
.remove()
)
.attr('cx', d => d.x)
.attr("cy", d => d.y)
.attr('fill', d => d.fill)
.attr("stroke-width", d => {
if (d.item.type === 'user') return "5px";
return d.item.isOpen ? 0.2 * d.radius + "px" : "0px";
})
.attr("stroke", d => {
if (d.item.type === 'user') return '#ce1717';
return d.item.isOpen ? d.item.color + '60' : d.item.color;
})
.attr('cursor', d => (d.item.children.length > 0 ? 'pointer' : ''))
.on("click", (e, d) => {
if (d.item.children.length > 0) {
clickItem(d.id);
updateNodeList();
restartSimulation();
}
})
.on("contextmenu", e => e.preventDefault())
.on("mousedown", (e, d) => {
// node "wünscht" ins Zentrum gerückt zu werden.
// Deshalb wird ein Property mit dem aktuellen Zeitstempel geschrieben.
// alle anderen Nodes zurücksetzen
nodes.forEach(n => {
if (n.centeredRequested) {
n.centeredRequested = null;
}
});
d.centeredRequested = new Date();
})
.on("mouseup", (e, d) => {
if (!d.centeredRequested) {
return;
}
const startTime = d.centeredRequested;
const endTime = new Date();
const shouldCenter = endTime - startTime > 500;
if (shouldCenter) {
// alle anderen Nodes zurücksetzen
nodes.forEach(n => {
if (n.centered) {
n.centered = false;
}
});
d.centered = true;
updateNodeList();
restartSimulation();
}
d.centeredRequested = null;
});
title = title
.data(nodes, d => d.id)
.join(
enter =>
enter
.append("text")
.style('opacity', 0)
.text(d => d.item.title)
.call(g =>
g
.transition()
.ease(d3.easeCubicOut)
.delay(0)
.duration(config.transitionDuration)
.style('opacity', 1)
),
update => update,
exit => exit.remove()
)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("font-size", d => d.radius / 4 + "px")
.attr(
"font-family",
"-apple-system, system-ui, avenir, helvetica, roboto, noto, arial"
);
link = link
.data(links, d => d.target.id)
.join(enter =>
enter
.append("line")
.attr("stroke-opacity", "0")
.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.type === 'parent' ? d.source.fill : "#696969"
)
.call(enter =>
enter
.transition()
.ease(d3.easeCubicOut)
.delay(0)
.duration(config.transitionDuration)
.attr("stroke-opacity", 1)
)
);
plus = plus
.data(links.filter(l => l.type === 'user' || l.type === 'context'))
.join(enter =>
enter.append("g").attr("transform", d => {
const x = (d.target.x - d.source.x) / 2 + d.source.x;
const y = (d.target.y - d.source.y) / 2 + d.source.y;
return `translate(${x}, ${y})`;
})
);
plus
.append("circle")
.attr("stroke", "black")
.attr("fill", "white")
.attr("r", "15")
.attr("cx", "0")
.attr("cy", "0");
plus
.append("line")
.attr("x1", "-10")
.attr("y1", "0")
.attr("x2", "10")
.attr("y2", "0")
.attr("stroke", "black");
plus
.append("line")
.attr("x1", "0")
.attr("y1", "-10")
.attr("x2", "0")
.attr("y2", "10")
.attr("stroke", "black");
}
updateNodeList();
return svg.node();
}