带着canvas去流浪系列之二 绘制折线图
【摘要】 用canvasAPI实现echarts简易图表
示例代码托管在:http://www.github.com/dashnowords/blogs
一. 任务说明
使用原生canvasAPI
绘制折线图。(柱状图截图来自于百度Echarts官方示例库【查看示例链接】。
二. 重点提示
一般折线图是比较好实现的,只需要调用最基本的moveTo()
和lineTo( )
方法来绘制即可。平滑折线图是一个难点,需要借助贝塞尔曲线来进行绘制,此时每段曲线的控制点算法就成了核心难点,对原理感兴趣的读者可以自行研究,本文直接利用算法的结论来进行实现。
上一节中为了以文字中点为参考,在绘制x轴文字时采用的方法是用measureText( )
方法测量文字的宽度,然后偏移该距离的一半来达到效果,事实上我们可以通过设置textAlign
属性为'center'来达到以文字宽度方向中线为参考点的绘制。
context.textAlign = 'center';
context.drawText('Hello world',x ,y);
三. 示例代码
坐标轴及绘图参数设置请直接参见【带着canvas去流浪】(1)绘制柱状图或在示例demo中查看。
3.1 一般折线图
折线图数据绘制示例代码:
/**
* 绘制数据
*/
function drawData(options) {
let data = options.data;//数据点坐标
let xLength = (options.chartZone[2] - options.chartZone[0])*0.96;//线段尾部留白后x轴长
let yLength = (options.chartZone[3] - options.chartZone[1])*0.98;//线段尾部留白后y轴长
let gap = xLength / options.xAxisLabel.length;//x轴间隙
//缓存从数据值到坐标距离的比例因子
let yFactor =(options.chartZone[3] - options.chartZone[1]) *0.98 / options.yMax
let activeX = 0;//记录绘制过程中当前点的坐标
let activeY = 0;//记录绘制过程中当前点的y坐标
context.strokeStyle = options.barStyle.color || '#1abc9c'; //02BAD4
context.strokeWidth = 2;
context.beginPath();
context.moveTo(options.chartZone[0],options.chartZone[3]);//先将起点移动至0,0坐标
for(let i = 0; i < data.length; i++){
activeX = options.chartZone[0] + (i + 1) * gap;
activeY = options.chartZone[3] - data[i] * yFactor;
context.lineTo(activeX, activeY);
}
context.stroke();
}
浏览器中可查看效果:
3.2 用贝塞尔曲线绘制平滑折线图
一般折线图连接点部分非常生硬,更多的场景下我们更希望曲线相对平滑,这时候就需要用到贝塞尔曲线来进行绘制,关于控制点的确定可参考文章【怎样确定贝塞尔曲线的控制点】。
关于Canvas图形绘制中坐标系的一点提示
为了将参数集中,options对象中记录的数据坐标是相对于我们自己绘制的坐标系的,为了使用canvas绘图上下文中的贝塞尔曲线绘制函数,需要在绘制时将数据点的坐标值转换为相对于canvas的坐标值。
本文示例中采用的基本算法为(为复现绘制过程,直接采用面向过程的编程方式):
绘制x轴文字时记录相对于可视坐标系的坐标值,并存储于
options.xAxisPos
数组中。由于数据点是对齐x轴文字来绘制的,所以
options.xAxisPos
及options.data
中存储的坐标对就是数据点在可视坐标中的坐标点。遍历数据坐标点,计算使用三次贝塞尔曲线连接相邻点时的控制点的坐标,此时控制点坐标是相对于可视坐标系的,再经过坐标变换函数
transToCanvasCoord( )
处理将坐标数值转换为相对于canvas坐标系的数值。使用
context.bezierCurveTo(c1x, c1y, c2x, c2y, dx dy)
函数来绘制拟合曲线。
示例代码为:
/**
* 三次贝塞尔曲线数据拟合
*/
function drawDataWithCubicBezier(options) {
//计算用于绘图的数据点和控制点坐标
let drawingPoints = calcControlPoints(options);
//设置绘图样式
context.strokeStyle = options.barStyle.color || '#1abc9c'; //02BAD4
context.strokeWidth = 4;
context.beginPath();
context.moveTo(options.chartZone[0],options.chartZone[3]);//先将起点移动至0,0坐标
//逐个连接相邻坐标点
for(let i = 1; i < drawingPoints.length; i++){
context.bezierCurveTo(drawingPoints[i-1].cp1x, drawingPoints[i-1].cp1y, drawingPoints[i-1].cp2x, drawingPoints[i-1].cp2y, drawingPoints[i].dx, drawingPoints[i].dy);
}
//绘制线条
context.stroke();
}
/**
* 计算控制点
* 本例采用的算法,在每个点计算时需要用到该点左侧1个点和右侧2个点的坐标信息,影响边界点的绘制,本例中采用的方法为直接复制边界点坐标来简化边界点的坐标求值。
*/
function calcControlPoints(options) {
let results = [];
let y = options.data;
let x = options.xAxisPos;
//补充左值
y.unshift(y[0]);
x.unshift(0);
//补充右值
x.push(x[y.length - 1]);
x.push(x[y.length - 1]);
y.push(y[y.length - 1]);
y.push(y[y.length - 1]);
//计算用于绘制曲线的坐标点及控制点坐标值
for(let i = 1; i < y.length - 2; i++){
results.push({
dx:transToCanvasCoord(x[i], 'x'),
dy:transToCanvasCoord(y[i]),
cp1x:transToCanvasCoord(x[i] + (x[i+1] - x[i-1]) / 4,'x'),
cp1y:transToCanvasCoord(y[i] + (y[i+1] - y[i-1]) / 4),
cp2x:transToCanvasCoord(x[i+1] - (x[i+2] - x[i]) / 4,'x'),
cp2y:transToCanvasCoord(y[i+1] - (y[i+2] - y[i]) / 4),
})
}
console.log(results)
return results;
}
/**
* 将坐标转换为相对canvas的坐标
* @param {[type]} coord 相对于可视坐标系的值
* @param {[type]} flag 标记转换x坐标还是y坐标
*/
function transToCanvasCoord(coord,flag) {
let xLength = (options.chartZone[2] - options.chartZone[0])*0.96;
let yLength = (options.chartZone[3] - options.chartZone[1])*0.98;
let yFactor =(options.chartZone[3] - options.chartZone[1]) *0.98 / options.yMax;
if (flag === 'x') {
return coord + options.chartZone[0];
}
return options.chartZone[3] - coord * yFactor;
}
Tips:
在实际开发中,反复出现的计算结果可以通过闭包的形式缓存下来,例如本例中
transToCanvasCoord( )
函数中前半部分的计算实际上每次进行坐标转换时都会计算,这是没必要的。上例中的算法在计算控制点时是以当前点
x[i]
计算连接x[i]
到x[i+1]
时的控制点坐标并进行保存,而绘图时当循环变量为i
时,drawingPoints[i]
中存储的控制点坐标,是连接至(x[ i+1 ],y[ i+1 ])
时的控制点,所以取用参数时需要错一位。当然也可以在计算drawingPoints
时直接按需存储即可。
在浏览器中可以看到曲线拟合的绘制效果:
四. 大数据量场景
面对大数据量的可视化展现或是在交互后出现重绘时,就极容易造成主线程阻塞,这是需要极力避免的。常见的处理思路有以下几种:
数据采样并重新拟合以减少绘图数据点,也就是从源数据到绘图数据进行映射,毕竟显示器分辨率就那么高,过大的数据量加重了数据损失,却并不一定能在视觉和效果上获得对应的提升。
将大数据量及耗时的处理发送至
webWorker
中,利用工作线程来处理计算密集型任务。将同步的绘图任务分解为若干个异步的子任务来执行,避免阻塞主线程。
笔者阅历有限,并没有生产环境的大数据量绘制的性能优化实战经验,能想到的就是上面几点,非常欢迎有相关经验的读者交流讨论。
来源:华为云社区 作者:大史不说话
带着canvas去流浪系列之二 绘制折线图的更多相关文章
- 带着canvas去流浪系列之一:绘制柱状图
[摘要] 学习使用canvasAPI来实现数据可视化. 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 任务说明 使用原生canvasAPI绘制柱 ...
- 带着canvas去流浪系列之五 绘制K线图
[摘要] 用canvas原生API实现百度Echarts 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 任务说明 使用原生canvasAPI绘制 ...
- 带着canvas去流浪系列之四 绘制散点图
[摘要] 用原生canvasAPI实现百度Echarts图表 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 任务说明 使用原生canvasAPI ...
- 带着canvas去流浪系列之七 绘制水球图
[摘要] 用原生canvasAPI实现百度echarts 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 任务说明 使用原生canvasAPI绘制 ...
- 带着canvas去流浪系列之六 绘制雷达图
[摘要] 用canvas原生API实现百度Echarts基本图表. 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 任务说明 使用原生canvas ...
- 带着canvas去流浪系列之九 粒子动画【华为云技术分享】
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/devcloud/article/detai ...
- 带着canvas去流浪系列之三 绘制饼图
[摘要] 用canvas原生API绘制Echarts图表 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 任务说明 使用原生canvasAPI绘制 ...
- 带着canvas去流浪系列之九 粒子动画
[摘要] canvas实现粒子动画 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 粒子特效 粒子特效一般指密集点阵效果,它并不是canvas独有 ...
- 带着canvas去流浪系列之八 碰撞
[摘要] canvas动画-碰撞仿真 示例代码托管在:http://www.github.com/dashnowords/blogs 经过前面章节相对枯燥的练习,相信你已经能够上手canvas的原生A ...
随机推荐
- Codeforces 1178G. The Awesomest Vertex
传送门 首先通过dfs序把子树操作转化为区间操作,求最大值可以用斜率优化. 然后分个块,对每个块维护个凸包.修改时中间的打个标记,边角暴力重构:询问时中间的用斜率优化的方法求,边角的暴力求. 由于此题 ...
- CSPS模拟 71
全程傻眼 T1 毛衣衬 meet_in_middle.. 不再使用二分查找,而是直接枚举对面状态,虽然底数爆炸但是指数减半,复杂度是对的. T2 猫儿嗔 逆序关系有支配关系? $DAG$树.. 把逆序 ...
- BigInt 的使用!
今天学长讲的卡特兰数真的是卡的一批,整个全是高精的题,这时我就使用重载运算符,然后一下午就过去了 首先来看一波水题(也就卡了2小时) . A. 网格 内存限制:512 MiB 时间限制:1000 ms ...
- NLP-BM25算法理解
前两天老师给我们讲解了BM25算法,其中包括由来解释,以及算法推导,这里我再将其整理,这里我不讲解之前的BIM模型,大家有兴趣可以自行了解.Okapi BM25:一个非二值的模型bm25 是一种用来评 ...
- PHP微信授权登录用于多个域名的方法
PHP微信授权登录用于多个域名的方法appid和 回调地址换下就好了 <pre><!DOCTYPE html><html lang="en">& ...
- HTML——基础知识点1
- Linux下RIAD的实现及mdadm命令的基本用法
一.RAID简述 磁盘阵列(Redundant Arrays of Independent Disks,RAID),是把多个物理磁盘组成一个阵列,当作一个逻辑磁盘使用,它将数据以分段或条带的方式储存在 ...
- 深入理解计算机系统 第十章 系统级I/O
很多高级语言都提供了执行 I/O 的较高级别的函数.为什么我们还要学习 Unix I/O? 原因:1.由于 I/O 和其他系统概念之间有循环依赖关系,故了解 Unix I/O 将帮助我们理解其他的系统 ...
- Groovy单元测试框架spock基础功能Demo
spock是一款全能型的单元测试框架. 最近在做单元测试框架的调研和尝试,目前确定的方案框架包括是:spock,Junit,Mockito以及powermock.由于本身使用Groovy的原因,比较钟 ...
- Ubuntu18.04.3 LTS初体验
安装系统 想来虚拟机安装太慢,正好有一台旧电脑,干脆整个乌班图系统. 启动盘工具:UltraISO,老牌工具了. 上官网下载ISO镜像文件: https://cn.ubuntu.com/downloa ...