上篇我們用Canvas製作了windows繪圖軟體。本篇我們將柱狀圖,扇形圖,環形圖三類圖表使用面向對象的方式寫出來。
第三篇我們完成過面向對象的柱狀圖,便於理解,所以在第三篇對對象的方法,拆分的很細緻,顯而易見的就是代碼的行數增加了,我們在實際開發過程中,只要把核心部分注釋出來,能夠讓自己和同事看懂就行。
這節篇文章中我們專門說下面向對象的圖表製作,這個和實際工作場景是一樣的。
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width_=device-width, initial-scale=1, user-scalable=no"> <title>面向對象圖表</title> <style> .countdown{ border:2px solid green; } </style> </head> <body> <div style="width:600px;height:300px;"> <canvas id="canvas1" width="600px" height="300px"></canvas> </div> <div style="width:600px;height:300px;"> <canvas id="canvas2" width="600px" height="300px"></canvas> </div> <div style="width:600px;height:300px;"> <canvas id="canvas3" width="600px" height="300px"></canvas> </div> <div class="countdownwrap"></div> <script src="fchart.js"></script> <script> var data = { 阿里: {figure: 100, color: #1abc9c}, 騰訊: {figure: 180, color: #2ecc71}, 百度: {figure: 40, color: #e74c3c}, 京東: {figure: 20, color: #f1c40f}, } ? var opt1 = { data: data, align: left, wrapper: document.getElementById(canvas1), r: 200 };
var fcs1 = new fchart(opt1); ? var opt2 = { data: data, align: right, wrapper: document.getElementById(canvas2), r: 200, type: ringchart }; var fcs2 = new fchart(opt2); ? var opt3 = { data: data, wrapper: document.getElementById(canvas3), type: barchart }; var fcs3 = new fchart(opt3); </script> </body> </html>
其實很簡單的,重點就兩部分,數據+圖表配置,就是這裡
var data = { 阿里: {figure: 100, color: #1abc9c}, 騰訊: {figure: 180, color: #2ecc71}, 百度: {figure: 40, color: #e74c3c}, 京東: {figure: 20, color: #f1c40f}, } ? var opt1 = { data: data, align: left, wrapper: document.getElementById(canvas1), r: 200 };
知道了這些,我們考慮下,fchart.js怎麼寫。
既然是面向對象方式,而且上面我們也有
var fcs1 = new fchart(opt1);
啥也不用說了,開始先把架子搭出來。
function fchart(opt) { this.canvas = opt.wrapper; // canvas this.ctx = opt.wrapper.getContext(2d); // canvas context this.type = opt.type || piechart; ? this.data = new fdata(opt.data); this.data.total = utils.getTotal(this.data.raw); utils.sort(this.data.raw, this.data.sorted); utils.getPercentage(this.data.sorted, this.data.percentage, this.data.total);
this.getWrapperSize(); this.draw(opt); }
fchart.prototype.draw = function(opt) { var obj; switch(this.type){ case ringchart: obj = new RingChart(opt, this); break; case barchart: obj = new BarChart(opt, this); break; default: obj = new PieChart(opt, this); break; } ? obj.draw(); };
這樣大家就看明白了,其實就是通過選擇傳入不同的type 去new對應圖表的對象。我們一個明白了,其它就都明白了,其實都是一個套路。因為我們前面已經說過柱狀圖,大家理解容易些,我們就深入分析下柱狀圖。
function BarChart(opt, fc) { this.ctx = fc.ctx; this.canvas = fc.canvas; this.data = fc.data; }
構造函數,沒什麼好看的,無非就是傳入需要的各種數據和畫布,重點我們看看方法。
BarChart.prototype._drawAxisYLabel = function(x, y, figure) { var ctx = this.ctx; ctx.font = "30px -apple-system-font, "Helvetica Neue", Helvetica, STHeiTi, sans-serif"; var txt = figure; ctx.fillStyle = "#000000"; ctx.fillText(figure, x - ctx.measureText(txt).width - 10, y + 15); }
這個也很容易,無非就是位置、內容,fillStyle、fillText都是老朋友了。
BarChart.prototype._drawAxis = function(ctx) { this.centerX = this.canvas.width / 2; this.centerY = this.canvas.height / 2; ? // 80%的畫布長寬作為坐標軸 this.axisXLen = this.canvas.width * 0.8; this.axisYLen = this.canvas.height * 0.8; ? this.axisZeroPointX = this.centerX - this.axisXLen / 2; this.axisZeroPointY = this.centerY + this.axisYLen / 2; ? this.axisDesPointX = this.axisZeroPointX + this.axisXLen; this.axisDesPointY = this.axisZeroPointY - this.axisYLen; ? // 畫坐標軸 ctx.beginPath(); // 回到原點 ctx.moveTo(this.axisZeroPointX, this.axisZeroPointY); // ctx.lineTo(this.axisDesPointX, this.axisZeroPointY); ctx.moveTo(this.axisZeroPointX, this.axisZeroPointY); ctx.lineTo(this.axisZeroPointX, this.axisDesPointY); ctx.lineWidth = 1; ctx.closePath(); ctx.stroke();
// Y軸數值每節高度 this.axisYPerLen = this.axisYLen / 4; ? // 找最大值 var max = 0; this.arrLen = 0; for (key in this.data.raw) { if (this.data.raw[key].figure > max) { max = this.data.raw[key].figure; } this.arrLen++; } ? // Y軸數值 this.yValue = Math.ceil(max / 4); var bitArr = [];
// 計算端值 this.accu = 0; for (var i = 5; i > 0 ; i =i + 5) { this.accu = i * 4; if (this.accu > this.yValue) { break; } } // 畫Y坐標軸端點及數值 ctx.beginPath(); ? for (var i = 0; i <= 4; i++) { var x = this.axisZeroPointX - 5; var y = this.axisZeroPointY - i * this.axisYPerLen - 5; this._drawAxisYLabel(x, y, this.accu * i); ctx.fillRect(x, y, 10, 10); } ctx.closePath(); };
我們之前柱狀圖的時候,已經繪畫坐標了,只是改了一種寫法,包在對象裏,沒有什麼理由不會,當然了剩下的就是數學的一通算,麻煩,但是不複雜。
BarChart.prototype._drawBar = function() { var ctx = this.ctx; // x軸上放bar的允許長度 var barAxisW = this.axisXLen * 0.8; var barW = barAxisW / this.arrLen * 0.7; var gap = barAxisW / this.arrLen * 0.3; var x = this.axisZeroPointX - (this.axisYLen - barAxisW) / 2
for (key in this.data.raw) { ctx.beginPath(); var barH = this.data.raw[key].figure / this.accu * this.axisYPerLen; var y = this.axisZeroPointY - barH; ctx.fillStyle = this.data.raw[key].color; ctx.fillRect(x, y, barW, barH); x += (barW + gap); ctx.closePath(); } };
全是數學計算和基礎知識,如果大家覺得喫力,就按照你自己的畫法也行,但是至少得保證能夠用面向對象的把柱狀圖畫出來。
BarChart.prototype.draw = function() { var ctx = this.ctx; this._drawAxis(ctx); this._drawBar(); };
大家看到裡面,其實就調用了話坐標和畫柱子的方法,其實就是3,4定義的那些嘛。
其實核心代碼並不複雜。
看一看,我們如何把html裡面的配置和數據搞到庫裡面,並處理數據和配置的。
html頁面裡面,我們看下opt3變數以及其依賴:
var data = { 阿里: {figure: 100, color: #1abc9c}, 騰訊: {figure: 180, color: #2ecc71}, 百度: {figure: 40, color: #e74c3c}, 京東: {figure: 20, color: #f1c40f}, } var opt3 = { data: data, wrapper: document.getElementById(canvas3), type: barchart }; var fcs3 = new fchart(opt3);
第一個是數據不用說了,第二個是畫到哪裡。第三個是畫什麼類型的圖表。
關鍵點 new fchart(opts),回到庫裏我們看看。
繼續看,data被哪些方法用了,肯定是畫柱狀圖和坐標。
重點在這:
for (key in this.data.raw) { ctx.beginPath(); var barH = this.data.raw[key].figure / this.accu * this.axisYPerLen; var y = this.axisZeroPointY - barH; ctx.fillStyle = this.data.raw[key].color; ctx.fillRect(x, y, barW, barH); x += (barW + gap); ctx.closePath(); }
這裡我們就看到了,數據通過for in循環用到了對應的fillRect裡面。
到此為止,我們深入的分析了:
1.面向對象圖表庫怎麼使用的。
2.根據用法,我們如何創建構造函數
3.構造函數如何調用畫坐標和柱圖的方法。
4.柱圖方法和畫坐標方法實現。
5.html頁面數據和配置項如何傳到庫裡面進行使用。
其實其他的圖跟柱狀圖畫法套路一模一樣,無非是調用了對應的畫圖方法:
整體庫最終完整如下fchart.js:
var utils = { ? sort: function(raw, sorted) { var sortTable = []; for (key in raw) { sortTable.push([key, raw[key].figure]); } sortTable.sort(function(a, b) {return b[1] - a[1]}); ? // resume other data field value for (key in sortTable) { var index = sortTable[key][0]; sorted[index] = {}; for (k in raw[index]) { sorted[index][k] = raw[index][k]; } } }, ? getTotal: function(raw) { var total = 0; for (key in raw) { total += raw[key].figure; } return total; }, ? getPercentage: function(sorted, percentage, total) { for (key in sorted) { percentage[key] = sorted[key].figure / total; } }, ? getRadius: function(deg) { return deg / 180 * Math.PI; }, ? }; ? /** * BarChart */ function BarChart(opt, fc) { this.ctx = fc.ctx; this.canvas = fc.canvas; this.data = fc.data; } ? BarChart.prototype.draw = function() { var ctx = this.ctx; this._drawAxis(ctx); this._drawBar(); }; ? BarChart.prototype._animateDraw = function(drawFunc) { drawFunc.call(self); }; ? BarChart.prototype._drawAxisYLabel = function(x, y, figure) { var ctx = this.ctx; ctx.font = "30px -apple-system-font, "Helvetica Neue", Helvetica, STHeiTi, sans-serif"; var txt = figure; ctx.fillStyle = "#000000"; ctx.fillText(figure, x - ctx.measureText(txt).width - 10, y + 15); } ? BarChart.prototype._drawBar = function() { var ctx = this.ctx; // x軸上放bar的允許長度 var barAxisW = this.axisXLen * 0.8; var barW = barAxisW / this.arrLen * 0.7; var gap = barAxisW / this.arrLen * 0.3; var x = this.axisZeroPointX - (this.axisYLen - barAxisW) / 2
for (key in this.data.raw) { ctx.beginPath(); var barH = this.data.raw[key].figure / this.accu * this.axisYPerLen; var y = this.axisZeroPointY - barH; ctx.fillStyle = this.data.raw[key].color; ctx.fillRect(x, y, barW, barH); x += (barW + gap); ctx.closePath(); } }; ? BarChart.prototype._drawAxis = function(ctx) { this.centerX = this.canvas.width / 2; this.centerY = this.canvas.height / 2; ? // 80%的畫布長寬作為坐標軸 this.axisXLen = this.canvas.width * 0.8; this.axisYLen = this.canvas.height * 0.8; ? this.axisZeroPointX = this.centerX - this.axisXLen / 2; this.axisZeroPointY = this.centerY + this.axisYLen / 2; ? this.axisDesPointX = this.axisZeroPointX + this.axisXLen; this.axisDesPointY = this.axisZeroPointY - this.axisYLen; ? // 畫坐標軸 ctx.beginPath(); // 回到原點 ctx.moveTo(this.axisZeroPointX, this.axisZeroPointY); // ctx.lineTo(this.axisDesPointX, this.axisZeroPointY); ctx.moveTo(this.axisZeroPointX, this.axisZeroPointY); ctx.lineTo(this.axisZeroPointX, this.axisDesPointY); ctx.lineWidth = 1; ctx.closePath(); ctx.stroke();
// 計算端值 this.accu = 0; for (var i = 5; i > 0 ; i =i + 5) { this.accu = i * 4; if (this.accu > this.yValue) { break; } } ? // 畫Y坐標軸端點及數值 ctx.beginPath(); ? for (var i = 0; i <= 4; i++) { var x = this.axisZeroPointX - 5; var y = this.axisZeroPointY - i * this.axisYPerLen - 5; this._drawAxisYLabel(x, y, this.accu * i); ctx.fillRect(x, y, 10, 10); } ? ctx.closePath(); ? }; ? ? /** * PieChart */ function PieChart(opt, fc) { this.ctx = fc.ctx; this.canvas = fc.canvas; this.data = fc.data; this.cx = opt.cx || 100; // piechart x coordinate this.cy = opt.cy || 100; // piechart y coordinate this.r = opt.r || 100; this.lineWidth = opt.lineWidth || 50; this.align = opt.align || center; } ? PieChart.prototype.draw = function() { this._align(); this._animateDraw(this._drawPieChart); }; ? PieChart.prototype._animateDraw = function(drawFunc) { var ctx = this.ctx; var startDeg = -90; var incre = 30; var self = this; ? var dr = setInterval(function() { ctx.save();
ctx.clearRect(0,0,600,600); drawFunc.call(self, startDeg); startDeg += incre; ? if (startDeg >= 300) { clearInterval(dr); PieChart.prototype._drawLabel.call(self); }
ctx.restore(); }, 30); }; ? // draw piechart PieChart.prototype._drawPieChart = function(startDeg){ var ctx = this.ctx; ? // var startDeg = -90; // top degree is -90 degree var deg = 0; // start degree var endDeg = 0; // end degree var startRadius = 0; // start radius var endRadius = 0; // end radius var startPos = {x: this.cx, y: this.r - this.y}; // start drawing position var endPos = {x: 0, y: 0}; // end line position this.currentDeg = 0; //accumulated degrees for drawing icon ? for (key in this.data.percentage) { this.data.info[key] = {}; deg = this.data.percentage[key] * 360; if (deg === 0) { continue; } endDeg = startDeg + deg; startRadius = utils.getRadius(startDeg); endRadius = utils.getRadius(endDeg); //store info this.data.info[key].deg = deg; this.data.info[key].startDeg = startDeg; this.data.info[key].endDeg = endDeg; this.data.info[key].startRadius = startRadius; this.data.info[key].endRadius = endRadius; ? // drawing pichart ctx.beginPath(); ctx.moveTo(this.cx, this.cy); ctx.lineTo(startPos.x, startPos.y); ctx.arc(this.cx, this.cy, this.r, startRadius, endRadius, 0, 0); this._getPos(endDeg, endPos, this.r); ctx.fillStyle = this.data.sorted[key].color; ctx.fill(); ctx.closePath(); ? ? // next sector data startDeg = endDeg; startPos.x = endPos.x; startPos.y = endPos.y; ? } }; ? // get end line of sector position PieChart.prototype._getPos = function(currentDeg, lineToPos, r) { var radius = 0; var deg = 0; currentDeg += 90; ? if (currentDeg > 360) { currentDeg -= 360; } ? if (currentDeg <= 90) { deg = 90 - currentDeg; radius = utils.getRadius(deg); lineToPos.x = this.cx + Math.cos(radius) * r; lineToPos.y = this.cy - Math.sin(radius) * r; } else if (currentDeg <= 180) { deg = currentDeg - 90; radius = utils.getRadius(deg); lineToPos.x = this.cx + Math.cos(radius) * r; lineToPos.y = this.cy + Math.sin(radius) * r; } else if (currentDeg <= 270) { deg = 270 - currentDeg; radius = utils.getRadius(deg); lineToPos.x = this.cx - Math.cos(radius) * r; lineToPos.y = this.cy + Math.sin(radius) * r; } else if (currentDeg <= 360) { deg = currentDeg - 270; radius = utils.getRadius(deg); lineToPos.x = this.cx - Math.cos(radius) * r; lineToPos.y = this.cy - Math.sin(radius) * r; } }; ? PieChart.prototype._align = function() { switch(this.align){ case left: this.cx = this.r + this.lineWidth; break; case right: this.cx = this.canvas.clientWidth - this.r - this.lineWidth; break; default: this.cx = this.canvas.clientWidth / 2; break; } ? this.cy = this.canvas.clientHeight / 2; }; ? // draw label and data PieChart.prototype._drawLabel = function() { var ctx = this.ctx; switch(this.align){ case left: var x = this.cx + this.r + 60; break; case right: var x = 60; break; default: return; break; }
var y = this.cy - this.r; ? for (key in this.data.sorted) { ctx.fillStyle = this.data.sorted[key].color; ctx.fillRect(x, y, 30, 30); PieChart.prototype._drawText.call(this, x, y, key); y += 60; } }; ? PieChart.prototype._drawText = function(x, y, key) { var ctx = this.ctx; ctx.font = "30px -apple-system-font, "Helvetica Neue", Helvetica, STHeiTi, sans-serif"; ctx.fillStyle = "#000000"; var showFigure = Math.round(this.data.percentage[key] * 10000) / 100; ctx.fillText(key + + showFigure + %, x + 40, y + 25); }; ? /** * RingChart */ function RingChart(opt, fc) { this.ctx = fc.ctx; this.canvas = fc.canvas; this.data = fc.data; this.cx = opt.cx || 100; // piechart x coordinate this.cy = opt.cy || 100; // piechart y coordinate this.r = opt.r || 100; this.lineWidth = opt.lineWidth || 50; // piechart radius this.align = opt.align || center; } ? RingChart.prototype.draw = function() { PieChart.prototype._align.call(this); PieChart.prototype._animateDraw.call(this, this._drawRingChart); }; ? RingChart.prototype._drawRingChart = function(startDeg) { var ctx = this.ctx; // var startDeg = -90; var deg = 0; var endDeg = 0; var startRadius = 0; var endRadius = 0; var startPos = {x: this.cx, y: this.r - this.y}; // start drawing position var endPos = {x: 0, y: 0}; // end line position this.currentDeg = 0; //accumulated degrees for drawing icon ? for (key in this.data.percentage) { this.data.info[key] = {}; deg = this.data.percentage[key] * 360; if (deg === 0) { continue; } endDeg = startDeg + deg; startRadius = utils.getRadius(startDeg); endRadius = utils.getRadius(endDeg); //store info this.data.info[key].deg = deg; this.data.info[key].startDeg = startDeg; this.data.info[key].endDeg = endDeg; this.data.info[key].startRadius = startRadius; this.data.info[key].endRadius = endRadius; ? // drawing pichart ctx.beginPath(); ctx.strokeStyle = this.data.sorted[key].color; ctx.arc(this.cx, this.cy, this.r, startRadius, endRadius, 0); ctx.lineWidth = this.lineWidth; ctx.stroke(); ctx.closePath(); ? // next sector data startDeg = endDeg; startPos.x = endPos.x; startPos.y = endPos.y; ? } ? }; ? function fdata(data) { this.raw = data; this.sorted = {}; this.info = {}; this.total = 0; this.percentage = {}; } ? function fchart(opt) { this.canvas = opt.wrapper; // canvas this.ctx = opt.wrapper.getContext(2d); // canvas context this.type = opt.type || piechart; ? this.data = new fdata(opt.data); this.data.total = utils.getTotal(this.data.raw); utils.sort(this.data.raw, this.data.sorted); utils.getPercentage(this.data.sorted, this.data.percentage, this.data.total);
this.getWrapperSize(); this.draw(opt); } ? // get wrapper size and set canvas size fchart.prototype.getWrapperSize = function() { this.canvas.width = this.canvas.parentNode.clientWidth * 2; this.canvas.height = this.canvas.parentNode.clientHeight * 2; this.canvas.style.cssText = -webkit-transform: translateX(- + (this.canvas.width / 4) + px) scale(0.5);-webkit-transform-origin: 50% 0; }; ? // draw canvas fchart.prototype.draw = function(opt) { var obj; switch(this.type){ case ringchart: obj = new RingChart(opt, this); break; case barchart: obj = new BarChart(opt, this); break; default: obj = new PieChart(opt, this); break; } ? obj.draw(); };
html部分使用 :
其中一些方法是輔助方法,而另外一些事為了功能完善的,而不是畫圖的核心方法,大家不用管,比如_animateDraw方法是實現圖表動畫效果的。
通過本文大家重點學習面向對象畫圖的組織方式,一旦掌握了,在工作中熟練應用,就能夠形成自己的代碼風格和和一套自己靈活使用的類庫。