这回算是真明白了什么叫"林子大了什么鸟都有!"之前就有听说面试骗代码的情况,但也仅仅只是听说。这回是真亲身遇到了。来来来,自带小板凳,准备好瓜子。好好看看我被骗的经历。顺便也看看使用原生Canvas绘制饼图,使用插件(比如Echart)也就分分钟的事情,但多了解一些原生的东西,总不会有错的。
正文开始.....
我是不是被骗代码了???
还是前段时间面试时发生的事情。3月21号晚八点,此时心态已处于第三阶段(详情可查看面试总结),突然收到一封邮件,如下:
巧了,3月22有两场面试,还是两家我觉得不错的公司(南方+、爱范儿科技),我误以为就是这两家其中一家的测试。
熬到22号两点,饼图倒是画出来了,只是线条还有很大问题。当时的想法是通过计算位置,使用div来画线条。这有两个问题:一是无法实现拆线;二是会不准。因为白天还有面试,所以就直接发了半成品过去,并询问是什么公司。对话如下:
居然还没约面试,只有想会是哪家公司呢?反正没往骗代码上想!3月23,继续尝试了一下,线条也通过canvas来绘制,解决了之前的两个问题,还处理考虑挤一起的需求,算得上已经实现需求。效果如下:
3月23晚上,发送过去。3月25晚上,收到回复确是这样:
你好,舒同学。看了你的作品,能否再完善一下?因为这是仿支付宝的饼图,所以希望是适配于移动设备的,另外APP里的Webview好像要在6.0以上才支持es6语法,想把它转成es5语法的,麻烦舒同学了
到这里我才开始觉得不对劲。 为啥要ES6转ES5,又体现不了什么技术能力,又不是实际使用;手机适配的问题,我这大小是可配置的并没有写死 。所以,马上询问是什么公司。回复如下:
林老师,测试题的目的应该就是了解一下应聘者的能力。我想,题目做到现在,我大概的代码风格和技术能力,你应该了解了。
请问贵公司是?
然后。。。然后就再没收到回复。。。。
这里我才想到自己是不是被骗代码了?可现在都不敢相信呀,这种代码也有人骗么?可如果不是,难道我这代码写得太low了,所以连个面试机会都拿不到?
所以,这里贴上代码,分享一下生Canvas绘制饼图的想法,同时也让大家帮忙看看,这样的代码能不能得到一次面试机会呀![笑哭]*10
饼图绘制代码
稍微有些难的几个点:
- 会用到三角函数各种计算坐标,如果早已忘记,需要回头看看;
- 如何处理点会挤在一的情况;
- canvas的画弧方法arc的0度是从笛卡坐标的90度开始,角度不一致需要区分;
下面是完整的代码,有完成的注释,代码比注释还多。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>饼图</title>
</head>
<body>
<script>
/**
* 绘制饼图函数
* 使用到的ES6语法有函数默认参数、解构、字符模板
* 如果不熟悉,可以看看阮老师的《ECMAScript 6 入门》
* 网址 http://es6.ruanyifeng.com/
* 函数的默认参数
* r 圆环的圆半径 data 数据项
* width 图表宽度 height 图表高度
*/
function addPie({r = 100,width = 450,height = 400,data = []} = {}) {
let cns = document.createElement('canvas'); //创建一个canvas
let ctx = cns.getContext('2d'); //获取canvas操作对象
let w = width;
let h = height; //将width、height赋值给w、h
let originX = w / 2; //原点x值
let originY = h / 2; //原点y值
let points = []; //用于保存数据项线条起点坐标
let leftPoints = []; //保存在左边的点
let rightPoints = []; //保存在右边的点,分出左右是为了计算两点垂直间距是否靠太近
let fontSize = 12; //设置字体大小,像素
//total保存总花费,用于计算数据项占比
let total = data.reduce(function(v, item) {
return v + item.cost;
}, 0)
/**
* sAngel 起始角弧度
* arc方法绘制弧线/圆时,弧的圆形的三点钟位置是 0 度
* 也就是0弧度对应笛卡坐标的90度位置
* 为了让饼图从笛卡坐标的0度开始
* 起始角弧度需要设置为-.5 * Math.PI
*/
let sAngel = -.5 * Math.PI;
let eAngel = -.5 * Math.PI; //结束角弧度,初始值等于sAngel
let aAngel = Math.PI * 2; //整圆弧度,用于计算数据项弧度
let pointR = r + 10; //计算线条起始点的半径
let minPadding = 30; //设置数据项两点最小间距
//设置canvas和画布大小
cns.width = ctx.width = w;
cns.height = ctx.height = h;
let cAngel; //数据项中间位置的弧度值,用于计算线条起始点
for (let i = 0, len = data.length; i < len; i++) { /* 绘制不同消费的份额 */
/**
* 计算结束角弧度
* 等于上一项数据起始弧度值(sAngel)
* 加数据占比(data[i].cost/total)乘以整圆弧度(aAngel)
*/
eAngel = sAngel + data[i].cost/total * aAngel ;
//画弧
_drawArc(ctx, {
origin: [originX, originY],
color: data[i].color,
r,
sAngel,
eAngel
})
/**
* 计算cAngel值
* cAngel是用于计算线条起始点
* 等于当前数据项的起始弧度:sAngel
* 加上当前数据项所占弧度的一半:(eAngel - sAngel) / 2
* 因为arc方法0弧度对应笛卡坐标的90度位置,我们让sAngel从 -0.5 * Math.PI开始的
* 所以cAngel还要加 0.5 * Math.PI
*/
cAngel = 0.5 * Math.PI + sAngel + (eAngel - sAngel) / 2;
/**
* 保存每个数据项线条的起始点
* 根据三角函数
* 已知半径/斜边长:pointR, 通过正弦函数可以计算出对边长度
* 原点x坐标加对边长度,就是线条起始点x坐标
* 通过余弦函数可以计算出邻边长度
* 原点y坐标减邻边长度,就是线条起始点y坐标
*/
points.push([originX + Math.sin(cAngel) * pointR, originY - Math.cos(cAngel) * pointR])
sAngel = eAngel; //设置下一数据项的起始角度为当前数据项的结束角度
}
for (let i = 0, len = points.length; i < len; i++) { /* 绘制起始点的小圆点,并分出左右 */
// 绘制起始点的小圆点
_drawArc(ctx, {
origin: points[i],
color: data[i].color,
r: 2
})
if (points[i][0] < originX) { /* x坐标小于原点x坐标,在左边 */
leftPoints.push({
point: points[i],
/**
* top标记坐标是否在y轴正方向(是不是在上方)
* 用于判断当两点挤在一起时,是优先向下还是向上移动线条线束点坐标
*/
top: points[i][1] < originY, //y坐标小于原点y坐标。表示在上方
/**
* endPoint保存线条结束点坐标
* y值不变,在左边时结束点x为零
*/
endPoint: [0, points[i][1]]
});
} else { /* 否则在右边*/
rightPoints.push({
point: points[i],
top: points[i][1] < originY, //y坐标小于原点y坐标。表示在上方
endPoint: [w, points[i][1]] //y值不变,在右边时结束点x为图表宽度w
});
}
}
_makeUseable(rightPoints); //处理右边挤在一起的情况
_makeUseable(leftPoints.reverse(), true); //处理左边挤在一起的情况
leftPoints.reverse(); //为什么要翻转一下,看_makeUseable函数
let i = 0;
for (let j = 0, len = rightPoints.length; j < len; j++) { // 绘制右侧线条、文本
_drawLine(ctx, {data:data[i], point:rightPoints[j], w, direct: 'right'});
i++;
}
for (let j = 0, len = leftPoints.length; j < len; j++) { // 绘制左侧线条、文本
_drawLine(ctx, {data:data[i], point:leftPoints[j], w});
i++;
}
/* 再绘制一个圆盖住饼图,实现圆环效果 */
_drawArc(ctx, {
origin: [originX, originY],
r: r / 5 * 3
})
document.body.appendChild(cns); /* 添加到body中 */
/* 画弧函数 */
function _drawArc(ctx, {color = '#fff',origin = [0, 0],r = 100,sAngel = 0, eAngel = 2 * Math.PI}) {
ctx.beginPath(); //开始
ctx.strokeStyle = color; //设置线条颜色
ctx.fillStyle = color; //设置填充色
ctx.moveTo(...origin); //移动原点
ctx.arc(origin[0], origin[1], r, sAngel, eAngel); //画弧
ctx.fill(); //填充
ctx.stroke();//绘制已定义的路径,可省略
}
/* 画线和文本 函数 */
function _drawLine (ctx, {direct='left',data={},point={},w = 200}) {
ctx.beginPath(); //开始
ctx.moveTo(...point.point); //移动画笔到线条起点
ctx.strokeStyle = data.color; //设置线条颜色
if (point.turingPoint) //存在折点
ctx.lineTo(...point.turingPoint); //画一条到折点的线
ctx.lineTo(...point.endPoint);//画一条到结束点的线
ctx.stroke();//绘制已定义的路径
ctx.font = `${fontSize}px 微软雅黑`; //设置字体相关
ctx.fillStyle = '#000'; //设置字体颜色
ctx.textAlign = direct;//设置文字对齐方式
//绘制数据项花费文字,垂直上移两个像素
ctx.fillText(data.cost,direct === 'left'?0:w, point.endPoint[1] - 2);
//绘制数据项名称,垂直下移fontSize个像素
ctx.fillText(data.category, direct === 'left'?0:w, point.endPoint[1] + fontSize);
}
function _isUseable(arr) { // 判断是否会有数据挤在一起(两点最小间距是否都大于等于minPadding)
if (arr.length <= 1)
return true;
return arr.every(function(p, index, arr) {
if (index === arr.length-1) {
//因为是当前项和下一项比较,所以index === arr.length-1直接返回true
return true;
} else {
/**
* 判断当前数据项结束点:p.endPoint[1]
* 和下一数据项结束点垂直间距是否大于等于最小间距:minPadding
* 只有数据线条结束点垂直间距大于等于最小间距,才会返回true
*/
return arr[index + 1].endPoint[1] - p.endPoint[1] >= minPadding;
}
})
}
function _makeUseable(arr, left) {// 处理挤在一起的情况
let diff, turingAngel, x, maths = Math.sin,diffH, l;
/**
* 这里的思路是
* 如果数据是非可用的(会挤在一起,_isUseable判断)
* 就一直循环移动数据,直至可用
* 数据项过多时会出现死循环
* 因为需求上说数据项不会过多,并且还要让大家帮我看看能不能获得面试机会
* 所以这里不做修改
* 可能会有更好的算法,我这鱼木脑袋只想到这种的
* 欢迎大家提供更好的思路或算法
*/
while (!_isUseable(arr)) { //每次循环处理一次,直至数据不会挤在一起
for (let i = 0, len = arr.length - 1; i < len; i++) { //遍历数组
diff = arr[i + 1].endPoint[1] - arr[i].endPoint[1]; //计算两点垂直间距
if (diff < minPadding) { //小于最小间距,表示会挤到一起
if (arr[i].top && arr[i + 1].top) { //是在上部的点,向上移动
/**
* 判断当前的点是否还可以向上移动
* 上方第一个点最往上只可以移动到y值为0
* 之后依次最往上只能移动动y值为:i * minPadding
* 所以下面判断应该是:arr[i].endPoint[1] - (minPadding - diff) > i * minPadding
*/
/**
* 上面左边leftPoints的点需要翻转一下的原因是
* 左边leftPoints的点最上面的点是排在最后的
*/
if (arr[i].endPoint[1] - (minPadding - diff) > 0 && arr[i].endPoint[1] > i * minPadding) {
//当前点还能向上移动
//向上移动到不挤(满足最小间距)
arr[i].endPoint[1] = arr[i].endPoint[1] - (minPadding - diff);
} else {
//当前点不向上移动到满足最小间距的位置
//先把当前点移动到能够移动的最上位置
arr[i].endPoint[1] = i * minPadding;
//再把下个点移动,使满足最小间距
arr[i + 1].endPoint[1] = arr[i + 1].endPoint[1] + (minPadding - diff);
}
} else {
//是在下部的点,向下移动
/**
* 判断当前点的下个点是否还可以向下移动
* 下方最后一个点最往下只可以移动到y值为h,即图表高度
* 之前的点依次最往下只能移动动y值为:h - (len - i - 1) * minPadding
* 所以下面判断应该是:arr[i + 1].endPoint[1] + (minPadding - diff) < h - (len - i - 1) * minPadding
*/
if (arr[i + 1].endPoint[1] + (minPadding - diff) < h && arr[i + 1].endPoint[1] < h - (len - i - 1) * minPadding) {
//当前点的下个点还能向下移动
//当前点的下个点向下移动到不挤(满足最小间距)
arr[i + 1].endPoint[1] = arr[i + 1].endPoint[1] + (minPadding - diff)
} else {
//当前点的下个点不能向下移动
//先把当前点的下个点向下移动能够移动的最下位置
arr[i + 1].endPoint[1] = h - (len - i - 1) * minPadding;
//再把当前点移动,使满足最小间距
arr[i].endPoint[1] = arr[i].endPoint[1] - (minPadding - diff);
}
}
break; //每次移动完成直接退出循环,判断一次是否已经不挤
}
}
}
/**
* 遍历已经可用的数据
* 起点和结束点不在同一水平线上
* 需要设置折点
* 这里通过设置折线角度,计算出折点位置
* 回头一想,其实可以用更简单的方法,想复杂了
*/
for (let i = 0, len = arr.length; i < len; i++) {
//起点和结束点y值不等,则不在同一水平线,需要设置折点
if (arr[i].point[1] !== arr[i].endPoint[1]) {
turingAngel = 1 / 3 * Math.PI; //默认折线角度设置60度
//计算出起点和结束点高度差
diffH = arr[i].endPoint[1] - arr[i].point[1];
//计算出起点和结束点水平距离l
l = Math.abs(arr[i].endPoint[0] - arr[i].point[0]);
/**
* x 这里的本意是
* 想计算出折点和起始点的水平距离x
* 因为起始点到折点的水平距离
* 不能大于起始点到结束的水平距离-40(留40放文字)
* 通过x可以确定折点的x坐标值
* 所以已知对边和角度,应该使用正切函数求邻边边长
* 这里却使用了正弦求了斜边
*/
x = Math.abs(maths(turingAngel) * diffH);
/**
* 如果始点到折点的水平距离
* 大于起始点到结束的水平距离-40(留40放文字)
* 减小角度,计算新折点
*/
while (x > (l - 40)) {
turingAngel /= 2;
x = maths(turingAngel) * (arr[i].endPoint[1] - arr[i].point[1]);
}
//通过x可以确定折点的x坐标值,y坐标就是结束点的y坐标
arr[i].turingPoint = [arr[i].point[0] + (left ? -x : x), arr[i].endPoint[1]]
}
}
}
}
//调用绘图函数
addPie({
data: [{
cost: 4.94,
category: '通讯',
color: "#e95e45",
}, {
cost: 4.78,
category: '服装美容',
color: "#20b6ab",
}, {
cost: 4.00,
category: '交通出行',
color: "#ef7340",
}, {
cost: 3.00,
category: '饮食',
color: "#eeb328",
}, {
cost: 49.40,
category: '其他',
color: "#f79954",
}, {
cost: 28.77,
category: '生活日用',
color: "#00a294",
}]
})
</script>
</body>
</html>
写在最后
因为是单个测试题目,所以没有用图表库。之所以没用SVG去实现,是因为之前只有接触过canvas。不过,后续真可以考虑使用svg来实现一下。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。