Canvas 图表制作实战

Posted 前端深夜告解室

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Canvas 图表制作实战相关的知识,希望对你有一定的参考价值。

本文将一步步开始,绘制一张 canvas 图表,为了文章演示,我拜访了联合国(https://www.un.org/),在那儿,我拿到了一份数据:自 1950 年至 2015 年以来的人口年龄分布总数的数据。

表格以每五年一个区间,展示各个年龄阶段(5岁一个区间)的总人口数。 我们拿到这个数据要做什么呢?我想看看,随着时间的推进,人类是否真的像自己所说的那样,我们正在慢慢变老(老龄化)。

我们将基于这份数据,绘制出一张图表

准备工作

数据处理

当我们拿到这张 excel 表格的时候,除了一堆数字,什么也看不出来。由于我想了解人口老龄化的问题,所以,我只想知道每个时期各个年龄段所占总数的比例,而不是具体人数。我们需要对数据进行一些处理:

处理思路是利用 node 的 fs 模块,读取 csv 文件为字符串,通过 split,转换为数组,再进行一系列 mapreduce 进行处理,细节不是重点,不再展示。

处理后:

{
"0-4": {
"1950": "0.13341889187883807",
"1955": "0.1463118492859392",
// ...
},
"5-9": {
"1950": "0.10640407684497935",
// ...
},
// ...
}

其中每一项的值代表该年龄段在当年所占人口总数比。

框架

为了方便开发调试,我简单起了一个 vue 的脚手架,利用 vue-cli 配合 canvas 来快速地进行开发的同时方便调试。

mock

框架根目录下新建一个 mock 文件夹,将处理好的数据放置在 mock 文件夹下,并在 build 文件夹中的 dev-server.js 中添加 ajax 模拟请求 mock 数据中间件。

正式开始

基本元素

前期准备完成以后,我们新建一个组件,创建一个基本画布:

<template>
<canvas ref="canvas" width="1300" height="700"></canvas>
</template>

此时画的宽高为 1300*700,(代码中,很多数据都是基于这个数值),但是空白的,我们需要在画布中添加坐标系:

  • 首先,我们在生命周期 mounted 阶段通过 ajax 获取图表数据,并进行格式化为二维数组的处理,方便数据的读取

import axios from 'axios'
export default {
data () {
return {
data: [],
cellX: 0,
title: []
}
},
mounted () {
axios.get('/api/bezier').then((res) => {
// 响应值转换为二维数组,方便处理
const data = Object.values(res.data).map(it => Object.values(it))
// 保存数据名
this.title = Object.keys(res.data)
})
}
}
  • 然后我们获取 x 轴的单位长度,将数据转化为坐标点,保存在 data 中。

  mounted () {
axios.get('/api/bezier').then((res) => {
// 响应值转换为二维数组,方便处理
const data = Object.values(res.data).map(it => Object.values(it))
// 获取 x 轴单位长度
this.cellX = Math.floor(1300 / data.reduce((memo, curr) => memo > curr.length ? memo : curr.length, 0))
// 将数据转化为坐标点,赋值
this.data = data.map(item => item.map((it, i) => [i * this.cellX + this.cellX, 650 - (650 * it * 100 / 15)]))
})
}
  • 最后,定义绘出坐标轴的方法,画出 x 轴和 y 轴。

  methods: {
drawPoint (x, y, r) {
const ctx = this.$refs.canvas.getContext('2d')
ctx.beginPath()
ctx.arc(x, y, r, 0, Math.PI * 2, true)
ctx.stroke()
ctx.fill()
},
yAxis (count) {
const ctx = this.$refs.canvas.getContext('2d')
const xPoint = this.cellX / 2
ctx.beginPath()
ctx.lineTo(xPoint, 675)
ctx.lineTo(xPoint, 0)
ctx.stroke()
;(new Array(count)).fill(0).map((_, i) => i * 2.5).forEach(it => {
this.drawPoint(xPoint, 675 - (it * 45), 3)
ctx.fillText(`${it}%`, xPoint - 40, 690 - (it * 45))
})
},
xAxis (count) {
const ctx = this.$refs.canvas.getContext('2d')
ctx.beginPath()
ctx.lineTo(this.cellX / 2, 675)
ctx.lineTo(this.cellX / 2 + 10, 685)
ctx.lineTo(this.cellX / 2 + 20, 665)
ctx.lineTo(this.cellX / 2 + 30, 675)
ctx.lineTo(this.cellX * count, 675)
ctx.stroke()
;(new Array(count)).fill(0).map((_, i) => i * this.cellX + this.cellX).forEach((it, i) => {
this.drawPoint(it, 675, 3)
ctx.fillText(`${i * 5 + 1950}年`, it - 20, 690)
})
}
},
mounted () {
axios.get('/api/bezier').then((res) => {
// ... 省略前面代码
// 画出纵坐标轴
this.yAxis(7)
// 画出横坐标轴
this.xAxis(data[0].length)
})
}

回到浏览器:

Canvas 图表制作实战

将数据放入画布

我们将请求到的数据放入画布中。之前我们为了绘制 x、y 轴,已经定义了绘制点的函数,利用这个函数,将数据 data 绘入画布:

在绘制坐标轴的后面,添加一段代码

  mounted () {
axios.get('/api/bezier').then((res) => {
// ... 省略前面代码
// 绘点
this.data.forEach((item) => item.forEach(it => this.drawPoint(...it, 2)))
})
}

此时回到浏览器,我们发现:

Canvas 图表制作实战

除了感觉像是在下雨,并不能体现什么。 我们有必要加入线条:

添加定义绘制数据线条的函数

  methods: {
// ... 省略前面代码
drawDataLine (data, i) {
const ctx = this.$refs.canvas.getContext('2d')
ctx.beginPath()
data.forEach(it => ctx.lineTo(...it))
// 线条名字
ctx.moveTo(1000, 10 + i * 10)
ctx.lineTo(1100, 10 + i * 10)
ctx.fillText(this.title[i], 1110, 15 + i * 10)
// 区分线条颜色(随机色)
ctx.strokeStyle = `rgb(${Math.floor(Math.random() * 255)},${Math.floor(Math.random() * 255)},${Math.floor(Math.random() * 255)})`
ctx.stroke()
}
}

并在 mounted 内进行绘制

  mounted () {
axios.get('/api/bezier').then((res) => {
// ... 省略前面代码
// 绘线
this.data.forEach((it, i) => this.drawDataLine(it, i))
})
}

回到浏览器:

Canvas 图表制作实战

Wow!图表看似不错。 图表中新生儿的人口占比在不断骤降,而高龄人口比重却在稳步上升,可以说明,随着科技的发展,医疗水平在不断提高,人们的寿命更长了;但伴随着文明的进展,人类越来越不愿意生孩子;另外,社会福利贡献主力军(青壮年)的人口比例却并没有提高,社会福利将面临巨大压力。 还有一个很有意思的地方,二战(<1950)导致新生儿比例过低,在时间往后推演的过程中,这群人一致在人口比例中处于低比例状态。


最后

本文详细描述了一张 canvas 图表的制作过程,但还有不完善的地方。假设图中每个点都是准确的数据,那么线条是否准确呢?例如按照图中说法:1950 年新生儿(0-4岁)占人口总数的 13.34%,1955 年新生儿占人口总数的 14.63%,那么 1952 年年中新生儿应该为中位数:(13.34% + 14.63)/2 = 13.98%。很明显低了,1960 年新生儿占比开始下降,可以推断出在 1950~1955 年期间,新生儿人口占比增长率是趋缓的。1952 年新生儿占比肯定要大于中位数。所以图中线条是不准确的,随着时间的推演,人口占比在图中应该是一条平滑的曲线,而不是折线。还记得《canvas 与贝塞尔曲线》那篇文章后面遗留的问题吗?下一篇文章,我们就针对这个案例,绘制出尽可能准确的图表趋势图。


Canvas 图表制作实战

简历请投递至邮箱

FSP.FE@meituan.com


Canvas 图表制作实战

新美大智能支付终端团队


面向互联网开发从业者;

我们将分享团队技术文章,提供研发岗位招聘信息等。


以上是关于Canvas 图表制作实战的主要内容,如果未能解决你的问题,请参考以下文章

百度数据可视化图表套件echart实战

HTML5 Canvas 支持和 Android Webview

H5 Canvas 绘图

H5使用Canvas绘图

H5使用Canvas绘图

在线报表设计实战系列 – ⑦制作图表类报表