需求:

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——写个折线图相关推荐

  1. 带你快速玩转canvas(8)非常用API的说明集

    10. API简单说明 解释: 1. 写这个的目的只是熟悉API的功能,因此不会去像上面那样写DEMO和一一验证: 2. 只写API和API的功能,具体请参照MDN: 3. 或许存在兼容性问题,用的时 ...

  2. 封装构造函数,用canvas写饼状图和柱状图

    封装构造函数,用canvas写饼状图和柱状图 封装函数 // 场景 function XDLScence( options ) {this.stage = options.stage;//执行场景的初 ...

  3. Android之玩转MPAndroidChart让(折线图、柱形图、饼状图、散... http://www.apkbus.com/thread-267832-1-1.html)...

    Android之玩转MPAndroidChart让(折线图.柱形图.饼状图.散... http://www.apkbus.com/thread-267832-1-1.html) 转载于:https:/ ...

  4. canvas制作柱形图/折线图/饼状图,Konva写动态饼状图

    制作饼状图 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF ...

  5. Android之玩转MPAndroidChart让(折线图、柱形图、饼状图、散列图、雷达图)优雅的舞...

    2019独角兽企业重金招聘Python工程师标准>>> 把开源项目MPAndroidChart里面的折线图.柱形图.饼状图.散列图.雷达图怎么使用和一些属性详细的介绍,当我们项目 g ...

  6. canvas绘制经典折线图(一)

    最终效果图如下: 实现步骤如下:注-引用了jQuery HTML代码 <!doctype html> <html lang="en"> <head&g ...

  7. canvas动态绘制折线图

    公司的实践课题vue项目,用折线图展示近六个月的收入和支出,以前看到图表,第一个想到的就是从echarts官网调用,但这次导师建议我们用canvas去画,然后去封装,这样可以加大课题难度,同时理解底层 ...

  8. python绘制不带颜色曲线图_Python数据可视化库-Matplotlib——折线图,子图绘制

    # coding:utf-8 import pandas as pd import numpy as np from matplotlib import pylab as plt # 导入数据可视化库 ...

  9. Android之玩转MPAndroidChart让(折线图、柱形图、饼状图、散列图、雷达图)优雅的舞动

    第一步:不废话,先爆照 我的github地址:https://github.com/changechenyu/MPAndroidChartTest 第二步:介绍MPAndroidChart适用场景并把 ...

  10. 玩转canvas之实现一个圆角矩形

    使用canvas封装一个绘制圆角矩形的方法 代码 原理 思路 分析 讲解 缺陷 改进 改进圆角非正规圆角的问题 改进后的代码实现的圆角 改进圆角过大的问题 总结 完整代码 在canvas中绘制一个矩形 ...

最新文章

  1. linux cpu 超频,Linux 调整 cstate 实现cpu超频
  2. 基于FastJson的通用泛型解决方案
  3. 如何去遍历对象中的所有的属性值
  4. Android中的表格布局
  5. 一次面试引发的思考(中小型网站优化思考)
  6. UVa 1629 切蛋糕(记忆化搜索)
  7. cocos2d-x 2.2 创建项目
  8. C#LeetCode刷题之#559-N叉树的最大深度​​​​​​​(Maximum Depth of N-ary Tree)
  9. 【Elasticsearch】估算在 Elasticsearch 中存储文档的成本
  10. espresso 2.0.4 Apple Xcode 4.4.1 coteditor 价格
  11. offload error: cannot find offload entry解决办法
  12. 大数据可视化的应用方法
  13. 泛型思想理解数据结构链表
  14. Fiddler的安装和使用教程(详细)
  15. Tomact运行不起来,打开startup.bat 一闪而过
  16. 程序员如何写一份打动面试官的简历?这些坑你必须注意了!
  17. lol韩服游戏内设置_英雄联盟手游韩服怎么设置中文 英雄联盟手游韩服设置中文教程...
  18. git报错:remote: error: hook declined to update refs/heads/master
  19. mac打不开磁盘映像资源忙
  20. 鼠标指针乱跑的解决方案

热门文章

  1. javaweb简单源代码_Java Web轻松学39 - JSP核心原理
  2. tomcat出现5个using_婚姻出现危机的8个迹象,日常5个细节促进夫妻情感
  3. opencv 梯度幅值_OpenCV学习:图像边缘检测
  4. 巴伦变压器电路图_基于变压器原理的巴伦电路系统分析
  5. python中itertools groupby函数是干嘛的_python 分组函数 itertools groupby
  6. 基于FaceNet人脸识别的人脸对比认证(一) -- 环境部署、认证实验
  7. 机器学习基础 -- 李宏毅2020机器学习课程笔记(一)
  8. 基于京东家电商品知识图谱的自动问答系统(二) -- IDEA搭建开发环境
  9. ifconfig知识总结
  10. mysql5.7 主从