chart = {
const clusterWidth = 160;
const zoomFactor = 3.5;
let zoomLevel = 0;
const clusters = data.children
.sort((a, b) => (a.color > b.color ? 1 : -1))
.map(child => pack(child, clusterWidth, clusterWidth));
const simulation = forceLayout(clusters);
const svg = d3
.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("class", "root");
const main = svg.append("g").attr("class", "main");
const zoom = d3
.zoom()
.scaleExtent([0.2, 6])
.on("zoom", event => {
const { transform } = event;
main.attr("transform", transform);
});
svg.call(zoom).on("click", zoomTo);
const cluster = main
.selectAll("g")
.data(clusters)
.join("g")
.call(drag(simulation));
simulation.on("tick", () => {
cluster.attr("transform", d => `translate(${d.x}, ${d.y})`);
});
// Cluster Kreis
cluster
.append("circle")
.attr('r', "80")
.attr("class", d => getCircleClasses(d, d))
.on("click", (event, d) => {
event.stopPropagation();
zoomTo(event, d);
});
// Cluster Border Kreis
cluster
.append("circle")
.attr('r', "85")
.attr('class', d => {
const colorClass = d.data.color;
return `outline ${colorClass}`;
});
// Cluster Child Kreise (Pack)
cluster.each((cluster, idx, elements) => {
const self = d3.select(elements[idx]);
const children = cluster.descendants().slice(1);
const enter = self
.selectAll("g")
.data(children)
.enter()
.append("g")
.attr(
"transform",
d => `translate(${d.x - clusterWidth / 2},${d.y - clusterWidth / 2})`
);
enter
.append("circle")
.attr("r", d => d.r)
.attr("class", d => getCircleClasses(cluster, d))
.on("click", (event, d) => {
event.stopPropagation();
zoomTo(event, d);
});
});
// ------------ Zoom Funktion ------------
function zoomTo(event, d) {
const isRoot = event.target.tagName === 'svg';
if (isRoot) {
transform(0, 0, 0, true);
return;
}
// Aktuelle Position des Elementes auf SVG Koordinaten System auslesen
const { x, y } = getNewPosition(event.target);
transform(x, y, d.depth, false);
}
// ------------ Hilfsfunktion, um neue Position herauszufinden ------------
// anhand eines Dom-Elementes. Gibt ein Objekt mit x, y zurück
function getNewPosition(element) {
// Aktuelle Position des Elementes auf SVG Koordinaten System auslesen
const pos = getCirclePosition(element);
// Aktuelles Transform auslesen über Dom-Element, wo zoom angehängt ist. (main)
var transform = d3.zoomTransform(main.node());
// Aktuelle Position - Aktuelle Transform Position geteilt durch transform Zoom Faktor
const x = (pos.x - transform.x) / transform.k;
const y = (pos.y - transform.y) / transform.k;
return { x, y };
}
// ------------ Transition Transform auf neue Position ------------
function transform(x, y, depth, isRoot) {
zoomLevel = isRoot ? 0 : depth + 1;
const scale = isRoot ? 1 : zoomLevel * zoomFactor;
svg
.transition()
.duration(700)
.call(zoom.transform, function() {
return d3.zoomIdentity.scale(scale).translate(-x, -y);
})
.on("end", () => {
// update css classes
cluster.each((cl, idx, elements) => {
const self = d3.select(elements[idx]);
self.selectAll("circle:not(.outline").attr("class", d => {
return getCircleClasses(cl, d);
});
});
});
}
// ------------ Hilfsfunktion, um die Klassen eines Kreises zu bestimmen ------------
function getCircleClasses(cluster, d) {
const depth = d.depth;
const color = cluster.data.color;
const shouldBeClickable = depth <= zoomLevel;
return `depth${depth} ${color} ${shouldBeClickable ? 'clickable' : ''}`;
}
return svg.node();
}