学习了这么多Canvas中的API,是时候出来溜溜了,写一个low版的柱状图吧!
先瞜一眼效果图:
column.gif
分析一下简版思路:
-
上个canvas
宽高为600* 600 -
绘制刻度轴
-
绘制单根柱子
-
单个tip的绘制
-
根据数据个数循环绘制
第一步: 上个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;
}
}
好了,总结一下这些功能
- 来个刻度
scaleLine(context, isColumn, isPlus, step, length) - 数据来一打
Var arr; - 将数据传入
drawData(context, arr, time)
该方法里面调用了:
3.1 单个tip的绘制功能
drawTips(context, text, color)
单个柱子的绘制
3.2 drawRect(context, x, width, height, bgColor)
low的柱状图就这么low,low的写完了~
网友评论