使用 Vue & SVG 快速绘制曲线图(带动画)

9,217 阅读4分钟

图表

图表

当接到类似以上需求时,你的第一想法是不是跟我一样,使用 Canvas 来绘制,啥都不说就开始撸代码。如果你是用 Vue 之类的 MVVM 框架,那意味着你得提供一个结点供 Canvas 着陆,同时让 Canvas 能够响应数据变动。

写起代码来应该是长这样的:

<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 很很突出的一个特性是,用文本编辑器打开就能看到源代码,编辑保存就能修改图片!!!

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

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

<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 属性。

<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 Path 指令列表
/Users/helkyle/projects/w3ctrain
图片来自SVG 研究之路 (4) - Path 基礎篇

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

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

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
通过工具更容易看出来怎么选择控制点的位置 cubic-bezier

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

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

line-chart-animation

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

设计稿出自伟大的 Ray.John

codepen地址

如需转载,请注明出处: w3ctrain.com/2017/09/19/…