【帶着canvas去流浪】(2)繪製折線圖

圖片描述

示例代碼託管在: 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

3.1 通常折線圖

折線圖數據繪製示例代碼: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();
    }

瀏覽器中可查看效果:算法

圖片描述

3.2 用貝塞爾曲線繪製平滑折線圖

通常折線圖鏈接點部分很是生硬,更多的場景下咱們更但願曲線相對平滑,這時候就須要用到貝塞爾曲線來進行繪製,關於控制點的肯定可參考文章【怎樣肯定貝塞爾曲線的控制點】編程

關於Canvas圖形繪製中座標系的一點提示canvas

爲了將參數集中,options對象中記錄的數據座標是相對於咱們本身繪製的座標系的,爲了使用canvas繪圖上下文中的貝塞爾曲線繪製函數,須要在繪製時將數據點的座標值轉換爲相對於canvas的座標值。

本文示例中採用的基本算法爲(爲復現繪製過程,直接採用面向過程的編程方式):

  1. 繪製x軸文字時記錄相對於可視座標系的座標值,並存儲於options.xAxisPos數組中。
  2. 因爲數據點是對齊x軸文字來繪製的,因此options.xAxisPosoptions.data中存儲的座標對就是數據點在可視座標中的座標點。
  3. 遍歷數據座標點,計算使用三次貝塞爾曲線鏈接相鄰點時的控制點的座標,此時控制點座標是相對於可視座標系的,再通過座標變換函數transToCanvasCoord( )處理將座標數值轉換爲相對於canvas座標系的數值。
  4. 使用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:

  1. 在實際開發中,反覆出現的計算結果能夠經過閉包的形式緩存下來,例如本例中transToCanvasCoord( )函數中前半部分的計算實際上每次進行座標轉換時都會計算,這是不必的。
  2. 上例中的算法在計算控制點時是以當前點x[i]計算鏈接x[i]x[i|+1]時的控制點座標並進行保存,而繪圖時當循環變量爲i時,drawingPoints[i]中存儲的控制點座標,是鏈接至(x[ i+1 ],y[ i+1 ])時的控制點,因此取用參數時須要錯一位。固然也能夠在計算drawingPoints時直接按需存儲便可。

在瀏覽器中能夠看到曲線擬合的繪製效果:

圖片描述

四. 大數據量場景

面對大數據量的可視化展示或是在交互後出現重繪時,就極容易形成主線程阻塞,這是須要極力避免的。常見的處理思路有如下幾種:

  1. 數據採樣並從新擬合以減小繪圖數據點,也就是從源數據到繪圖數據進行映射,畢竟顯示器分辨率就那麼高,過大的數據量加劇了數據損失,卻並不必定能在視覺和效果上得到對應的提高。
  2. 將大數據量及耗時的處理髮送至webWorker中,利用工做線程來處理計算密集型任務。
  3. 將同步的繪圖任務分解爲若干個異步的子任務來執行,避免阻塞主線程。

筆者閱歷有限,並無生產環境的大數據量繪製的性能優化實戰經驗,能想到的就是上面幾點,很是歡迎有相關經驗的讀者交流討論。

相關文章
相關標籤/搜索