Public
Edited
Nov 14, 2022
Insert cell
Insert cell
chart = BarChart(alphabet, {
x: d => d.frequency,
y: d => d.letter,
// 调用函数时,传入纵轴的定义域 yDomain 手动设置分组类别
// 并且类别是按照其相应的频次进行降序排列的
// 使用方法 d3.groupSort(iterable, accessor, key) 对可迭代对象(如数组)iterable 进行归类分组,其中 key 指定分组的依据
// 最后返回(排序好的)类别数组,最终的输出值可以查看 👇 下一个 cell 所演示的结果
// 由于该方法默认按照 accessor 访问器的返回值升序排列,这里是希望降序排列,仅仅需要在返回值前面添加负号 - 即可
yDomain: d3.groupSort(alphabet, ([d]) => -d.frequency, d => d.letter),
xFormat: "%", // 横轴的刻度值采用百分比表示
xLabel: "Frequency →",
width,
color: "steelblue"
})
Insert cell
d3.groupSort(alphabet, ([d]) => -d.frequency, d => d.letter)
Insert cell
// 读取并解析数据
alphabet = FileAttachment("alphabet.csv").csv({typed: true})
Insert cell
Insert cell
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/horizontal-bar-chart
function BarChart(data, {
// 每个数据点的 x 值的 accessor function 访问函数
// 从数据点的原始值中提取出用作横坐标值(横坐标值应该采用 quantitative 数值型数据,以表示具体定量的值)
x = d => d,
// 每个数据点的 y 值的 accessor function 访问函数
// 从数据点的原始值中提取出用作纵坐标值(纵坐标值应该采用 ordinal 离散型数据,以表示不同类别)
y = (d, i) => i,
// 每个数据点的标注信息的 accessor function 访问函数,该函数的入参是各个数据点 d
title, // 为每个柱子标注的信息
titleColor = "white", // 标注信息在柱子上时,文字的颜色
titleAltColor = "currentColor", // 标注信息在条形图的背景上时,文字的颜色
// 以下有一些关于图形的宽高、边距尺寸相关的参数
// margin 为前缀的产生是在外四边留白,构建一个显示的安全区,以便在四周显示坐标轴
marginTop = 30, // the top margin, in pixels
marginRight = 0, // the right margin, in pixels
marginBottom = 10, // the bottom margin, in pixels
marginLeft = 30, // the left margin, in pixels
width = 640, // svg 的宽度
height, // svg 的高度
xType = d3.scaleLinear, // 横轴所采用的比例尺,对于数值型数据,默认采用线性比例尺
xDomain, // 横坐标轴的定义域范围,即数据的范围 [xmin, xmax]
// 横坐标轴的值域(可视化属性,这里是长度)范围 [left, right] 从左至右,和我们日常使用一致
xRange = [marginLeft, width - marginRight],
xFormat, // 格式化数字的说明符 specifier 用于格式化横坐标轴的刻度值
xLabel, // 为横坐标轴添加额外文本(一般是刻度值的单位等信息)
// 纵坐标轴的定义域范围,是一个数组,其中的每一个元素都是一个不同的类别
// 一般是基于原始数据(去重)提取而成的
// 也可以在这里手动直接设置希望显示的类别,然后在函数内部有相关的代码对数据进行筛选
yDomain, // an array of (ordinal) y-values
// 纵坐标轴的值域
// 如果纵坐标是映射定量数值时,应该特别留意 svg 的坐标体系的正方向(向右,向下)
// 但是因为当前绘制的是横向条形图,纵轴映射的是分类数据
// 💡 所以这里的值域**不一定需要**采用从下往上与定义域进行映射 [bottom, top]
// 这里默认就采用 [top, bottom]
yRange, // [top, bottom]
yPadding = 0.1, // 设置条形图中邻近柱子之间的间隔大小
color = "currentColor", // 柱子的颜色
} = {}) {
/**
*
* 处理数据
*
*/
// 通过 d3.map() 迭代函数,使用相应的 accessor function 访问函数从原始数据 data 中获取相应的值
const X = d3.map(data, x);
const Y = d3.map(data, y);

/**
*
* 构建比例尺和坐标轴
*
*/
// 计算坐标轴的定义域范围
// 横坐标轴的定义域 [xmin, xmax] 其中最大值 xmax 使用方法 d3.max(X) 从所有数据点的 X 值获取
if (xDomain === undefined) xDomain = [0, d3.max(X)];

// 如果调用函数时没有传入纵坐标轴的定义域范围 yDomain,则将其先设置为由所有数据点的 y 值构成的数组
if (yDomain === undefined) yDomain = Y;
// 然后基于 yDomain 值创建一个 InternSet 对象,以便去重
// 这样所得的 yDomain 里的元素都是唯一的,作为纵坐标轴的定义域(分类的依据)
yDomain = new d3.InternSet(yDomain);

// 这里还做了一步数据清洗
// 基于纵坐标轴的定义域所包含的类别
// 使用 JavaScript 数组的原生方法 arr.filter() 筛掉不属于 yDomain 类别的任意一个的数据点
// 其中 d3.range(X.length) 生成一个等差数列(使用 Y.length 也可以),作为索引值,便于对数据点进行迭代
const I = d3.range(X.length).filter(i => yDomain.has(Y[i]));

// 如果调用函数时没有设置高度,则基于柱子的数量和上下的留白宽度算出默认的 svg 的高度
// 其中 yDomain.size 得到 InternSet 对象中包含的元素个数(即类别数量),然后这里假设每个柱子宽度是 25px
// 其中加上 yPadding 是为了考虑柱子间存在的间隔
// 通过 Math.ceil() 方法进行向上修约(让 svg 留足空间来绘制条形图)
if (height === undefined) height = Math.ceil((yDomain.size + yPadding) * 25) + marginTop + marginBottom;
// 然后计算出纵坐标轴的值域 [top, bottom]
if (yRange === undefined) yRange = [marginTop, height - marginBottom];

// 横坐标轴的数据是连续型的数值,默认使用 d3.scaleLinear 构建一个线性比例尺
const xScale = xType(xDomain, xRange);
// 横轴是一个刻度值朝上的坐标轴
// 并设置坐标轴的刻度数量和刻度值格式
const xAxis = d3.axisTop(xScale).ticks(width / 80, xFormat);

// 纵坐标轴的数据是条形图的各种分类,使用 d3.scaleBand 构建一个带状比例尺
// 并设置间隔占据(柱子)区间的比例
const yScale = d3.scaleBand(yDomain, yRange).padding(yPadding);
// 纵轴是一个朝左的坐标轴
// 而且将坐标轴的外侧刻度 tickSizeOuter 长度设置为 0(即取消坐标轴首尾两端的刻度)
const yAxis = d3.axisLeft(yScale).tickSizeOuter(0);

/**
*
* 柱子的标注信息的 accessor function 访问函数
* 统一为**基于索引**获取数据点的标注信息
*
*/
// 如果调用函数时没有设定标注信息的 accessor function 访问函数
// 则构建一个 accessor function 访问函数
// 它接受一个表示数据点的索引值,并从 X 中分别提取出柱子相应的频率组成标注信息
if (title === undefined) {
// 构建一个数值格式器(根据设置来自动确定数据的精度,更适用于阅读)
const formatValue = xScale.tickFormat(100, xFormat);
// 标注信息由该柱子相应的频率 formatValue(Y[i]) 组成
title = i => `${formatValue(X[i])}`;
} else {
// 如果调用函数时由设定标注信息的 accessor function 访问函数
// 为了便于后面统一基于索引值进行调用,需要进行转换
// 将 title 变成**基于索引**获取数据点的标注信息的 accessor function 访问函数
const O = d3.map(data, d => d);
const T = title; // 将原始的标注信息访问函数赋给 T 变量
title = i => T(O[i], i, data); // title 变成基于索引的标注信息 accessor function 访问函数
}

/**
*
* 绘制条形图的容器(边框和坐标轴)
*
*/
// 创建 svg(返回的是一个包含 svg 元素的选择集)
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");

// 绘制横坐标轴
svg.append("g")
// 通过设置 CSS 的 transform 属性将横向坐标轴容器「移动」到顶部
.attr("transform", `translate(0,${marginTop})`)
.call(xAxis) // 调用坐标轴(对象)方法,将坐标轴在相应容器内部渲染出来
.call(g => g.select(".domain").remove()) // 删掉上一步所生成的坐标轴的轴线(它含有 domain 类名)
.call(g => g.selectAll(".tick line").clone() // 这里复制了一份刻度线,用以绘制竖向的参考线
.attr("y2", height - marginTop - marginBottom) // 调整复制后的刻度线的终点位置(往下移动)
.attr("stroke-opacity", 0.1)) // 调小网格线的透明度
.call(g => g.append("text") // 为坐标轴添加额外信息名称(一般是刻度值的单位等信息)
// 将该文本移动到容器的右侧
.attr("x", width - marginRight)
.attr("y", -22)
.attr("fill", "currentColor")
.attr("text-anchor", "end") // 设置文本的对齐方式
.text(xLabel)); // 文本内容
// 绘制纵坐标轴
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`) // 将纵坐标轴容器定位到左侧
.call(yAxis);

/**
*
* 绘制条形图内的柱子
*
*/
svg.append("g")
.attr("fill", color)
// 使用 <rect> 元素来绘制柱子
// 通过设置矩形的左上角 (x, y) 及其 width 和 height 来确定其定位和形状
.selectAll("rect")
.data(I) // 绑定的数据是表示数据点的索引值(数组),以下会通过索引值来获取各柱子相应的数据
.join("rect")
// 因为绘制的是水平方向的条形图
// 所以每个柱子都是对齐到 y 轴的,即矩形的左上角横坐标值都是 xScale(0)
.attr("x", xScale(0))
// 通过索引值来读取矩形的左上角纵坐标值
.attr("y", i => yScale(Y[i]))
// 矩形的宽度
// 即水平柱子的长度,通过比例尺映射后,柱子的宽度是 xScale(X[i]) - xScale(0)) 的差值
.attr("width", i => xScale(X[i]) - xScale(0))
// 矩形的高度
// 即柱子的大小,通过纵轴的比例尺的方法 yScale.bandwidth() 获取 band 的宽度(不包含间隙 padding)
.attr("height", yScale.bandwidth());

/**
*
* 设置每个柱子的标注信息
* 根据柱子的长短,决定标注文本的不同位置和颜色
* 如果柱子较长,将标注信息置于柱子上,文本颜色为白色
* 如果柱子较短,将标注信息置于柱子旁边(在条形图的背景上),文本的颜色为黑色
*/
// 之前在垂直条形图中,只有当鼠标 hover 在柱子上才显示的标注信息,
// 💡 而现在由于条形图是横向的,与文字阅读方向相同,所以现在有足够的位置可以直接显示在相应的柱子上
svg.append("g")
// 先默认标注信息都在柱子上,设置文字的颜色
.attr("fill", titleColor)
// 因为默认文本在柱子的最右侧,所以对齐方式设置为 end
// 即文本的 (x, y) 定位坐标是其末尾,文字向左展开
.attr("text-anchor", "end")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("text")
.data(I) // 绑定的数据是表示数据点的索引值(数组),以下会通过索引值来获取各柱子相应标注信息
.join("text")
// 将文本移动到相应的柱子上
.attr("x", i => xScale(X[i])) // 文本的横向坐标,移到柱子的最右侧
.attr("y", i => yScale(Y[i]) + yScale.bandwidth() / 2) // 文本的纵向坐标
.attr("dy", "0.35em")
.attr("dx", -4)
.text(title)
// 最后再基于柱子的长度对文本的定位和颜色进行调整
.call(text =>
// 筛选出较短的柱子所对应的文本
// 当矩形的长度 xScale(X[i]) - xScale(0) 小于 20 时就是较短的柱子
text.filter(i => xScale(X[i]) - xScale(0) < 20) // short bars
.attr("dx", +4) // 将文本稍微向右移动,这样文本就位于条形图的白色背景上(而不是彩色的柱子上)
.attr("fill", titleAltColor) // 所以需要将白色的文字改成黑色的文字
// 而且改变文字的对齐方式为 start,即文本的 (x, y) 定位坐标是其开头,文字向右展开
.attr("text-anchor", "start")
);


return svg.node();
}
Insert cell
import {howto, altplot} from "@d3/example-components"
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