d3.js 与 react.js

本文简单总结一下 d3(v4) 在 react 中如何使用,如果你还不知道 d3.js 是什么,请移步 d3.js
d3.js 官方样例中的用法都是类似

1
2
3
4
5
d3.select("body")
.selectAll("p")
.data([4, 8, 15, 16, 23, 42])
.enter().append("p")
.text(function(d) { return "I’m number " + d + "!"; });

确实很简明,但是在 react 中却没法下手,所以在 react 中,只需将 d3.js 视为提供图形算法的库即可,不需要掌握如何使用 d3.js 操作 dom。

使用 svg 画一个 bar 图

我们先使用 svg 来画一个最简单的没有坐标轴的 bar 图,理解一下 d3 与 react 的分工。

首先假设我们的 bar 图大小为 1280(px) * 800(px),数据是

1
2
3
4
5
6
const data = [
{ city: '北京', amount: 1000, },
{ city: '上海', amount: 803, },
{ city: '广州', amount: 440, },
{ city: '深圳', amount: 780, },
];

然后我们需要理解 bar 图是如何构成(画)的,bar 图有两个坐标轴:

  • 横轴为分组,对应 city ,决定每根柱子的 x 位置
  • 竖轴为数值,对应 amount,决定每根柱子的高度即 y 的位置

d3 处理图形的算法和坐标计算很方便,使用 d3.scaleBand 将 city 与图的宽度(1280px) 做出映射关系:

1
2
3
const xAxis = d3.scaleBand()
.range([0, 1280])
.domain(['北京', '上海', '广州', '深圳'])

就可以用 xAxis('上海') 得到上海这根柱子的 x 位置。每根柱子的宽度使用 xAxis.bandwidth() 获得。

使用 d3.scaleLinear 将 amount 与图的高度(800px) 做出映射关系:

1
2
3
const yAxis = d3.scaleLinear()
.range([800, 0])
.domain([0, 1000])

就可以用 yAxis(803) 得到上海这根柱子的高度 y 位置。请注意 range([800, 0]),之所以是反向的,是因为 svg 的纵坐标系是反向的,而且 canvas 的纵坐标系也是反向的。

接下来创建我们的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import * as d3 from 'd3';
import React, { PureComponent } from 'react';

const width = 1280;
const height = 800;

const data = [
{ city: '北京', amount: 1000, },
{ city: '上海', amount: 803, },
{ city: '广州', amount: 440, },
{ city: '深圳', amount: 780, },
];

const xAxis = d3.scaleBand()
.range([0, 1280])
.domain(['北京', '上海', '广州', '深圳']);
const yAxis = d3.scaleLinear()
.range([800, 0])
.domain([0, 1000]);

const barWidth = xAxis.bandwidth();

class Bar extends PureComponent {

render() {
return <svg width={width} height={height}>
{data.map(({ city, amount }, index) => {
const x = xAxis(city);
const y = yAxis(amount);
const barHeight = height - y;

return <rect
key={index}
x={x}
y={y}
width={barWidth}
height={barHeight}
fill={'#bada55'} />;
})}
</svg>;
}

}

这样就完成了一个最简单的(正数) bar 图,需要注意的是其中计算 barHeight 的时候并没有判断 amount 为负数的情况,且没有处理各种边界错误。

性能

上边的例子中我们是直接 map 数据,创建了 n 个 rect 元素,当数据量上千后,性能会下降,不如改为 canvas 画法,使用 canvas 画的逻辑也是一样的,d3.js 只负责图形算法和坐标计算。

不同的是 canvas 无法像 svg 一样直接在 render 函数中画,需要在 componentDidMount 之后执行画图的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Bar extends PureComponent {

render() {
return <canvas
width={width}
height={height}
ref={r => this.canvas = r} />;
}

componentDidMount() {
const ctx = this.canvas.getContext('2d');
ctx.fillStyle = '#bada55';

data.map(({ city, amount }, index) => {
const x = xAxis(city);
const y = yAxis(amount);
const barHeight = height - y;

ctx.beginPath();
ctx.fillRect(x, y, barWidth, barHeight);
ctx.closePath();
});
}

}

总结

通过上边两个例子,相信你已经可以使用 svgcanvas 通过 d3.jsreact 中画出常用的基本可视化图形了。

在项目中,无论是你是要用 d3 自己画图,还是用 echarts/g2/highcharts,重要的是理解图形是如何组成的,如何画出来的,方法有很多。

avatar

Thomas Chan

年轻就是暴躁,年轻就是不安分,年轻就是发脾气