Public
Edited
Jul 7
Insert cell
Insert cell
Insert cell
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();
}
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
Insert cell
data
X*
Y*
Color
Size
Facet X
parent
Facet Y
Mark
Auto
Type Chart, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
function fillOpacity(d, i, focus) {
const textNode = d3.select(this.parentNode).datum();

// Always show: country (depth 0), provinces (depth 1), gemeentes (depth 2)
const isAlwaysVisible =
textNode.depth <= 2 || textNode.id.includes("Amsterdam ");

// ONLY show wijken of the focused gemeente
const isWijkOfFocusedGemeente =
textNode.depth >= 3 && textNode.parent === focus;

const shouldShow = isAlwaysVisible || isWijkOfFocusedGemeente;
return shouldShow ? (i === 0 ? 1 : 0.7) : 0;
}
Insert cell
color = d3.scaleSequential([0, 6], d3.interpolateBlues)
Insert cell
treemap = d3
.treemap()
// treemapBinary: Recursively partitions the specified nodes into an approximately-balanced binary tree, choosing horizontal partitioning for wide rectangles and vertical partitioning for tall rectangles
.tile(d3.treemapBinary)
.size([fixedWidth, fixedHeight])
.paddingOuter(2)
.paddingTop((d) => {
if (d.depth === 2) {
return 12;
}

return 18;
})
.paddingInner(1)
.round(true)
Insert cell
hierarchicalData = d3
.stratify()
.id((d) => d.name)
.parentId((d) => d.parent)(data)
.sum((d) => d.inh)
.sort((a, b) => {
return b.value - a.value;
})
Insert cell
// https://observablehq.com/@observablehq/synchronized-inputs
function set(input, value) {
input.value = value;
input.dispatchEvent(new Event("input"));
}
Insert cell
fixedWidth = 928
Insert cell
selected = search.length === 1 ? search[0] : null
Insert cell
fixedHeight = fixedWidth * ratio
Insert cell
height = width * ratio
Insert cell
ratio = 1
Insert cell
root = treemap(hierarchicalData)
Insert cell
data = FileAttachment("wijken@26.json").json()
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