這回算是真明白了什麼叫"林子大了什麼鳥都有!"以前就有據說面試騙代碼的狀況,但也僅僅只是據說。這回是真親身遇到了。來來來,自帶小板凳,準備好瓜子。好好看看我被騙的經歷。順便也看看使用原生Canvas繪製餅圖,使用插件(好比Echart)也就分分鐘的事情,但多瞭解一些原生的東西,總不會有錯的。
正文開始.....html
仍是前段時間面試時發生的事情。3月21號晚八點,此時心態已處於第三階段(詳情可查看面試總結),忽然收到一封郵件,以下:
巧了,3月22有兩場面試,仍是兩家我以爲不錯的公司(南方+、愛範兒科技),我誤覺得就是這兩家其中一家的測試。
熬到22號兩點,餅圖卻是畫出來了,只是線條還有很大問題。當時的想法是經過計算位置,使用div來畫線條。這有兩個問題:一是沒法實現拆線;二是會不許。由於白天還有面試,因此就直接發了半成品過去,並詢問是什麼公司。對話以下:
竟然還沒約面試,只有想會是哪家公司呢?反正沒往騙代碼上想!3月23,繼續嘗試了一下,線條也經過canvas來繪製,解決了以前的兩個問題,還處理考慮擠一塊兒的需求,算得上已經實現需求。效果以下:
3月23晚上,發送過去。3月25晚上,收到回覆確是這樣:es6
你好,舒同窗。看了你的做品,可否再完善一下?由於這是仿支付寶的餅圖,因此但願是適配於移動設備的,另外APP裏的Webview好像要在6.0以上才支持es6語法,想把它轉成es5語法的,麻煩舒同窗了
到這裏我纔開始以爲不對勁。 爲啥要ES6轉ES5,又體現不了什麼技術能力,又不是實際使用;手機適配的問題,我這大小是可配置的並無寫死 。因此,立刻詢問是什麼公司。回覆以下:面試
林老師,測試題的目的應該就是了解一下應聘者的能力。我想,題目作到如今,我大概的代碼風格和技術能力,你應該瞭解了。
請問貴公司是?
而後。。。而後就再沒收到回覆。。。。
這裏我纔想到本身是否是被騙代碼了?可如今都不敢相信呀,這種代碼也有人騙麼?可若是不是,難道我這代碼寫得太low了,因此連個面試機會都拿不到?
因此,這裏貼上代碼,分享一下生Canvas繪製餅圖的想法,同時也讓你們幫忙看看,這樣的代碼能不能獲得一次面試機會呀![笑哭]*10
算法
稍微有些難的幾個點:canvas
下面是完整的代碼,有完成的註釋,代碼比註釋還多。segmentfault
<!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來實現一下。數組