canvas-柱状

学习了这么多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的写完了~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容