为什么我不再使用 D3.js

这周读到关于 D3.js 的一篇文章,想起 D3.js 当年也是叱咤风云,而现在感觉已经消声灭迹很久了。以下就是我对它的理解与翻译。

D3 在 2011 发布的时候,可谓是一项极大的创新。那时候还是 jQuery 和 Backbone 的天下,浏览器也只是实现了一些简单的 css 标准如 “transitions” 等,像现在更为复杂的 ”flex” 布局还遥遥无期。D3 通过数据驱动来绘制图形的方式解决了这方面的难题,迅速笼络人心。

然而时代终究还是变了。现在的框架采用了虚拟 DOM 等更为灵活和强大的设计理念,CSS 在布局和动画方面也游刃有余。

让我再多给你举几个例子。

D3 的学习曲线

过去几年我一直在使用 D3,并用它绘制了各种各样的图形曲线。然而一个问题就是,虽然我理解关于 D3 的基本概念,但我还是难以做到轻车熟路,我身边的同事跟我也是同样的感受。和大多数人一样,许多时候,我们都是从网上找到一个示例,然后将它修改为实际工程中所需要的。

如果我们想要添加一些新奇的功能,就需要不停的搜索,不停的尝试,不停的修改,直到它看起来是我们想要的了。

听起来是不是很熟悉?现在的开发者们已经非常熟悉虚拟 DOM 和模板编程。掌握这么一个设计理念与思考方式与现在其他库大相径庭的技巧,看起来并没什么卵用。

绘制曲线,它实际上并不难!

如果让你自己从头写一个图表的话,你大概会感到不安和紧张,就好像自己要面临一个非常复杂的问题,但当你开始写的时候一切都变得简单起来。我们先来看一下如何使用 D3 画一副折线图:

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
44
45
46
// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// parse the date / time
var parseTime = d3.timeParse("%d-%b-%y");
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the line
var valueline = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Get the data
d3.csv("data.csv", function(error, data) {
if (error) throw error;
// format the data
data.forEach(function(d) {
d.date = parseTime(d.date);
d.close = +d.close;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.close; })]);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the X Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
svg.append("g")
.call(d3.axisLeft(y));
});

我们再来看一下在 Preact 中如何绘制:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
/* @jsx h */
let { Component, h, render } = preact
function getTicks (count, max) {
return [...Array(count).keys()].map(d => {
return max / (count - 1) * parseInt(d);
});
}
class LineChart extends Component {
render ({ data }) {
let WIDTH = 500;
let HEIGHT = 300;
let TICK_COUNT = 5;
let MAX_X = Math.max(...data.map(d => d.x));
let MAX_Y = Math.max(...data.map(d => d.y));

let x = val => val / MAX_X * WIDTH;
let y = val => HEIGHT - val / MAX_Y * HEIGHT;
let x_ticks = getTicks(TICK_COUNT, MAX_X);
let y_ticks = getTicks(TICK_COUNT, MAX_Y).reverse();

let d = `
M${x(data[0].x)} ${y(data[0].y)}
${data.slice(1).map(d => {
return `L${x(d.x)} ${y(d.y)}`;
}).join(' ')}
`;

return (
<div
class="LineChart"
style={{
width: WIDTH + 'px',
height: HEIGHT + 'px'
}}
>
<svg width={WIDTH} height={HEIGHT}>
<path d={d} />
</svg>
<div class="x-axis">
{x_ticks.map(v => <div data-value={v}/>)}
</div>
<div class="y-axis">
{y_ticks.map(v => <div data-value={v}/>)}
</div>
</div>
);
}
}
let data = [
{x: 0, y: 10},
{x: 10, y: 40},
{x: 20, y: 30},
{x: 30, y: 70},
{x: 40, y: 0}
];
render(<LineChart data={data} />, document.querySelector("#app"))

CSS 代码:

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
44
45
46
47
48
49
50
51
body {
margin: 0;
padding: 0;
font-family: sans-serif;
font-size: 14px;
}
.LineChart {
position: relative;
padding-left: 40px;
padding-bottom: 40px;
}
svg {
fill: none;
stroke: #33C7FF;
display: block;
stroke-width: 2px;
border-left: 1px solid black;
border-bottom: 1px solid black;
}
.x-axis {
position: absolute;
bottom: 0;
height: 40px;
left: 40px;
right: 0;
display: flex;
justify-content: space-between;
}
.y-axis {
position: absolute;
top: 0;
left: 0;
width: 40px;
bottom: 40px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
}
.y-axis > div::after {
margin-right: 4px;
content: attr(data-value);
color: black;
display: inline-block;
}
.x-axis > div::after {
margin-top: 4px;
display: inline-block;
content: attr(data-value);
color: black;
}

看起来有许多的代码,但你应该注意到,我是用自己已经掌握的 Preact 框架(其他框架也一样,React、Vuew、Angular 等等)和 CSS 来绘制的。如果使用 D3 的话,那么首先你要了解一大堆的概念 … 但现在你只需要掌握自己正在使用的库,就能基于它做出更多的修改了。

看看它的打包大小

最坏情况下,D3 可能要引入大概 70+ 字节的代码。如果你仅仅是想调用一行函数,那么引入这么大的一个第三方库真的合适吗?

通常 Canvas 和 HTML 要优于 SVG

不知道你有没有注意到,上述代码我使用了 SVG 来帮助我绘制图形,绘制图形的时候人们总想用大量的 SVG 来完成任务。然而 CSS 已经今非昔比,它的崛起让 SVG 相形见绌。例如,在 SVG 中实现文字环绕的效果需要自己使用 JavaScript 代码动态计算:

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
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null)
.append("tspan")
.attr("x", 0)
.attr("y", y)
.attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan")
.attr("x", 0)
.attr("y", y)
.attr("dy", ++lineNumber * lineHeight + dy + "em")
.text(word);
}
}
});
}

而使用 CSS 仅需要一行 white-space: normal。利用 CSS 的 transformborder-radius 基本上可以绘制任何基本图形。我现在之所以还是用 SVG 的唯一原因就是 <path> 标签,它是创建任意图案的绝佳助手。

如果你想要再进一步提升性能,那么可以选择在 Canvas 中进行绘制,相比于其它两种方式,它能占用更少的内存,更新也更快。

你可能会反驳我说 Canvas 不能像 SVG 那样任意放大和缩小图案,当放大 Canvas 的时候,页面上的图案开始变得模糊不清。这是因为你在放大页面的时候,并没有相应的修改 Canvas 的宽度和高度,以下是这一问题的解决方案:

1
2
3
4
5
6
7
8
9
10
onResize() {
let canvas = this.base.querySelector('canvas');
let ctx = canvas.getContext('2d');
let PIXEL_RATIO = window.devicePixelRatio;
canvas.width = canvas.offsetWidth * PIXEL_RATIO;
canvas.height = canvas.offsetHeight * PIXEL_RATIO;
ctx.setTransform(PIXEL_RATIO, 0, 0, PIXEL_RATIO, 0, 0);

this.props.onDraw(ctx, canvas.offsetWidth, canvas.offsetHeight);
}

总结

如上所示,有种种原因表明 D3 现在已经过时了,自它发布之日到现在,前端已经发生了巨变。如果你只是画一些简单的图标,例如条形图、折线图等,那就先思考一下如何在你正在使用的框架中完成这些任务。而且,在代码维护方面这样也更加便捷。

推荐文章