【摘要】 用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'; //02BAD4context.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的坐标值。

本文示例中采用的基本算法为(为复现绘制过程,直接采用面向过程的编程方式):

  1. 绘制x轴文字时记录相对于可视坐标系的坐标值,并存储于options.xAxisPos数组中。

  2. 由于数据点是对齐x轴文字来绘制的,所以options.xAxisPosoptions.data中存储的坐标对就是数据点在可视坐标中的坐标点。

  3. 遍历数据坐标点,计算使用三次贝塞尔曲线连接相邻点时的控制点的坐标,此时控制点坐标是相对于可视坐标系的,再经过坐标变换函数transToCanvasCoord( )处理将坐标数值转换为相对于canvas坐标系的数值。

  4. 使用context.bezierCurveTo(c1x, c1y, c2x, c2y, dx dy)函数来绘制拟合曲线。

示例代码为:

/**
* 三次贝塞尔曲线数据拟合
*/
function drawDataWithCubicBezier(options) {//计算用于绘图的数据点和控制点坐标let drawingPoints = calcControlPoints(options);//设置绘图样式context.strokeStyle = options.barStyle.color || '#1abc9c'; //02BAD4context.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:

  1. 在实际开发中,反复出现的计算结果可以通过闭包的形式缓存下来,例如本例中transToCanvasCoord( )函数中前半部分的计算实际上每次进行坐标转换时都会计算,这是没必要的。

  2. 上例中的算法在计算控制点时是以当前点x[i]计算连接x[i]x[i+1]时的控制点坐标并进行保存,而绘图时当循环变量为i时,drawingPoints[i]中存储的控制点坐标,是连接至(x[ i+1 ],y[ i+1 ])时的控制点,所以取用参数时需要错一位。当然也可以在计算drawingPoints时直接按需存储即可。

在浏览器中可以看到曲线拟合的绘制效果:

四. 大数据量场景

面对大数据量的可视化展现或是在交互后出现重绘时,就极容易造成主线程阻塞,这是需要极力避免的。常见的处理思路有以下几种:

  1. 数据采样并重新拟合以减少绘图数据点,也就是从源数据到绘图数据进行映射,毕竟显示器分辨率就那么高,过大的数据量加重了数据损失,却并不一定能在视觉和效果上获得对应的提升。

  2. 将大数据量及耗时的处理发送至webWorker中,利用工作线程来处理计算密集型任务。

  3. 将同步的绘图任务分解为若干个异步的子任务来执行,避免阻塞主线程。

笔者阅历有限,并没有生产环境的大数据量绘制的性能优化实战经验,能想到的就是上面几点,非常欢迎有相关经验的读者交流讨论。

demo.rar

来源:华为云社区  作者:大史不说话

带着canvas去流浪系列之二 绘制折线图相关推荐

  1. 带着canvas去流浪系列之一:绘制柱状图

    [摘要] 学习使用canvasAPI来实现数据可视化. 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 任务说明 使用原生canvasAPI绘制柱 ...

  2. 带着canvas去流浪系列之七 绘制水球图

    [摘要] 用原生canvasAPI实现百度echarts 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 任务说明 使用原生canvasAPI绘制 ...

  3. 带着canvas去流浪系列之五 绘制K线图

    [摘要] 用canvas原生API实现百度Echarts 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 任务说明 使用原生canvasAPI绘制 ...

  4. 带着canvas去流浪系列之四 绘制散点图

    [摘要] 用原生canvasAPI实现百度Echarts图表 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 任务说明 使用原生canvasAPI ...

  5. 带着canvas去流浪系列之六 绘制雷达图

    [摘要] 用canvas原生API实现百度Echarts基本图表. 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 任务说明 使用原生canvas ...

  6. 带着canvas去流浪系列之八 碰撞

    [摘要] canvas动画-碰撞仿真 示例代码托管在:http://www.github.com/dashnowords/blogs 经过前面章节相对枯燥的练习,相信你已经能够上手canvas的原生A ...

  7. 带着canvas去流浪系列之九 粒子动画

    [摘要] canvas实现粒子动画 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 粒子特效 粒子特效一般指密集点阵效果,它并不是canvas独有 ...

  8. 带着canvas去流浪系列之三 绘制饼图

    一. 任务说明 使用原生canvasAPI绘制饼图(南丁格尔玫瑰). 二. 重点提示 南丁格尔玫瑰图的画法有很多种,Echarts中提供的以半径或面积两种不同模式,本文中以面积比例画法为例,绘制算法如 ...

  9. 【带着canvas去流浪(15)】threejs fundamentals翻译系列1-scene graph

    示例代码托管在:http://www.github.com/dashnowords/blogs 博客园地址:<大史住在大前端>原创博文目录 华为云社区地址:[你要的前端打怪升级指南] 目录 ...

最新文章

  1. 《Mysql数据库及应用》_MySQL数据库及应用
  2. SharePoint 2010设置问卷调查权限
  3. 自定义流操作符 重载wcout
  4. (二)HTTP初相识--了解HTTP协议
  5. 调用postman拿CSRF token的隐藏Cookie处理
  6. linux之ps命令详解
  7. leetcode —— 17. 电话号码的字母组合
  8. SQL数据库调优之性能监视篇
  9. python3 常见命令
  10. java开发的程序怎么用_java安装后怎么使用?第一次编写java程序
  11. 安全控件开发原理分析 支付宝安全控件开发 网银密码控件 C++
  12. js中鼠标事件mouseover、mouseenter和mouseleave、mouseout的区别
  13. C#读取Excel文件(*.xls)|*.xls(2种方法)
  14. java毕业设计——基于java+JDBC+sqlserver的POS积分管理系统设计与实现(毕业论文+程序源码)——POS积分管理系统
  15. Calibre发邮件至Kindle
  16. CRM系统部署模式有哪些
  17. partitionBy()的解释
  18. 【一】JAVAScript 学习笔记:如何用javascript输出helloworld
  19. 山楂(牛客月赛45 )
  20. Hadoop Shell命令-hdfs常用命令

热门文章

  1. php静态属性和普通属性吗,php中静态属性和普通属性的区别
  2. 10kv电压互感器型号_《装表接电》知识点16:设备型号
  3. 海尔计算机类,海尔计算机类笔题
  4. 【Linux开发】linux设备驱动归纳总结(八):3.设备管理的分层与面向对象思想...
  5. leetcode 198 python
  6. 华科计算机复试考什么,华科计算机复试的机试
  7. easyexcel导入时读不到数据_easyexcel读取任意表格以及使用中的坑
  8. 广度优先搜索——填涂颜色(洛谷 P1162)
  9. c语言过磅系统,为什么要用无人值守_自动过磅系统?
  10. 两个局域网如何互联_如何申请使用三大航司的机上wifi服务?