chart = {
let focus = root;
const initialViewBox = [0, 0, fixedWidth, fixedHeight];
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", initialViewBox)
.attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;");
const shadow = DOM.uid("shadow");
svg
.append("filter")
.attr("id", shadow.id)
.append("feDropShadow")
.attr("flood-opacity", 0.2)
.attr("dx", 0)
.attr("stdDeviation", 3);
const node = svg
.selectAll("g")
.data(d3.group(root, (d) => d.depth))
.join("g")
.attr("filter", shadow)
.selectAll("g")
.data((d) => d[1])
.join("g")
.attr("pointer-events", (d) => (!d.children ? "none" : null))
.attr("transform", (d) => `translate(${d.x0},${d.y0})`);
const format = d3.format(",d");
node.append("title").text(
(d) =>
`${d
.ancestors()
.reverse()
.map((d) => d.data.name)
.join("/")}\nPopulation: ${format(d.value)}`
);
node
.append("rect")
.attr("id", (d) => (d.nodeUid = DOM.uid("node")).id)
.attr("fill", (d) => color(d.height))
.attr("width", (d) => d.x1 - d.x0)
.attr("height", (d) => d.y1 - d.y0)
.attr("pointer-events", (d) => (!d.children ? "none" : null))
.on("mouseover", function () {
d3.select(this).attr("stroke", "#000");
})
.on("mouseout", function () {
d3.select(this).attr("stroke", null);
})
.on("click", (event, d) => {
if (d.children && focus !== d) {
zoom(event, d), event.stopPropagation();
}
});
node
.append("clipPath")
.attr("id", (d) => (d.clipUid = DOM.uid("clip")).id)
.append("use")
.attr("xlink:href", (d) => d.nodeUid.href);
const label = node
.append("text")
.attr("font-size", (d) =>
d.depth === 2 || d.id.includes("Amsterdam ") ? 8 : null
)
.attr("pointer-events", "none")
.attr("clip-path", (d) => d.clipUid)
.attr("fill", (d) => (d.id === "Nederland" ? "white" : "inherit"));
const tspan = label
.selectAll("tspan")
.data((d) => {
return [d.data.name, format(d.value)];
})
.join("tspan")
.attr("fill-opacity", function (d, i) {
return fillOpacity.call(this, d, i, focus);
})
.text((d) => d);
node
.filter((d) => d.children)
.selectAll("tspan")
.attr("dx", 3)
.attr("y", function (d) {
const textNode = d3.select(this.parentNode).datum();
if (textNode.depth === 2) {
return 9;
}
return 13;
});
node
.filter((d) => !d.children)
.selectAll("tspan")
.attr("x", 1)
.attr(
"y",
(d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`
);
svg.on("click", (event) => zoom(event, root));
if (selected) {
if (!selected.children) {
zoom(null, selected.parent);
} else {
zoom(null, selected);
}
}
let currentViewBox = initialViewBox;
function zoom(event, d) {
focus = d;
// sync input to clicked node
if (event) {
set(viewof search[0], [focus.id]);
}
const padding = 10;
const rectWidth = d.x1 - d.x0;
const rectHeight = d.y1 - d.y0;
const rectAspectRatio = rectWidth / rectHeight;
const svgAspectRatio = width / height;
let targetViewBox;
if (focus.depth >= 2) {
node.attr("pointer-events", null);
} else {
node.attr("pointer-events", (d) => (!d.children ? "none" : null));
}
if (rectAspectRatio > svgAspectRatio) {
// Rectangle is wider than SVG, fit by width
const viewWidth = rectWidth + padding * 2;
const viewHeight = viewWidth / svgAspectRatio;
const centerY = (d.y0 + d.y1) / 2;
targetViewBox = [
d.x0 - padding,
centerY - viewHeight / 2,
viewWidth,
viewHeight
];
} else {
// Rectangle is taller than SVG, fit by height
const viewHeight = rectHeight + padding * 2;
const viewWidth = viewHeight * svgAspectRatio;
const centerX = (d.x0 + d.x1) / 2;
targetViewBox = [
centerX - viewWidth / 2,
d.y0 - padding,
viewWidth,
viewHeight
];
}
const t = svg
.transition()
.duration(event?.altKey ? 7500 : 750)
.ease(d3.easeCubicInOut)
.tween("viewBox", () => {
const interpolator = d3.interpolateZoom(
[
currentViewBox[0] + currentViewBox[2] / 2,
currentViewBox[1] + currentViewBox[3] / 2,
currentViewBox[2]
],
[
targetViewBox[0] + targetViewBox[2] / 2,
targetViewBox[1] + targetViewBox[3] / 2,
targetViewBox[2]
]
);
return (t) => {
const [x, y, w] = interpolator(t);
const h = w / svgAspectRatio;
const newViewBox = [x - w / 2, y - h / 2, w, h];
svg.attr("viewBox", newViewBox.join(" "));
if (t === 1) {
currentViewBox = [...newViewBox];
}
};
});
node
.select("text")
.attr("font-size", (d) => {
const isWijk = d.depth >= 3 && !d.id.includes("Amsterdam ");
if (isWijk) {
return 2.5;
}
// scale down gemeente text to fit the square when zoomed in
if (focus.depth === 2) {
const rectWidth = d.x1 - d.x0;
const rectHeight = d.y1 - d.y0;
const maxFontSize = Math.min(rectWidth / 15, rectHeight / 4);
return `${Math.max(3, Math.min(maxFontSize, 8))}px`;
}
const isGemeenteOrStadsdeel =
d.depth === 2 || d.id.includes("Amsterdam ");
return isGemeenteOrStadsdeel ? 8 : null;
})
.transition(t)
.selectAll("tspan")
.attr("fill-opacity", function (d, i) {
return fillOpacity.call(this, d, i, focus);
});
}
return svg.node();
}