Published
Edited
Jan 29, 2021
6 forks
8 stars
Insert cell
Insert cell
chart = {
const clusterWidth = 160;
const zoomFactor = 3.5; // Wie weit pro Stufe weiter hineingezoomt wird

// Aktuelle ZoomStufe (0 - 3) Wird überschrieben, wenn hineingeklickt wird.
let zoomLevel = 0;

// Erstelle ein Pack Layout für jedes Cluster
const clusters = data.children
.sort((a, b) => (a.color > b.color ? 1 : -1))
.map(child => pack(child, clusterWidth, clusterWidth));

// Erstelle Force Layout für die Cluster
// (x und y der obersten Ebene werden gesetzt)
const simulation = forceLayout(clusters);

// SVG Container
const svg = d3
.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("class", "root");

// main group (Transform wird angehängt für Padding & Zooming)
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);

// Cluster group (Container Element für alle Cluster inkl. der enthaltenen Komponenten)
// Jedes Cluster Group Objekt wird positioniert über die Force simulation (mit jedem Tick)
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();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
height = 600
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more