带你快速玩转canvas——写个折线图
需求:
1. 自适应父Dom的宽高,但设置canvas元素的最小宽高,小于最小宽高则设置父Dom带滚动条。
2. 窗口大小变化时,重新绘制折线图,以适应新的大小,保证折线图一直以最好效果展现;
3. x轴的坐标尺度为时间,单位是月份,数据类型是数组,数组元素是字符串,格式类似"2016/12"这种,为了方便,假设x轴固定有12个刻度,初始刻度为x=0的位置;
4. y轴尺度为数字,要求跟随数据的最大值而自动变化,以保证良好的可见性,并且需要绘制参考线;
5. 简单来说,参考excel表格自动生成的折线图而绘制。
分析需求:
1. canvas元素要自适配父Dom,因此要设法获得父Dom的大小,然后调整自身的大小;
2. canvas元素的宽高的设置,需要写在html元素中,而不能通过css设置(否则可能出问题),因此应该通过js显式的写出来(例如通过canvas.width这样的方法);
3. 折线图,首先确定xy坐标轴绘制在画布上的范围(比canvas画布小);
4. 然后绘制xy坐标轴;
5. 再确定表示数据的折线的范围,注意,数据折线的范围应该比xy坐标轴的范围要小,且左下角和xy坐标轴的原点重合,如此方能有更好的体验(否则若比如数据折线的最右边和坐标轴的最右边重合,是会容易带来误解的);
6. x轴的时间刻度为12个刻度,且初始刻度为x=0的位置,因此每个刻度的宽度 = 数据折线的总宽度/11;
7. 假如x轴的时间刻度为可变数量,也不难,每个刻度的宽度 = 数据折线总宽度(可确认) / (x轴刻度总数量 - 1) 即可;8. 比较麻烦的y轴的刻度确认。
9. 首先要确认y轴刻度的最大值,遍历所有数据,取其最大值dataMaxY。
10. 然后将dataMaxY向上取值获得用于计算刻度的最大值MaxY,逻辑是这样的:假如第一个数字是8或者9,,那么取比开头数字大1的数字。例如2开头就是3,6开头就是7,然后后面用0补足位数(以确保和原来的最大数值是同一个量级的)假如第一个数字是1开头,例如13,那么取比其前两位大的偶数,例如13的话取14,14的话取16,18或19则取20;
11. 根据MaxY来确定y轴的刻度,存储刻度的为数组,数组的元素个数就是刻度的数量。
12. 当MaxY小于10时,固定刻度为2/格,最大10
13. 当MaxY为1开头时,刻度为2/格,最大刻度比MaxY要大;
14. 当MaxY为2~5开头时,刻度为5/格,最大刻度比MaxY大;
15. 当MaxY为6~9开头时,刻度为10/格,最大刻度比MaxY大;
16. 根据刻度数组,以及数据折线图的绘制区域,绘制参考线;
17. 至此y轴刻度和参考线完;18. 根据刻度数组的最后一个元素(刻度的最大值),以及数据的值,外加数据折线图区域的坐标,可以绘制出折线图;
19. 绘制折线图时可以顺便写下x轴的刻度(x坐标和数据折线图当前数据坐标相同,y坐标固定);
20. 有必要的话,添加输入验证,如果输入错误,则在绘制区域显示错误文字。
21. 添加各种自定义设置,用于设置文字的样式、颜色,宽度大小等;
分拆需求为函数:
1. 获得并设置canvas标签的最小宽高,然后返回canvas中,绘图区域坐标(指x、y坐标轴的绘图坐标);
2. 绘制x、y坐标轴(不包含刻度);
3. 获得y轴最大尺度;
4. 确定y轴刻度数组;
5. 根据y轴刻度数组,绘制参考线和刻度的数字;
6. 绘制数据折线图和x坐标刻度;
7. 绘图函数,需要重新绘制图时,通过本方法来调用以上方法(本接口暴露出来可被外界调用);
8. 输入检查函数,用于提示错误;
9. 刷新函数,用于在窗口大小变化时主动调用绘图函数重新绘图。
10. 【测试用】自动生成数据的函数;
源代码如下:
<html>
<head><meta charset="UTF-8"><title>用Canvas绘制折线图</title><style>#test {border: 1px solid black;height: 100%;width: 100%;box-sizing: border-box;}</style>
</head>
<body>
<div id="test"></div>
<script>var dom = document.getElementById("test"); //数据源,这里是模拟生成的//数据生成函数var caseInfo = (function () {var caseInfo = [];//最大值,实际使用中不要用这一条,这个只是用来生成数据的var max = Math.pow(10, parseInt(Math.random() * 5));for (let i = 0; i < 12; i++) {caseInfo.push(parseInt(max * Math.random()));}console.log(caseInfo);return caseInfo;})();// var caseInfo = [0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0];var dateText = ["2014/2", "2014/3", "2014/4", "2014/5", "2014/6", "2014/7", "2014/8", "2014/9", "2014/10", "2014/11", "2014/12", "2015/1"];var draw = new drawCanvas(dom, caseInfo, dateText);// 绘图函数的类,传入的参数依次为,canvas标签应该被放置的父Dom,数据,时间// 1、父dom:支持自适应,最小宽高(此时会设置父dom的overflow为auto)// 2、数据:标准的为12个数据,类型为number,不足12个会自动用0填充满(填充在数组的开始部分);// 3、时间要求格式为:年份/月份,例如2016/12,类型为字符串,非该格式的会被识别为错误并报错(如需修改请自行更改相关判断部分);// 4、y轴坐标的刻度会根据数据的最大值而自动变化;// 5、x轴坐标的刻度自动设为12格function drawCanvas(Dom, caseInfoArray, dateTextArray) {// 设置var color = {xyAxisLine: "#000", //x、y坐标轴的颜色xScale: "#000", //x轴刻度文字的颜色yScale: "#000", //y轴刻度文字的颜色referenceLine: "#bbb", //参考线带颜色dataLine: "#f6793c", //数据线条的颜色errorMessage: "#000" //错误提示的颜色};var font = {yScale: "Microsoft YaHei 12px", //y轴刻度文字xScale: "Microsoft YaHei 12px" //x轴刻度文字};var dataLineWidth = 3; //数据线条的宽度var error = {errorCaseInfo: "错误的数据",errorCaseTpye: "数据类型不是数字,无法绘图",errorDate: "错误的时间输入"}// 设置完//获取基础数据var canvas = document.createElement("canvas");Dom.appendChild(canvas);var ctx = canvas.getContext("2d");var caseInfo = caseInfoArray;var dateText = dateTextArray;//获得并设置canvas标签的最小宽高,然后返回canvas中,绘图区域坐标var setWidthWithHeight = function (Dom, canvas) {//在dojo中,用aspect.after改造// window.onresize,每次触发事件时重置一次,并且绘制一次//获得画布区域的宽度和高度,并重置if (Dom.clientWidth < 700) {canvas.width = 700;Dom.style.overflowX = "auto";} else {canvas.width = Dom.clientWidth;}if (Dom.clientHeight < 250) {canvas.height = 250;Dom.style.overflowY = "auto";} else {canvas.height = Dom.clientHeight;}//坐标轴区域//注意,实际画折线图区域还要比这个略小一点return {x: 60 - 0.5, //坐标轴在canvas上的left坐标y: 40 - 0.5, //坐标轴在canvas上的top坐标maxX: canvas.width - 60.5, //坐标轴在canvas上的right坐标maxY: canvas.height - 40.5 //坐标轴在canvas上的bottom坐标};}// 绘制x、y坐标轴(不包含刻度)var drawAxis = function (ctx, axis) {ctx.beginPath();ctx.lineWidth = 1;ctx.strokeStyle = color.xyAxisLine;ctx.moveTo(axis.x, axis.maxY);ctx.lineTo(axis.x, axis.y);ctx.lineTo(axis.x - 5, axis.y + 5);ctx.moveTo(axis.x, axis.y);ctx.lineTo(axis.x + 5, axis.y + 5);ctx.stroke();// 再画X轴ctx.beginPath();ctx.lineWidth = 1;ctx.strokeStyle = color.xyAxisLine;ctx.moveTo(axis.x, axis.maxY);ctx.lineTo(axis.maxX, axis.maxY);ctx.lineTo(axis.maxX - 5, axis.maxY + 5);ctx.moveTo(axis.maxX, axis.maxY);ctx.lineTo(axis.maxX - 5, axis.maxY - 5);ctx.stroke();// 写y轴原点的数字(注意,虽然是坐标原点,但这个是y轴的)ctx.font = font.yScale;ctx.textAlign = "right";ctx.fillStyle = color.referenceLine;// 设置字体内容,以及在画布上的位置ctx.fillText("0", axis.x - 5, axis.maxY);}// 获得Y轴的最大尺度var getMAXrectY = function (caseInfo) {var theMaxCaseInfo = 0;//用于获取最大值caseInfo.forEach(function (item) {if (item > theMaxCaseInfo) {theMaxCaseInfo = item;}});//返回计算出的最大数字return (function (str) {var number = null;//用于计量坐标轴y轴的最大数字if (str[0] == 1) {if (str[0] + str[1] >= 18) {number = '20';} else {if (Number(str[1]) % 2) {number = str[0] + String(Number(str[1]) + 1);} else {number = str[0] + String(Number(str[1]) + 2);}}for (let i = 2; i < str.length; i++) {number += '0';}} else {number = String(Number(str[0]) + 1);for (let i = 1; i < str.length; i++) {number += '0';}}return number;})(String(theMaxCaseInfo));}//划线和确定单元格的逻辑在这里,逻辑确定好后是将单元格放在rectYArray这个数组中var getDrawYLineLaw = function (MAXrectY) {var rectYArray = [];//当最大案件数小于等于10时,以2为一格if (MAXrectY <= 10) {console.log(MAXrectY);rectYArray.push(2, 4, 6, 8, 10);} else {var str = String(MAXrectY);var zeroNumber = MAXrectY.length - 2;// 用于填充的0的数量,原因是判断时只判断前一位或两位var fillZero = String(Math.pow(10, zeroNumber)).replace('1', '');// 然后先判断首位,如果是1,则最大是之前获取到的最大数值,以2/格为单位// 如果是2~5,则以5/格为单位// 如果是6~9,则以10/格为单位if (Number(str[0]) === 1) {for (var i = 0; i < Number(str[0] + str[1]); i = i + 2) {rectYArray.push(i + 2 + fillZero);}} else if (Number(str[0]) >= 2 && Number(str[0]) < 6) {for (var i = 0; i < Number(str[0] + str[1]); i = i + 5) {rectYArray.push(i + 5 + fillZero);}} else if (Number(str[0]) >= 6 && Number(str[0]) < 10) {for (var i = 0; i < Number(str[0] + str[1]); i = i + 10) {rectYArray.push(i + 10 + fillZero);}}}console.log(rectYArray);return rectYArray;}//画y轴参考线和坐标数字var DrawYLine = function (ctx, axis, YLineLaw) {// 在得到单元格后,开始绘图,绘出y轴上每个单元格的直线// Y轴参考线的x坐标是从0到axis.maxX - 10var yMaxPoint = axis.y + 20; //最上面的y轴坐标var xMaxPoint = axis.maxX - 10; //最右边的x轴坐标ctx.strokeStyle = color.referenceLine;for (let i = 0; i < YLineLaw.length; i++) {ctx.beginPath();// 当前绘制线条的y坐标let yLine = (YLineLaw[i] - YLineLaw[0] ) / YLineLaw[YLineLaw.length - 1] * (axis.maxY - yMaxPoint) + yMaxPoint;ctx.moveTo(axis.x, yLine);ctx.lineTo(xMaxPoint, yLine);ctx.stroke();//绘完线条写文字ctx.font = font.yScale;ctx.textAlign = "right";ctx.fillStyle = color.yScale;// 设置字体内容,以及在画布上的位置ctx.fillText(YLineLaw[YLineLaw.length - i - 1], axis.x - 5, yLine + 5);}}//绘制数据var DrawData = function (ctx, axis, caseInfo, YLineMax, dateText) {// 折线绘图区域的x轴从x=0开始绘图,绘制的最右边是axis.maxX-20(参考线是-10)// y轴是从y=0开始绘制,绘制的最顶部是最顶部参考线的位置(axis.y+20)// 参数依次为:绘图对象ctx,坐标轴区域坐标axis,绘图用的数据caseInfo,Y轴最大值YLineMax,x轴横坐标文字dateTextvar rect = {left: axis.x, //折线绘图区域的lefttop: axis.y + 20, //折线绘图区域的topheight: axis.maxY - axis.y - 20, //折线绘图区域的bottomwidth: axis.maxX - 20 - axis.x //折线绘图区域的right};//绘制数据的折线ctx.beginPath();ctx.strokeStyle = color.dataLine;ctx.lineWidth = dataLineWidth;var firstPoint = {x: rect.left + 0.5, //之所以+0.5,是因为rect.x来源于axis.x表示划线,因此少了0.5px宽,这里要弥补上y: rect.top + (1 - caseInfo[0] / YLineMax) * rect.height + 0.5}
// console.log(firstPoint);ctx.moveTo(firstPoint.x, firstPoint.y);for (let i = 0; i < caseInfo.length; i++) {var point = {x: rect.left + i / 11 * rect.width + 0.5,y: rect.top + (1 - caseInfo[i] / YLineMax) * rect.height};ctx.lineTo(point.x, point.y);//写x轴坐标文字ctx.font = font.xScale;ctx.textAlign = "center";ctx.fillStyle = color.xScale;ctx.fillText(dateText[i], point.x, rect.top + rect.height + 15);}ctx.stroke();}//错误检查var inputError = function () {//不是数组if (!(caseInfo instanceof Array)) {return error.errorCaseInfo;}// 数组数目不足12,用0填充靠前的部分// 大于12,移除前面的部分if (caseInfo.length < 12) {while (caseInfo.length < 12) {caseInfo.unshift(0);}} else if (caseInfo.length > 12) {while (caseInfo.length > 12) {caseInfo.shift(0);}}//判断数组每个元素的类型是否是number或者能否转换为numbervar checkElementType = caseInfo.every(function (item) {//如果强制转换后为NaN,那么if (typeof item !== "number") {return false;} else {return true;}})if (!checkElementType) {return error.errorCaseTpye;}// 月份应该是字符串,如2016/2// 如果被/分割拆分后数组长度不是2,或者拆分后元素0的长度不是4,或者拆分后元素1的长度不是1或2// 或者parseInt转换后为NaNvar checkDateText = dateText.every(function (item) {var date = item.split("/");if (date.length !== 2 || date[0].length !== 4 || date[1].length < 1 || date[1].length > 2 ||isNaN(parseInt(date[0])) || isNaN(parseInt(date[1]))) {return false;} else {return true;}})if (!checkDateText) {return error.errorDate}return false;}//绘图函数,绘制时调用本函数this.toDraw = function () {// 设置canvas的Dom的宽高var axis = setWidthWithHeight(Dom, canvas);// 绘制x、y坐标轴(不包含刻度)drawAxis(ctx, axis);//如果检测返回false// 如果没问题,则返回false,否则值可以隐式转换为truevar errorMessage = inputError();if (errorMessage) {ctx.font = "Bold 20px Arial";ctx.textAlign = "center";ctx.fillStyle = color.errorMessage;ctx.fillText(errorMessage, (axis.x + axis.maxX) / 2, (axis.y + axis.maxY) / 2);return;}// 获得Y轴的最大尺度var MAXrectY = getMAXrectY(caseInfo);// 获得y轴划参考线规则var YLineLaw = getDrawYLineLaw(MAXrectY);// 绘制Y轴参考线DrawYLine(ctx, axis, YLineLaw);// 绘制数据DrawData(ctx, axis, caseInfo, YLineLaw[YLineLaw.length - 1], dateText);};//启动本实例时绘图一次this.toDraw();var self = this;//浏览器窗口大小变化时,绘图一次//潜在缺点:会覆盖其他的这个方法,建议用jquery的$(window).resize来替代window.onresize = function () {self.toDraw();};}</script>
</body>
</html>
带你快速玩转canvas——写个折线图相关推荐
- 带你快速玩转canvas(8)非常用API的说明集
10. API简单说明 解释: 1. 写这个的目的只是熟悉API的功能,因此不会去像上面那样写DEMO和一一验证: 2. 只写API和API的功能,具体请参照MDN: 3. 或许存在兼容性问题,用的时 ...
- 封装构造函数,用canvas写饼状图和柱状图
封装构造函数,用canvas写饼状图和柱状图 封装函数 // 场景 function XDLScence( options ) {this.stage = options.stage;//执行场景的初 ...
- Android之玩转MPAndroidChart让(折线图、柱形图、饼状图、散... http://www.apkbus.com/thread-267832-1-1.html)...
Android之玩转MPAndroidChart让(折线图.柱形图.饼状图.散... http://www.apkbus.com/thread-267832-1-1.html) 转载于:https:/ ...
- canvas制作柱形图/折线图/饼状图,Konva写动态饼状图
制作饼状图 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF ...
- Android之玩转MPAndroidChart让(折线图、柱形图、饼状图、散列图、雷达图)优雅的舞...
2019独角兽企业重金招聘Python工程师标准>>> 把开源项目MPAndroidChart里面的折线图.柱形图.饼状图.散列图.雷达图怎么使用和一些属性详细的介绍,当我们项目 g ...
- canvas绘制经典折线图(一)
最终效果图如下: 实现步骤如下:注-引用了jQuery HTML代码 <!doctype html> <html lang="en"> <head&g ...
- canvas动态绘制折线图
公司的实践课题vue项目,用折线图展示近六个月的收入和支出,以前看到图表,第一个想到的就是从echarts官网调用,但这次导师建议我们用canvas去画,然后去封装,这样可以加大课题难度,同时理解底层 ...
- python绘制不带颜色曲线图_Python数据可视化库-Matplotlib——折线图,子图绘制
# coding:utf-8 import pandas as pd import numpy as np from matplotlib import pylab as plt # 导入数据可视化库 ...
- Android之玩转MPAndroidChart让(折线图、柱形图、饼状图、散列图、雷达图)优雅的舞动
第一步:不废话,先爆照 我的github地址:https://github.com/changechenyu/MPAndroidChartTest 第二步:介绍MPAndroidChart适用场景并把 ...
- 玩转canvas之实现一个圆角矩形
使用canvas封装一个绘制圆角矩形的方法 代码 原理 思路 分析 讲解 缺陷 改进 改进圆角非正规圆角的问题 改进后的代码实现的圆角 改进圆角过大的问题 总结 完整代码 在canvas中绘制一个矩形 ...
最新文章
- linux cpu 超频,Linux 调整 cstate 实现cpu超频
- 基于FastJson的通用泛型解决方案
- 如何去遍历对象中的所有的属性值
- Android中的表格布局
- 一次面试引发的思考(中小型网站优化思考)
- UVa 1629 切蛋糕(记忆化搜索)
- cocos2d-x 2.2 创建项目
- C#LeetCode刷题之#559-N叉树的最大深度​​​​​​​(Maximum Depth of N-ary Tree)
- 【Elasticsearch】估算在 Elasticsearch 中存储文档的成本
- espresso 2.0.4 Apple Xcode 4.4.1 coteditor 价格
- offload error: cannot find offload entry解决办法
- 大数据可视化的应用方法
- 泛型思想理解数据结构链表
- Fiddler的安装和使用教程(详细)
- Tomact运行不起来,打开startup.bat 一闪而过
- 程序员如何写一份打动面试官的简历?这些坑你必须注意了!
- lol韩服游戏内设置_英雄联盟手游韩服怎么设置中文 英雄联盟手游韩服设置中文教程...
- git报错:remote: error: hook declined to update refs/heads/master
- mac打不开磁盘映像资源忙
- 鼠标指针乱跑的解决方案
热门文章
- javaweb简单源代码_Java Web轻松学39 - JSP核心原理
- tomcat出现5个using_婚姻出现危机的8个迹象,日常5个细节促进夫妻情感
- opencv 梯度幅值_OpenCV学习:图像边缘检测
- 巴伦变压器电路图_基于变压器原理的巴伦电路系统分析
- python中itertools groupby函数是干嘛的_python 分组函数 itertools groupby
- 基于FaceNet人脸识别的人脸对比认证(一) -- 环境部署、认证实验
- 机器学习基础 -- 李宏毅2020机器学习课程笔记(一)
- 基于京东家电商品知识图谱的自动问答系统(二) -- IDEA搭建开发环境
- ifconfig知识总结
- mysql5.7 主从