美文网首页
canvas-柱状

canvas-柱状

作者: 我只会吃饭 | 来源:发表于2019-07-09 08:16 被阅读0次

    学习了这么多Canvas中的API,是时候出来溜溜了,写一个low版的柱状图吧!
    先瞜一眼效果图:


    column.gif

    分析一下简版思路:

    1. 上个canvas
      宽高为600* 600

    2. 绘制刻度轴

    3. 绘制单根柱子

    4. 单个tip的绘制

    5. 根据数据个数循环绘制


    第一步: 上个Canvas

    // 创建canvas
    var canvas = document.createElement('canvas');
    // 设置宽高
    canvas.width = 600;
    canvas.height = 600;
    // 背景颜色
    canvas.style.backgroundColor = '#eee';
    // 添加至body中
    document.body.appendChild(canvas);
    // 获取2d上下文
    var ctx = canvas.getContext('2d');
    

    第二步:绘制刻度轴
    1.绘制刻度轴的时候,我们的的轴心(0, 0)在canvas中的(50, 400)上,因此我们可以translate移动原点,当然,需要提前保存当前的状态
    2.绘制Y轴刻度时,需要考虑到刻度值是反着的,并且文案绘制的时候,水平对齐方式,垂直对齐方需要稍微注意一下

    绘制刻度线的函数

    /**
     * 绘制刻度线
     * @param {*} context 
     * @param {*} isColumn : 是否垂直
     * @param {*} isPlus : 是否为正
     * @param {*} step : 刻度值
     * @param {*} length : 刻度个数
     */
    function scaleLine(context, isColumn, isPlus, step, length) {
        context.save();
        context.lineWidth = 2;
        context.strokeStyle = '#000';
        context.textAlign = 'right';
        context.textBaseline = 'middle';
        context.beginPath();
        context.moveTo(0, 0);
        if (isColumn) {
            // 垂直绘制Y轴
            for (var i = 0; i < length; i++) {
                // 正负轴的判断
                var y = isPlus ? -i * step : i * step;
                // 绘制每段刻度
                context.lineTo(0, y);
                // 刻度值的突出线
                context.lineTo(-5, y);
                // 刻度值
                context.fillText(-y, -10, y)
                context.lineTo(0, y);
            }
        } else {
            // 水平绘制X轴
            for (var i = 0; i < length; i++) {
                // 正负轴的判断
                var x = isPlus ? -i * step : i * step;
                context.lineTo(x, 0);
            }
        }
        context.stroke();
        context.restore();
    }
    

    通过调用scaleLine函数 ,我们可以另写一个函数,统一调用,并且统一的将原点移动至(50, 400)位置

    // 绘制坐标刻度线
    function scaleXY(context) {
        context.save();
        // 移动原点, 将刻度线坐标(0, 0) 移动到 (50,400)
        context.translate(50, 400);
        // 绘制刻度
        // +y轴
        scaleLine(context, true, true, 50, 7);
    
        // -y轴
        scaleLine(context, true, false, 50, 3);
    
        // x轴
        scaleLine(context, false, false, 50, 9);
    
        context.restore();
    
    轴.PNG

    好了,这样我们基本的刻度轴在此时就会出现在画布上,是不是很简单~

    第三步: 绘制单根柱子
    在绘制单根柱子的时候,顶部会有弹性的表现,采用最简单的思路,
    1.画一帧:画高于当前数据值
    2.擦一帧,擦高于当前数据值
    3.画一帧:画低于当前数据值
    4.擦一帧,擦低于当前数据值
    5.画一帧:画高于当前数据值

    这四个步骤循环,直到最后回到当前数据值,我们需要的就是控制其步长,那么我们完全可以使用比例来画,并且用数组存储比例,数组的长度就是步长,每帧按顺序画一次数组中的比例及实现了,就是这么简单,就是这么的low(其实是因为自己写弹性动画的时候,边界值的判断卡着自己脑壳了,如果有更好的思路希望能提供一下,感谢~)

    // 每一帧的比例,画多少帧,取决于比例数组的长度
        var scaleStep = [0.2, 0.3, 0.4, 0.5, 0.6, 0.75, 0.85, 0.95, 1, 1.05, 1.1, 1.15, 1.1, 1.05, 1, 0.975, 0.950, 0.925, 0.90, 0.875, 0.850, 0.825, 0.80, 0.825, 0.850, 0.875, 0.90, 0.925, 0.950, 0.975, 1];
    

    按照每一帧画上去,肯定是需要擦除上一帧
    因此:
    当画第二帧比例的时候,需要擦去第一帧所画的
    当画第三帧比例的时候,需要擦去第二帧所画的
    当画第四帧比例的时候,需要擦去第三帧所画的
    ……
    代码就是:


    Height: 为数据的高度

    var clearH = height * (scaleStep[i === 0 ? 0 : i - 1] + 0.1);
    

    var fillH = height * scaleStep[i];
    

    为什么擦的时候这判断?

    (scaleStep[i === 0 ? 0 : i - 1] + 0.1)
    

    这个判断是考虑到,当为第一帧的时候,我们没有上一帧了呀,还擦个球球,因此第一帧的时候,擦的话就擦自己吧。擦完了自己就将自己画上,当执行第二帧的时候去擦掉第一帧
    好滴,好奇为什么擦要+ 0.1 的比例呢?
    哈哈,好像是精度不足,擦不完,可能会有点漏了,因此擦的时候就多擦点吧

    在画柱子的时候呢,X轴会稍稍有点被盖住,因此需要重绘一下X轴

    scaleLine(context, false, false, 50, 9);
    

    然后呢? 这柱子画那呢?
    当然是从x轴开始画呀,所以又要移动一下原点啦,这个是每一帧都需要的移动的,不可能在定时器外面使用(定时器是异步的)

    // 移动原点, 将刻度线坐标(0, 0) 移动到 (50,400)
            context.translate(50, 400);
    

    好了,综上所述,来个定时器吧,把他们装起来,每17毫秒来一下,就实现的弹性的效果了

    /**
     * 绘制单根树状
     * @param {*} context
     * @param {*} x x轴坐标
     * @param {*} width 宽度
     * @param {*} height 高度
     * @param {*} bgColor 填充颜色
     */
    function drawRect(context, x, width, height, bgColor) {
        // 每一帧的比例,画多少帧,取决于比例数组的长度
        var scaleStep = [0.2, 0.3, 0.4, 0.5, 0.6, 0.75, 0.85, 0.95, 1, 1.05, 1.1, 1.15, 1.1, 1.05, 1, 0.975, 0.950, 0.925, 0.90, 0.875, 0.850, 0.825, 0.80, 0.825, 0.850, 0.875, 0.90, 0.925, 0.950, 0.975, 1];
        var i = 0;
        var timer = setInterval(function () {
            context.save();
            // 移动原点, 将刻度线坐标(0, 0) 移动到 (50,400)
            context.translate(50, 400);
    
            // 清除是上一根柱子的高度
            var clearH = height * (scaleStep[i === 0 ? 0 : i - 1] + 0.1);
            context.clearRect(x, -clearH, width, clearH);
    
            // 柱子的颜色
            context.fillStyle = bgColor;
    
            // 绘制柱子的高度
            var fillH = height * scaleStep[i];
            context.fillRect(x, -fillH, width, fillH);
    
            // 重新绘制一下X轴: 因为柱子会遮住X轴
            scaleLine(context, false, false, 50, 9);
    
            // 下一帧
            i++;
            
            // 当循环步长数组结束时 
            if (i === scaleStep.length) {
                clearInterval(timer);
                timer = i = scaleStep = null;
            }
    
            context.restore();
        }, 17);
    }
    

    来个参数测试一下吧~


    simpleC.gif

    实现起来也很简单
    只不过多个之间需要保持间距,那么下一个tip是前面所有tip的间距以及高度之和就可以了
    来一个起始间距高度

    来一个起始间距高度

    var allHeight = 10;
    

    每绘制一个tip高度就需要叠加一次(我就来了个死的, 毕竟low嘛)

    allHeight += 20;
    

    好吧,上代码

    / 每绘制一个提示,则需要叠加计算一次,下一次绘制的坐标是之前绘制过后的高度之和
        // 起始高度间距
        var allHeight = 10;
        /**
         * 
         * @param {*} context 
         * @param {*} text 文案名字
         * @param {*} color 填充颜色
         */
        function drawTips(context, text, color) {
            context.save();
            // 填充颜色
            context.fillStyle = color;
    
            // 小色块的绘制
            context.fillRect(500, allHeight, 10, 10);
    
            // 绘制文字
            context.font = '14px bold';
            context.textBaseline = 'middle';
            // x轴的位置随意定义一个
            context.fillText(text, 520, allHeight + 6);
            context.restore();
    
            // 高度每次画完一个需要叠加一次
            allHeight += 20;
        }
    
    simpleTip.PNG

    到这就已经完成前面四步了,就剩下数据了~


    好滴:我准备了一组low版数据

    var arr = [
        {
            name: '项目一',
            height: 50,
            color: 'purple'
        },
        {
            name: '项目二',
            height: 100,
            color: 'skyblue'
        },
        {
            name: '项目三',
            height: 120,
            color: 'rgb(252, 157, 154)'
        },
        {
            name: '项目四',
            height: 200,
            color: 'rgb(244, 208, 4)'
        },
        {
            name: '项目五',
            height: -50,
            color: 'orange'
        },
        {
            name: '项目六',
            height: -100,
            color: 'rgb(254, 67, 101)'
        },
        {
            name: '项目七',
            height: 170,
            color: 'rgb(204, 200, 169)'
        },
        {
            name: '项目八',
            height: 250,
            color: 'rgb(240, 205, 173)'
        },
        {
            name: '项目九',
            height: -20,
            color: 'rgb(131, 175, 155)'
        },
        {
            name: '项目十',
            height: -100,
            color: 'rgb(220, 87, 18)'
        }
    ];
    

    绘制每根柱子都需要有间距,也和绘制tip一样,需要依次循环叠加x坐标值
    每次绘制柱子之间需要有时间的间隔
    绘制柱子的同时,需要绘制tip,那么我们可以整合至一个功能里面

    /**
     * 绘制数据步骤
     * @param {*} context 
     * @param {*} arr 数据
     * @param {*} time 每绘制一根柱子的间隔时间
     */
    function drawData(context, arr, time) {   
        // 从第一个数据开始,每隔500毫秒绘制下一个数据
        var i = 0;
        // 每绘制一根柱子,则需要叠加计算一次,下一次绘制的坐标是之前绘制过后的宽度之和
        var allWidth = 10;
        var timer = setInterval(function () {
            // 绘制每一根数据
            drawRect(context, allWidth, 20, arr[i].height, arr[i].color);
    
            // 绘制提示
            drawTips(context, arr[i].name, arr[i].color);
    
            // 每次都加30  柱子的宽度以及间隔10 
            allWidth += 30;
            i++;
            if (i === arr.length) {
                clearInterval(timer);
                timer = null;
            };
    
        }, time);
    
        // 每绘制一个提示,则需要叠加计算一次,下一次绘制的坐标是之前绘制过后的高度之和
        // 起始高度间距
        var allHeight = 10;
        /**
         * 
         * @param {*} context 
         * @param {*} text 文案名字
         * @param {*} color 填充颜色
         */
        function drawTips(context, text, color) {
            context.save();
            // 填充颜色
            context.fillStyle = color;
    
            // 小色块的绘制
            context.fillRect(500, allHeight, 10, 10);
    
            // 绘制文字
            context.font = '14px bold';
            context.textBaseline = 'middle';
            // x轴的位置随意定义一个
            context.fillText(text, 520, allHeight + 6);
            context.restore();
    
            // 高度每次画完一个需要叠加一次
            allHeight += 20;
        }
    }
    

    好了,总结一下这些功能

    1. 来个刻度
      scaleLine(context, isColumn, isPlus, step, length)
    2. 数据来一打
      Var arr;
    3. 将数据传入
      drawData(context, arr, time)
      该方法里面调用了:
      3.1 单个tip的绘制功能
      drawTips(context, text, color)
      单个柱子的绘制
      3.2 drawRect(context, x, width, height, bgColor)

    low的柱状图就这么low,low的写完了~

    相关文章

      网友评论

          本文标题:canvas-柱状

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