美文网首页
带你快速玩转canvas(4)实战——写个折线图

带你快速玩转canvas(4)实战——写个折线图

作者: 淡淡紫色 | 来源:发表于2018-10-12 14:07 被阅读0次

    需求:

    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 - 10
    
                var 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轴横坐标文字dateText
                var rect = {
                    left: axis.x,           //折线绘图区域的left
                    top: axis.y + 20,       //折线绘图区域的top
                    height: axis.maxY - axis.y - 20,      //折线绘图区域的bottom
                    width: 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或者能否转换为number
                var 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转换后为NaN
                var 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,否则值可以隐式转换为true
                var 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>
    

    转自:https://blog.csdn.net/qq20004604/article/details/53675596

    相关文章

      网友评论

          本文标题:带你快速玩转canvas(4)实战——写个折线图

          本文链接:https://www.haomeiwen.com/subject/ygxgaftx.html