示例代碼託管在: https://github.com/dashnowords/blogs/tree/master/Demo/canvas-echarts/line-chart博客園地址:《大史住在大前端》原創博文目錄javascript
華爲雲社區地址:【你要的前端打怪升級指南】html
使用原生canvasAPI
繪製折線圖。(柱狀圖截圖來自於百度Echarts官方示例庫【查看示例連接】。前端
通常折線圖是比較好實現的,只須要調用最基本的moveTo()
和lineTo( )
方法來繪製便可。平滑折線圖是一個難點,須要藉助貝塞爾曲線來進行繪製,此時每段曲線的控制點算法就成了核心難點,對原理感興趣的讀者能夠自行研究,本文直接利用算法的結論來進行實現。java
上一節中爲了以文字中點爲參考,在繪製x軸文字時採用的方法是用measureText( )
方法測量文字的寬度,而後偏移該距離的一半來達到效果,事實上咱們能夠經過設置textAlign
屬性爲'center'來達到以文字寬度方向中線爲參考點的繪製。git
context.textAlign = 'center'; context.drawText('Hello world',x ,y);
座標軸及繪圖參數設置請直接參見 【帶着canvas去流浪】(1)繪製柱狀圖 或在示例demo中查看。github
折線圖數據繪製示例代碼:web
/** * 繪製數據 */ function drawData(options) { let data = options.data;//數據點座標 let xLength = (options.chartZone[2] - options.chartZone[0])*0.96;//線段尾部留白後x軸長 let yLength = (options.chartZone[3] - options.chartZone[1])*0.98;//線段尾部留白後y軸長 let gap = xLength / options.xAxisLabel.length;//x軸間隙 //緩存從數據值到座標距離的比例因子 let yFactor =(options.chartZone[3] - options.chartZone[1]) *0.98 / options.yMax let activeX = 0;//記錄繪製過程當中當前點的座標 let activeY = 0;//記錄繪製過程當中當前點的y座標 context.strokeStyle = options.barStyle.color || '#1abc9c'; //02BAD4 context.strokeWidth = 2; context.beginPath(); context.moveTo(options.chartZone[0],options.chartZone[3]);//先將起點移動至0,0座標 for(let i = 0; i < data.length; i++){ activeX = options.chartZone[0] + (i + 1) * gap; activeY = options.chartZone[3] - data[i] * yFactor; context.lineTo(activeX, activeY); } context.stroke(); }
瀏覽器中可查看效果:算法
通常折線圖鏈接點部分很是生硬,更多的場景下咱們更但願曲線相對平滑,這時候就須要用到貝塞爾曲線來進行繪製,關於控制點的肯定可參考文章【怎樣肯定貝塞爾曲線的控制點】。編程
關於Canvas圖形繪製中座標系的一點提示canvas
爲了將參數集中,options對象中記錄的數據座標是相對於咱們本身繪製的座標系的,爲了使用canvas繪圖上下文中的貝塞爾曲線繪製函數,須要在繪製時將數據點的座標值轉換爲相對於canvas的座標值。
本文示例中採用的基本算法爲(爲復現繪製過程,直接採用面向過程的編程方式):
options.xAxisPos
數組中。options.xAxisPos
及options.data
中存儲的座標對就是數據點在可視座標中的座標點。transToCanvasCoord( )
處理將座標數值轉換爲相對於canvas座標系的數值。context.bezierCurveTo(c1x, c1y, c2x, c2y, dx dy)
函數來繪製擬合曲線。示例代碼爲:
/** * 三次貝塞爾曲線數據擬合 */ function drawDataWithCubicBezier(options) { //計算用於繪圖的數據點和控制點座標 let drawingPoints = calcControlPoints(options); //設置繪圖樣式 context.strokeStyle = options.barStyle.color || '#1abc9c'; //02BAD4 context.strokeWidth = 4; context.beginPath(); context.moveTo(options.chartZone[0],options.chartZone[3]);//先將起點移動至0,0座標 //逐個鏈接相鄰座標點 for(let i = 1; i < drawingPoints.length; i++){ context.bezierCurveTo(drawingPoints[i-1].cp1x, drawingPoints[i-1].cp1y, drawingPoints[i-1].cp2x, drawingPoints[i-1].cp2y, drawingPoints[i].dx, drawingPoints[i].dy); } //繪製線條 context.stroke(); } /** * 計算控制點 * 本例採用的算法,在每一個點計算時須要用到該點左側1個點和右側2個點的座標信息,影響邊界點的繪製,本例中採用的方法爲直接複製邊界點座標來簡化邊界點的座標求值。 */ function calcControlPoints(options) { let results = []; let y = options.data; let x = options.xAxisPos; //補充左值 y.unshift(y[0]); x.unshift(0); //補充右值 x.push(x[y.length - 1]); x.push(x[y.length - 1]); y.push(y[y.length - 1]); y.push(y[y.length - 1]); //計算用於繪製曲線的座標點及控制點座標值 for(let i = 1; i < y.length - 2; i++){ results.push({ dx:transToCanvasCoord(x[i], 'x'), dy:transToCanvasCoord(y[i]), cp1x:transToCanvasCoord(x[i] + (x[i+1] - x[i-1]) / 4,'x'), cp1y:transToCanvasCoord(y[i] + (y[i+1] - y[i-1]) / 4), cp2x:transToCanvasCoord(x[i+1] - (x[i+2] - x[i]) / 4,'x'), cp2y:transToCanvasCoord(y[i+1] - (y[i+2] - y[i]) / 4), }) } console.log(results) return results; } /** * 將座標轉換爲相對canvas的座標 * @param {[type]} coord 相對於可視座標系的值 * @param {[type]} flag 標記轉換x座標仍是y座標 */ function transToCanvasCoord(coord,flag) { let xLength = (options.chartZone[2] - options.chartZone[0])*0.96; let yLength = (options.chartZone[3] - options.chartZone[1])*0.98; let yFactor =(options.chartZone[3] - options.chartZone[1]) *0.98 / options.yMax; if (flag === 'x') { return coord + options.chartZone[0]; } return options.chartZone[3] - coord * yFactor; }
Tips:
- 在實際開發中,反覆出現的計算結果能夠經過閉包的形式緩存下來,例如本例中
transToCanvasCoord( )
函數中前半部分的計算實際上每次進行座標轉換時都會計算,這是不必的。- 上例中的算法在計算控制點時是以當前點
x[i]
計算鏈接x[i]
到x[i|+1]
時的控制點座標並進行保存,而繪圖時當循環變量爲i
時,drawingPoints[i]
中存儲的控制點座標,是鏈接至(x[ i+1 ],y[ i+1 ])
時的控制點,因此取用參數時須要錯一位。固然也能夠在計算drawingPoints
時直接按需存儲便可。
在瀏覽器中能夠看到曲線擬合的繪製效果:
面對大數據量的可視化展示或是在交互後出現重繪時,就極容易形成主線程阻塞,這是須要極力避免的。常見的處理思路有如下幾種:
webWorker
中,利用工做線程來處理計算密集型任務。筆者閱歷有限,並無生產環境的大數據量繪製的性能優化實戰經驗,能想到的就是上面幾點,很是歡迎有相關經驗的讀者交流討論。