Public
Edited
Oct 26, 2023
89 forks
142 stars
Revenue by music format, 1973–2018New Zealand tourists, 1921–2018Sea ice extent, 1978–2017U.S. population by State, 1790–1990Hertzsprung–Russell diagramSpilhaus shoreline mapWalmart’s growthInequality in American citiesU.S. state choroplethWorld choroplethScatterplot matrixLine chart, multiple seriesLine chart with tooltipTreemapBar chart transitionsBand chartCancer survival ratesSlope chartDifference chartDiverging bar chartDiverging stacked bar chartScatterplotSpike mapBubble mapBox plotPSR B1919+21Normalized stacked area chartDirected chord diagramChord dependency diagramVolcano contoursRadial area chartRadial stacked bar chart, sortedRadial stacked bar chartHorizon chartSunburstStreamgraphTidy treeCluster treeRadial cluster treeBeeswarmIciclePie chartCircle packingRadial tidy treeHorizontal bar chartBubble chartStacked area chartLine chart, percent changeSankey diagramIndex chartDisjoint force-directed graphForce-directed graphHistogramBollinger bandsCandlestick chartConnected scatterplotDot plotGrouped bar chartStacked bar chart, normalizedStacked bar chart, horizontalStacked bar chartDonut chartLine chart, missing dataArea chart with missing dataArea chartChoroplethCalendarLine chartColor SchemesWord cloudd3.packEncloseNon-contiguous cartogramStar mapSolar pathSolar TerminatorWorld airports VoronoiU.S. airports VoronoiGeoTIFF contours IIVector fieldRaster & vectorClipped map tilesVector tilesRaster tilesWeb Mercator tilesTissot’s indicatrixProjection comparisonWorld map (canvas)Bivariate choroplethColor legendStyled axesGraticule labels (stereographic)Voronoi labelsPie chart componentBubble chart componentScatterplot with shapesRealtime horizon chartRidgeline plotParallel coordinatesThreshold encodingGradient encodingVariable-color lineMarey’s TrainsMarimekkoChord diagramHierarchical edge bundling IIHierarchical edge bundlingArc diagramMobile patent suitsForce-directed treeTree of LifeIndented treeCircle packing componentNested treemapCascaded treemapParallel setsNormal quantile plotQ–Q PlotHexbin mapHexbin (area)HexbinContoursDensity contoursKernel density estimationMoving averageSeamless zoomable map tilesZoomable bar chartZoomable area chartPannable chartBrushable scatterplot matrixBrushable scatterplotVersor draggingZoomable sunburstZoomable icicleCollapsible treeZoomable circle packingZoomable treemapHierarchical bar chartWorld tourOrthographic to equirectangularZoom to bounding boxSmooth zoomingStreamgraph transitionsStacked-to-grouped barsBar Chart RaceScatterplot tourTemporal force-directed graph
Animated treemap
Also listed in…
Visualization
Insert cell
Insert cell
Insert cell
chart = {
const width = 928;
const height = width;

// This is normally zero, but could be non-zero if this cell is
// re-evaluated after the animation plays.
const initialIndex = viewof index.value;

// To allow the transition to be interrupted and resumed, we parse
// the displayed text (the state population) to get the current
// value at the start of each transition; parseNumber and
// formatNumber must be symmetric.
const parseNumber = string => +string.replace(/,/g, "");
const formatNumber = d3.format(",d");

// Get the maximum total population across the dataset. (We know
// for this dataset that it’s always the last value, but that isn’t
// true in general.) This allows us to scale the rectangles for
// each state to be proportional to the max total.
const max = d3.max(data.keys, (d, i) => d3.hierarchy(data.group).sum(d => d.values[i]).value);

// The category10 color scheme per state, but faded so that the
// text labels are more easily read.
const color = d3.scaleOrdinal()
.domain(data.group.keys())
.range(d3.schemeCategory10.map(d => d3.interpolateRgb(d, "white")(0.5)));

// Construct the treemap layout.
const treemap = d3.treemap()
.size([width, height])
.tile(d3.treemapResquarify) // to preserve orientation when animating
.padding(d => d.height === 1 ? 1 : 0) // only pad parents of leaves
.round(true);

// Compute the structure using the average value (since this
// orientation will be preserved using resquarify across the
// entire animation).
const root = treemap(d3.hierarchy(data.group)
.sum(d => Array.isArray(d.values) ? d3.sum(d.values) : 0)
.sort((a, b) => b.value - a.value));

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height + 20)
.attr("viewBox", [0, -20, width, height + 20])
.attr("style", "max-width: 100%; height: auto; font: 10px sans-serif; overflow: visible;");

