←首页

图表

当接到类似以上需求时,你的第一想法是不是跟我一样,使用 Canvas 来绘制,啥都不说就开始撸代码。如果你是用 Vue 之类的 MVVM 框架,那意味着你得提供一个结点供 Canvas 着陆,同时让 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
<template>
<canvas ref="chart">
</canvas>
</template>
<script>
/* eslint-disable */
import Chart from 'utils/chart'
export default {
props: {
duration: {
type: Number,
default: 2000
},
data: {
type: Object,
default: []
}
},
watch: {
'data' (val, oldVal) {
this.redraw()
}
},

mounted () {
this.chart = Chart.init(this.$refs.chart)
this.redraw()
},
methods: {
redraw () {
this.chart.draw({
duration: this.duratioin
})
}
}
}
</script>

类似的做法之前写过很多,比如上一篇文章里面绘制六芒星的方式,但是这种做法成本比较大,首先你得从‘头’开始写代码(创建 Canvas,计算所有坐标点,绘制所有可视内容),同时要求你熟练掌握 Canvas API,并且能够在两种不同的开发思想下来回切换代码,总体上成本较高,所以当设计师给我这样的设计稿时,我是拒绝的!(直接放个数字不行吗,搞这么麻烦)

当然这种为自己偷懒而找的理由最终都会被驳回,因为在你身经百战的 Leader 眼里,这些小 Case 都是不经入目的。

“有工作量吗?” —— Leader
“没有没有。” —— 我

需求接都接了,一个字,干!
做是肯定要做的了,那么应对这种需求,有没有更顺滑的方式?尝试下用 SVG 吧。

SVG 是什么就不说了。SVG 很很突出的一个特性是,用文本编辑器打开就能看到源代码,编辑保存就能修改图片!!!

1
2
3
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>

SVG 的属性很多,但还好一眼就能看出什么意思。了解一番之后,设计稿直接切图导出,拿到类似这样的东西:

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
<svg width="622px" height="245px" viewBox="0 0 622 245" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<circle id="path-1" cx="10" cy="16" r="10"></circle>
<filter x="-37.5%" y="-37.5%" width="175.0%" height="175.0%" filterUnits="objectBoundingBox" id="filter-2">
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="2.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
<!--省略-->
</defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(0.000000, -1.000000)">
<g id="曲线" transform="translate(18.000000, 33.000000)">
<!-- 这是一条曲线 -->
<path d="M10.6640625,19.6210938 C39.8348416,119.651983 77.4858832,169.520472 123.617188,169.226563 C231.858912,168.536938 285.925216,10.7569151 351.429688,10.1484375 C405.739364,9.64394951 415.953125,132.3125 464.898438,132.3125 C493.393229,132.3125 531.016927,113.764323 577.769531,76.6679688" stroke="#47E0FF" stroke-width="6" stroke-linecap="round"></path>
<!-- 这是一个点 -->
<g id="Oval-6">
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
<use fill="#47E0FF" fill-rule="evenodd" xlink:href="#path-1"></use>
</g>
<!--省略-->
</g>
</g>
</g>
</svg>

密密麻麻一堆像乱码的东西,但是能看出来,defs 里面定义了一些图形,通过 use 被引用,然后 path 就是那条曲线。

defs 里面我们不需要关心,设计师帮我们画好了。我们关心的是点的位置,曲线的位置,锚点。其他的都可以不用管。

点的位置,可以通过控制 g 元素的 translate 进行偏移也好写。那么线是怎么控制的?path 这里只有一个 d 属性。

1
<path d="M10.6640625,19.6210938 C39.8348416,119.651983 77.4858832,169.520472 123.617188,169.226563 C231.858912,168.536938 285.925216,10.7569151 351.429688,10.1484375 C405.739364,9.64394951 415.953125,132.3125 464.898438,132.3125 C493.393229,132.3125 531.016927,113.764323 577.769531,76.6679688" stroke="#47E0FF" stroke-width="6" stroke-linecap="round"></path>

看代码容易蒙圈,对照下指令表就清晰多了。

SVG Path 指令列表

图片来自SVG 研究之路 (4) - Path 基礎篇

所以 d 属性的值就是一堆指令和点的有机组合。
设计师用画笔绘制曲线的时候也不是一像素一像素绘制的,而是先定一个起点(M),选择点模式(这里用的是二阶贝塞尔曲线 C),选中下一个点,然后确定两个控制点,然后第二个点为起始点,继续描绘。

链接了生成规则之后,通过 Vue 的 computed 自动生成 d,轻而易举。

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
path () {
let steps = []
this.valueArr.forEach((curr, index) => {
if (index === 0) {
// 移动到起点
steps.push('M' + curr.x + ',' + curr.y)
}
if (index !== this.valueArr.length - 1) {
let next = this.valueArr[index + 1]
// 两个控制点坐标
var ctrl1 = {
x: (curr.x + next.x) * 0.5,
y: curr.y
}
var ctrl2 = {
x: ctrl1.x,
y: next.y
}
steps.push('C' + ctrl1.x + ',' + ctrl1.y)
steps.push(ctrl2.x + ',' + ctrl2.y)
steps.push(next.x + ',' + next.y)
}
})
return steps.join(' ')
}

为了让曲线看上去比较均匀,自然,我们选择让两个控制点的 x 值为起始点和结束点的 x 值的中间值,y 值分别还是起始点和结束点的 y 值。

cubic-bezier
通过工具更容易看出来怎么选择控制点的位置 cubic-bezier

代码调整一下,再结合 Tween 来实现渐进动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
doAnimation () {
animation.progress = 0
new TWEEN.Tween(animation)
.delay(1000)
.to({progress: 1}, this.duration, TWEEN.Easing.Quadratic.Out)
.onUpdate(this.onUpdate)
.start()
},
onUpdate () {
this.valueArr.forEach((item) => {
item.y = item.startY + (item.targetY - item.startY) * this.animation.progress
})
}

效果如下:

line-chart-animation

通过 Vue 的数据-视图绑定,我们只需要修改 data 数组的值,和 progress 动画进度,就可以实现图表数据更新和曲线动画了。又一次,我们的关注点回归到我们无比熟悉的数据层,Niceeeeeeeee!

设计稿出自伟大的 Ray.John

codepen地址

如需转载,请注明出处: http://w3ctrain.com / 2017/09/19/vue-svg-chart/

helkyle

我叫周晓楷

我现在是一名前端开发工程师,在编程的路上我还是个菜鸟,w3ctrain 是我用来记录学习和成长的地方。