D3 是一个数据驱动的可视化 js 库,有很高的灵活性,相对应,使用难度和学习成本也更高。
本篇笔记包含 D3 操作元素、SVG、比例尺、坐标轴、事件等基本概念,以及柱状图、散点图、热度图、地理路径、树形图的绘制方式。
是 freeCodeCamp 课程 “数据可视化” 中的笔记,学习了基础用法,并结合认证项目进行实践,加深理解与应用。
操作 HTML 元素
选择和添加元素
- 选中元素:
select('body')
- 选择所有匹配元素:
select('li')
- 附加元素:
append('h1')
- 获取或设置文本:
text()
和text('Learning D3')
d3.select('body')
.append('h1')
.text('Learning D3')
使用 D3 中的数据
- 先选择对应的元素,比如想基于数据创建 h2 ,就先选中 h2
- 给元素设置数据集:
data(dataset)
; 其中data()
方法会解析数据集,任何链接在后面的方法都会为数据集中的每个对象运行一次 - 把数据集和选择元素比较,用于补齐缺少的元素:
enter()
;如果有 9 个数据,即可通过 append 去创建 9 个元素。 - 基于数据去设置文本:
text((d) => d)
const dataset = [12, 31, 22, 17, 25, 18, 29, 14, 9];
// 在这行下面添加代码
d3.selectAll('h2')
.data(dataset)
.enter()
.append('h2')
.text((d) => `${d} USD`)
设置属性和样式
- 设置内联 CSS 样式: `style('font-family', 'verdana')
- 基于数据去设置内联 CSS 样式: `style('color', d => d < 20 ? 'red' : 'green')
- 设置 HTML 属性,包括 class:
attr('class', 'bar')
; 也支持基于数据通过回调函数去设置 - class 可以用于
:hover
改变样式等操作,写在 css 的样式优先于 attr 中的样式
d3.select('body').selectAll('div')
.data(dataset)
.enter()
.append('div')
.attr('class', 'bar')
.style('margin', '2px')
.style('height', (d) => `${d * 10}px`)
D3 中的 SVG
SVG 是 Scalable Vector Graphics 的缩写,缩放不会被像素化,用于制作常见的几何图形。
宽高单位
通过以上操作 HTML 元素的方法来操作 svg,width 和 height 支持 attr 和或 style 定义,需注意 width 和 height 没有单位,它定义在默认缩放下的宽度,而无论怎么缩放,元素的宽高比永远是 5:1 。
const w = 500;
const h = 100;
const svg = d3.select("body")
.append('svg')
.attr('width', w)
.attr('height', h)
// 或
const svg = d3.select("body")
.append('svg')
.style('width', w)
.style('height', h)
rect
形状
rect
用于创建方形rect
这类元素必须添加在svg
元素内- 通过 x 和 y 定义在 svg 中的位置,y 坐标是从 SVG 画布的顶端开始测量的,而不是从底端。可设置 y 值为容器高度减去柱子高度,使其翻转过来,正向显示
- 用
fill
属性着色
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", (d, i) => i * 30)
.attr("y", (d, i) => h - 3 * d)
.attr("width", 25)
.attr("height", (d, i) => 3 * d)
.attr("fill", "navy");
text
元素
- 需要
x
和y
属性来指定在 svg 上的位置 text
元素通过text()
方法填充文本
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text((d) => d)
.attr("x", (d, i) => i * 30)
.attr("y", (d, i) => h - (3 * d) - 3);
title
元素
- 用于悬停提示文本,类似 html 的 text 属性
title
元素通过text()
方法填充文本
.append('title')
.text(d => d)
circle
形状
- 用于创建圆形
- 三个主要的属性。 cx 和 cy 属性是坐标,定义图形的中心位置。 r 属性定义半径大小。
- cy 坐标跟 y 一样是从顶部,可以使用容器高度减去坐标位置,使其正向显示
比例尺基本用法
比例尺可帮助布局数据,用于缩放,将大量数据放到较小的显示区域。
线性比例尺 scaleLinear
https://d3js.org/d3-scale/linear
对于线性缩放(通常使用于定量数据),使用 D3 的 scaleLinear()
方法创建比例尺,返回一个 scale 函数。使用 scale 函数,调用函数时传入参数 50 ,控制缩放比例。
const scale = d3.scaleLinear(); // 在这里创建比例尺
const output = scale(50); // 调用 scale,传入一个参数
默认情况下,比例尺使用一对一关系(identity relationship), 即输入值直接映射为输出值。
定义域与值域
比例尺可以使用更灵活的配置:
- 定义域:比例尺的输入信息,通过
domain()
设置,假设有一个数据集值的范围为 50 到 480 - 值域:输出信息,通过
range()
方法设置,沿着 SVG 上的x
比例尺映射这些点,假设在 10 单位和 500 单位之间
scale.domain([50, 480]);
scale.range([10, 500]);
通过 padding 在散点图和 SVG 边缘之间添加空隙: 想象 x 比例尺是一条从 0 到 500 (SVG 宽的值)的水平直线。 在 range()
方法中包含 padding 使散点图沿着这条直线从 30 (而不是 0)开始,在 470 (而不是 500)结束。
const w = 500;
const padding = 30;
scale.range([padding, w - padding]);
查找数据集的最大最小值
设置域的时候,想找到数据集中的最小值和最大值,可使用 min()
和 max()
来返回这些值。
一维数组中找:
const exampleData = [34, 234, 73, 90, 6, 52];
d3.min(exampleData)
d3.max(exampleData)
找到二维数组的最大值和最小值的例子:
const locationData = [[1, 7],[6, 3],[8, 3]];
const minX = d3.min(locationData, (d) => d[0]);
min() 和 max() 方法在设置比例尺时十分有用,如使用 max() 方法创建 x 和 y 比例尺。为 y 坐标设置 range 时,一般大的值是第一个参数,小的值是第二个参数,使其可保持从下往上的顺序。
const w = 500;
const h = 500;
const padding = 30;
// 创建 x 和 y 比例尺
const xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, (d) => d[0])])
.range([padding, w - padding]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, (d) => d[1])])
.range([h - padding, padding]);
为了不要顶住比例尺的起始端或末端,设置 domain 时可适当加调整一些数值:
.domain([d3.min(dataset, (d) => d[1]) - 20, d3.max(dataset, (d) => d[1])] + 20)
比例尺应用与设置
比例尺就像操作函数一样,将 x
和 y
的原数据值变为适应 SVG 并可在 SVG 上正确渲染的值。
比例尺函数为 circle 图形设置坐标属性值:
.attr('cx', d => xScale(d[0]))
.attr('cy', d => yScale(d[1]))
.attr('r', 5)
比例尺函数为 rect 图形设置坐标值:
.attr('x', (d) => xScale(d[0]))
.attr('y', (d) => yScale(d[1]))
rect 图形设置 height 高度时就把总高度减去 y 比例尺位置:
.attr('height', (d) => h - padding - yScale(d[1]))
其他比例尺
支持多种比例尺,可到文档查看,按需选用
时间比例尺 scaleTime
https://d3js.org/d3-scale/time
通过 scaleTime 创建时间比例尺,类似于线性缩放比例尺,domain 设置为时间。
const xScale = d3
.scaleTime()
.domain([d3.min(dataset, (d) => new Date(d[0])), d3.max(dataset, (d) => new Date(d[0]))])
.range([padding, w - padding]);
刻度比例尺 scaleBand
https://d3js.org/d3-scale/band
指定均分的刻度比例尺,比如创建 12 个月的刻度:
const yScale = d3
.scaleBand()
.domain(Array.from({ length: 12 }, (_, i) => i))
.range([h - padding, padding]);
如所有年份需要作为刻度:
const xScale = d3
.scaleBand()
.domain(dataset.map((d) => d.year))
.range([padding, w - padding]);
通过 bandwidth 可以获取一个刻度对应的长度:
.attr('width', (d) => xScale.bandwidth(d.year))
.attr('height', (d) => yScale.bandwidth(d.month))
坐标轴
可将比例尺转换为可阅读的坐标轴,适用于大多比例尺。
将比例尺渲染为坐标轴
- 分别通过
axisLeft()
和axisBottom()
方法渲染 x 和 y 坐标轴。 - 可以使用
g
元素(英文中组 group的缩写)。 比例尺只是一条直线,是一个简单的图形,所以可以用g
。 - 使用
transform
属性将比例尺放置在 SVG 的正确位置上,否则会沿着 SVG 的边缘渲染,不可见 -
transform
属性使用translate
,根据给定的值移动整组 - x 比例尺作为参数被传递给
call()
方法
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);;
svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(" + (padding) + ", 0)")
.call(yAxis);
坐标轴格式化
可设置 tickFormat 传入函数,使坐标轴按特定形式格式化。例如,使用逗号分割千位:
axis.tickFormat(d3.format(",.0f"));
d3.format 指定格式化函数 https://d3js.org/d3-format
const f = d3.format(".1f");
for (let i = 0; i < 10; ++i) {
console.log(f(0.1 * i));
}
也可配合 d3.timeFormat 时间格式化 https://d3js.org/d3-time-format
axis.tickFormat(d3.timeFormat("%b %d"));
d3.format 和 d3.timeFormat 创建的函数也可以被直接调用,所以 tickFormat 也可以直接写函数。
function formatMinutes(seconds) {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
axis.tickFormat(formatMinutes);
坐标点过滤
如使用刻度比例尺时,所有年份需要作为刻度,这样刻度太多了,根本看不清。此时可以使用 tickValues 来只选取 10 的倍数作为刻度。
刻度比例尺的 domain 可以拿到所有值。
const xAxis = d3.axisBottom(xScale).tickValues(xScale.domain().filter((y) => y % 10 === 0));
事件与交互
事件监听
https://d3js.org/d3-selection/events
选择的元素可通过 on 监听事件,第一个返回元素是鼠标事件,第二个是当前触发事件元素的数据。
交互效果
如渲染 rect 时,监听 mouseover 和 mouseout 让提示渲染及隐藏:
svg
.selectAll('rect')
.data(data)
.enter()
.append('rect')
// ... 此处省略 x y width height fill 等属性设置
.on('mouseover', (e, d) => {
d3.select('#tooltip')
.style('display', 'block')
.style('left', xScale(new Date(d[0])) + 'px')
.style('bottom', padding + 20 + 'px')
.attr('data-date', formatDate(new Date(d[0])))
.text(formatDate(new Date(d[0])) + '\n' + d[1]);
})
.on('mouseout', () => {
d3.select('#tooltip').style('display', 'none');
});
基础图形绘图思路
先定好 svg 的宽高、padding等要素,选中 svg 对象并设置。
柱状图
1、拿到数据,定义 x 和 y 比例尺,渲染创建 x 和 y 坐标轴
2、绘制矩形柱子: svg 对象选中 rect 对象,应用数据,追加 rect 元素
3、rect 元素根据 x 和 y 比例尺渲染 x 和 y 的位置,渲染固定的宽度,渲染高度为总高度减去 y 比例尺位置
散点图
1、拿到数据,定义 x 和 y 比例尺,渲染创建 x 和 y 坐标轴
2、绘制圆形散点: svg 对象选中 circle 对象,应用数据,追加 circle 元素
3、circle 元素根据 x 和 y 比例尺渲染 cx 和 cy 位置,渲染固定的半径
热度图
1、定义 x y 两个维度的刻度比例尺,渲染创建 x 和 y 坐标轴
2、定义 value 不同范围的不同颜色
3、将 rect 元素放到 x y 轴对应的位置,宽高通过刻度比例尺获取每个刻度的距离,将填充颜色设置为对应颜色
绘制地理路径
地理路径生成器,geoPath,接收一个给定的 GeoJSON 几何体或要素对象,并生成 SVG 路径数据字符串。
geoPath 创建地理路径生成器
geoPath,使用默认设置创建新的地理路径生成器。
const path = d3.geoPath(projection); // for SVG
参数 projection ,默认为 null 。可指定与投影一起使用,通常是 D3 内置的 地理投影。
TopoJSON 与 GeoJSON
GeoJSON 是一种对各种地理数据结构进行编码的格式,基于JavaScript对象表示法(JSON)的地理空间信息数据交换格式。许多地图可视化库和框架(如Leaflet和D3.js)都支持直接使用GeoJSON作为地图数据输入
GeoJSON支持多种几何类型,包括点(Point)、线(LineString)、多边形(Polygon)、多点(MultiPoint)、多线(MultiLineString)、多多边形(MultiPolygon)和几何集合(GeometryCollection)。
TopoJSON 库由 D3 作者编写,是一种对地理拓扑进行编码的方式,是对 GeoJSON 的扩展。
npm i topojson-client
可以使用 TopoJSON 库在 TopoJSON 和 GeoJSON 间转化。
import * as topojson from 'topojson-client'
topojson.feature(topology, object)
用来返回 GeoJSON 的 Feature 结构对象数组,topology 参数是一个 TopoJSON 对象,object 参数是 topology 参数中想要返回的对象。
const countiesFeature = topojson.feature(counties, counties.objects.counties).features
topojson.mesh(topology[, object[, filter]])
用来返回相连的几何体的边,是多线(MultiLineString)的形式。可想象它返回一张“网(mesh)”,共用边只保留一份,方便对整个网进行操作,如上色等。
const countiesMesh = topojson.mesh(counties, counties.objects.states, (a, b) => a !== b);
渲染 path
要显示多个要素,请将它们组合成一个要素集合,文档示例为:
svg.append("path")
.datum({type: "FeatureCollection", features: features})
.attr("d", d3.geoPath());
以上 topojson.mesh
方法返回的 MultiLineString 正是 GeoJSON 要素集合,通过 datum 传入渲染:
svg.append('path').datum(countiesMesh).attr('d', path);
或者使用多个路径元素,文档示例为:
svg.selectAll()
.data(features)
.join("path")
.attr("d", d3.geoPath());
以上 topojson.feature
返回的 features 正适合这种方式来渲染:
svg.selectAll().data(countiesFeature).join('path').attr('d', path).attr('class', 'county');
树形图
树形图通过递归地将区域划分为矩形,根据每个节点的相关值进行划分。
https://d3js.org/d3-hierarchy/treemap
创建树形图布局
首先是配置 d3.treemap
创建新的树状图布局:
// 创建树形图布局
const treemap = d3.treemap();
treemap.size([w, h]);
treemap.paddingInner(1);
生成根节点
然后便是调用 d3.hierarchy
从树形结构的原生 js 对象,生成 treemap 使用的分层结构根节点 root。支持排序、求和等操作。
// 对数据排序和求和
const root = d3.hierarchy(data);
root.sum((d) => d.value);
root.sort((a, b) => b.height - a.height || b.value - a.value);
计算树形图布局
然后就是把根节点 root 传到树状图布局,进行计算。
// 计算树形图布局
treemap(root);
以上计算后,会在 root 及其后代上分配以下属性:
- node.x0 - 矩形的左边缘
- node.y0 - 矩形的上边缘
- node.x1 - 矩形的右边缘
- node.y1 - 矩形的底边
渲染叶子节点
通过 root.leaves()
获取所有叶子节点。可以对它的所有叶子节点进行渲染,渲染的宽高和位置通过以上属性计算。
// 获取所有叶子节点
const leaves = root.leaves();
// 渲染叶子节点
svg
.selectAll('rect')
.data(leaves)
.enter()
.append('rect')
.attr('x', (d) => d.x0)
.attr('y', (d) => d.y0)
.attr('width', (d) => d.x1 - d.x0)
.attr('height', (d) => d.y1 - d.y0)
.attr('fill', 'red')