// Draw a box representing the total population for each time. Only
// show the boxes after the current time (to avoid distracting gray
// lines in between the padded treemap cells).
const box = svg.append("g")
.selectAll("g")
.data(data.keys.map((key, i) => {
const value = root.sum(d => d.values[i]).value;
return {key, value, i, k: Math.sqrt(value / max)};
}).reverse())
.join("g")
.attr("transform", ({k}) => `translate(${(1 - k) / 2 * width},${(1 - k) / 2 * height})`)
.attr("opacity", ({i}) => i >= initialIndex ? 1 : 0)
.call(g => g.append("text")
.attr("y", -6)
.attr("fill", "#777")
.selectAll("tspan")
.data(({key, value}) => [key, ` ${formatNumber(value)}`])
.join("tspan")
.attr("font-weight", (d, i) => i === 0 ? "bold" : null)
.text(d => d))
.call(g => g.append("rect")
.attr("fill", "none")
.attr("stroke", "#ccc")
.attr("width", ({k}) => k * width)
.attr("height", ({k}) => k * height));

// Render the leaf nodes of the treemap.
const leaf = svg.append("g")
.selectAll("g")
.data(layout(initialIndex))
.join("g")
.attr("transform", d => `translate(${d.x0},${d.y0})`);

leaf.append("rect")
.attr("id", d => (d.leafUid = DOM.uid("leaf")).id)
.attr("fill", d => { while (d.depth > 1) d = d.parent; return color(d.data[0]); })
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0);

// Clip the text to the containing node.
leaf.append("clipPath")
.attr("id", d => (d.clipUid = DOM.uid("clip")).id)
.append("use")
.attr("xlink:href", d => d.leafUid.href);

// Generate two tspans for two lines of text (name and value).
leaf.append("text")
.attr("clip-path", d => d.clipUid)
.selectAll("tspan")
.data(d => [d.data.name, formatNumber(d.value)])
.join("tspan")
.attr("x", 3)
.attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`)
.attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null)
.text(d => d);

leaf.append("title")
.text(d => d.data.name);

// Scale the treemap layout to fit within a centered box whose area
// is proportional to the total current value. This makes the areas
// of each state proportional for the entire animation.
function layout(index) {
const k = Math.sqrt(root.sum(d => d.values[index]).value / max);
const tx = (1 - k) / 2 * width;
const ty = (1 - k) / 2 * height;
return treemap.size([width * k, height * k])(root)
.each(d => (d.x0 += tx, d.x1 += tx, d.y0 += ty, d.y1 += ty))
.leaves();
}

// Expose an update method on the chart that allows the caller to
// initiate a transition. The given index represents the frame
// number (0 for the first frame, 1 for the second, etc.).
return Object.assign(svg.node(), {
update(index, duration) {
box.transition()
.duration(duration)
.attr("opacity", ({i}) => i >= index ? 1 : 0);

leaf.data(layout(index)).transition()
.duration(duration)
.ease(d3.easeLinear)
.attr("transform", d => `translate(${d.x0},${d.y0})`)
.call(leaf => leaf.select("rect")
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0))
.call(leaf => leaf.select("text tspan:last-child")
.tween("text", function(d) {
const i = d3.interpolate(parseNumber(this.textContent), d.value);
return function(t) { this.textContent = formatNumber(i(t)); };
}));
}
});
}
Insert cell
update = chart.update(index, 2500) // trigger animation from the scrubber
Insert cell
data = {
const keys = d3.range(1790, 2000, 10);
const [regions, states] = await Promise.all([
FileAttachment("census-regions.csv").csv(), // for grouping states hierarchically
FileAttachment("population.tsv").tsv() // a wide dataset of state populations over time
]).then(([regions, states]) => [
regions,
states.slice(1).map((d) => ({
name: d[""], // the state name
values: keys.map(key => +d[key].replace(/,/g, "")) // parse comma-separated numbers
}))
]);
const regionByState = new Map(regions.map(d => [d.State, d.Region]));
const divisionByState = new Map(regions.map(d => [d.State, d.Division]));
return {keys, group: d3.group(states, d => regionByState.get(d.name), d => divisionByState.get(d.name))};
}
Insert cell
import {Scrubber} from "@mbostock/scrubber"
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more