Public
Edited
Jul 5, 2023
Insert cell
Insert cell
Insert cell
chart = {
// Specify the chart’s dimensions (except for the height).
const width = 928;
const marginTop = 30;
const marginRight = 10;
const marginBottom = 0;
const marginLeft = 30;

/**
*
* 对数据进行转换
*
*/
// Determine the series that need to be stacked.
const series = d3.stack()
.keys(d3.union(data.map(d => d.age))) // distinct series keys, in input order
.value(([, D], key) => D.get(key).population) // get value for each series key and stack
(d3.index(data, d => d.state, d => d.age)); // group by stack then series key

// 💡 在垂直堆叠条形图中,条带是横向分布的,所以每个条带的宽度与页面宽度相关
// 这是由于在桌面端的浏览器中水平滚动的操作比较麻烦 ❓ 所以一般 svg 的横向宽度一般不会超过页面的宽度,避免产生横向滚动
// 所以条带横向分布时,每个条带的宽度就是由页面的宽度所决定(通过横坐标轴的比例尺算出条带的带宽)
// 而在桌面端的浏览器中垂直滚动很方便,所以这里可以手动设置条带的宽度,再计算出 svg 的高度,比页面/屏幕的高度更大也无所谓
// 由于不需要将图表的高度限制在一页中,所以基于条形图中条带的数量,通过计算来求出 svg 的高度
// 这里 series[0].length 所获得的是条带的数量(对应于 52 个州),每个条带的宽度(包含间隔)是 25px
// 再加上 marginTop 和 marginBottom 上下的留白,总长作为 svg 的高度
const height = series[0].length * 25 + marginTop + marginBottom;
// console.log(height)

/**
*
* 构建比例尺
*
*/
// 💡 设置横坐标轴的比例尺,与**垂直堆叠条形图**的纵坐标轴相对应
// 横坐标轴的数据是连续型的数值(各州的人口数量),使用 d3.scaleLinear 构建一个线性比例尺
const x = d3.scaleLinear()
.domain([0, d3.max(series, d => d3.max(d, d => d[1]))])
.range([marginLeft, width - marginRight]); // 这里的 width 宽度是页面宽度

// 💡 设置纵坐标轴的比例尺,与**垂直堆叠条形图**的横坐标轴相对应
// 纵坐标轴的数据是条形图的各种分类,使用 d3.scaleBand 构建一个带状比例尺
const y = d3.scaleBand()
.domain(d3.groupSort(data, D => -d3.sum(D, d => d.population), d => d.state))
.range([marginTop, height - marginBottom]) // 这里的 height 高度是前面根据条形图的条带宽度和数量计算出来的
.padding(0.08); // 并设置间隔占据(柱子)区间的比例

const color = d3.scaleOrdinal()
.domain(series.map(d => d.key))
.range(d3.schemeSpectral[series.length])
.unknown("#ccc");

// A function to format the value in the tooltip.
const formatValue = x => isNaN(x) ? "N/A" : x.toLocaleString("en")

// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width) // 这里的 width 宽度是页面宽度,
.attr("height", height) // 💡 但这里的 height 高度是前面根据条形图的条带宽度和数量计算出来的
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

// Append a group for each series, and a rect for each element in the series.
svg.append("g")
.selectAll()
.data(series)
.join("g")
.attr("fill", d => color(d.key))
.selectAll("rect")
.data(D => D.map(d => (d.key = D.key, d)))
.join("rect")
// 为每个小矩形分别设置左上角 (x, y) 及其 width 和 height 来确定其定位和形状
// 💡 每个矩形的左上角横轴定位 x 由它的堆叠下边界决定
// 可以通过它所绑定的数据(一个数组)的第一个元素 d[0] 来获取
// 使用横坐标轴的比例尺(线性比例尺)进行映射,求出具体的横轴坐标值
.attr("x", d => x(d[0]))
// 💡 每个矩形的左上角纵轴定位 y 由它所属的州的名称决定
// 可以通过所绑定数据的属性 d.data[0] 来获取
// 使用横纵坐标轴的比例尺(带状比例尺)进行映射,求出具体的纵轴坐标值
.attr("y", d => y(d.data[0]))
// 每个矩形的宽度
// ⚠️ 需要通过纵坐标轴比例尺的方法 y.bandwidth() 获取 band 的宽度
// 而不是前面设置的 25px,因为该值是包含条带之间的间隙的
.attr("height", y.bandwidth())
// 💡 每个矩形的宽度
// 由所绑定的数据(一个数组)的两个元素(上边界和下边界)之间的差值所决定
.attr("width", d => x(d[1]) - x(d[0]))
.append("title")
.text(d => `${d.data[0]} ${d.key}\n${formatValue(d.data[1].get(d.key).population)}`);

/**
*
* 绘制坐标轴
*
*/
// 绘制横坐标轴
svg.append("g")
// 通过设置 CSS 的 transform 属性将横坐标轴容器「移动」到顶部
.attr("transform", `translate(0,${marginTop})`)
// 💡 横轴是一个刻度值朝上的坐标轴
// 并使用坐标轴对象的方法 axis.ticks() 设置坐标轴的刻度数量和刻度值格式
// 其中第一个参数用于设置刻度数量(这里设置的是预期值,并不是最终值,D3 会基于出入的数量进行调整,以便刻度更可视)
// 这里设置为 (width / 100) 基于页面的宽度来设置横坐标轴的预期刻度数量
// 而第二个参数用于设置刻度值格式,这里设置为 "s" 表示数值采用 SI-prefix 国际单位制词头,例如 k 表示千,M 表示百万
// 具体参考 https://en.wikipedia.org/wiki/Metric_prefix
.call(d3.axisTop(x).ticks(width / 100, "s"))
.call(g => g.selectAll(".domain").remove());

// 绘制纵坐标轴
svg.append("g")
// 通过设置 CSS 的 transform 属性将纵坐标轴容器「移动」到左侧
.attr("transform", `translate(${marginLeft},0)`)
// 💡 纵轴是一个刻度值朝左的坐标轴
.call(d3.axisLeft(y).tickSizeOuter(0))
.call(g => g.selectAll(".domain").remove());

// Return the chart with the color scale as a property (for the legend).
return Object.assign(svg.node(), {scales: {color}});
}
Insert cell
// 📝 该 cell 只是用于演示效果
series = d3.stack()
.keys(d3.union(data.map(d => d.age))) // distinct series keys, in input order
.value(([, D], key) => D.get(key).population) // get value for each series key and stack
(d3.index(data, d => d.state, d => d.age)); // group by stack then series key
Insert cell
data = {
const data = await FileAttachment("us-population-state-age.csv").csv({typed: true});
return data.columns.slice(1).flatMap((age) => data.map((d) => ({state: d.name, age, population: d[age]})));
}
Insert cell
import {legend} from "@d3/color-legend"
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