【帶着canvas去流浪(4)】繪製散點圖

圖片描述

示例代碼託管在: http://www.github.com/dashnowords/blogs

博客園地址:《大史住在大前端》原創博文目錄javascript

華爲雲社區地址:【你要的前端打怪升級指南】html

[TOC]前端

一. 任務說明

使用原生canvasAPI繪製散點圖。(截圖以及數據來自於百度Echarts官方示例庫【查看示例連接】)。java

圖片描述

二. 重點提示

學習過折線圖的繪製後,若是數據點只有座標數據,則經過基本的座標轉換在對應的點上繪製出散點並不難實現。而在氣泡圖中,當咱們直接將百度Echarts示例中的數據拿來通過必定的線性縮小後做爲半徑直接繪製散點時,就會出現一些問題,數據集的範圍跨度較大,致使大部分點呈現後都很是小,這個時候就須要使用某種方法從真實數據值映射到散點圓半徑進行映射,來縮小它們之間的差別,不然一旦數據集中有一個偏離度較大的點,就會形成其餘點所對應的散點半徑都很大或者都很小,對數據呈現來講是不可取的。例如在下面的示例中,當使用幾種不一樣的映射方法來處理數據後,能夠看到繪製的散點圖是不同的。git

//求散點半徑時所使用的公式
//1.直接數值
r = value * 5 / 100000000;
//2.求對數
r = Math.log(value);
//3.求指數
r = Math.pow(value,0.4) / 100;

所繪製出的散點圖以下所示:github

圖片描述

座標映射的實現思路其實並不算複雜,它的概念能夠參考算法的時間複雜度來進行理解,挑選一個增加更快的映射函數來區分相近的點,或者挑選一個增加更慢的映射函數來減少大跨度數據之間的差別,在數據可視化中是很是實用的技巧。本文示例中的效果是筆者本身手動調的,若是要實現根據數據集自動挑選適當的映射函數,還須要設計一些計算方法,感興趣的讀者能夠自行研究。算法

三. 示例代碼

氣泡散點圖繪製示例代碼(座標軸的繪製過程在前述博文中已經出現過不少次,故再也不贅述,有須要的小夥伴能夠直接翻看這個系列以前的博文或者查看本篇的demo):canvas

/*數據點來自於百度Echarts官方示例庫,每一個數值分別表示[橫座標,縱座標,數值,國家,年份]
*[28604,77,17096869,'Australia',1990]
*/

/**
 * 繪製數據
 */
function drawData(options) {
    let data = options.data;//獲取數據集
    let xLength = (options.chartZone[2] - options.chartZone[0]);
    let yLength = (options.chartZone[3] - options.chartZone[1]);
    let gap = xLength / options.xAxisLabel.length;

    //遍歷兩個年份
    for(let i = 0; i < data.length ;i++){
        let x,y,r,c;
        context.fillStyle = options.colorPool[i];//從顏色池中選取顏色
        context.globalAlpha = 0.8;//爲避免點覆蓋,採起半透明繪製
        //遍歷各個數據點
        for(let j = 0; j < data[i].length ; j++){
            //計算座標
             x = options.chartZone[0] + xLength * data[i][j][0] / 70000;
             y = options.chartZone[3] - yLength * (data[i][j][4] - 55) / (85 - 55);

             //直接數值
             r = data[i][j][5] * 5 / 100000000;
             //求對數
             r = Math.log(data[i][j][6]);
             //開根號
             r = Math.pow(data[i][j][7],0.4) / 100;
             //繪製散點
             context.beginPath();
             context.arc(x, y , r , 0 , 2*Math.PI,false);
             context.fill();
             context.closePath();
        }
    }
}

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

圖片描述

四.散點hover交互效果的實現

4.1 基本算法

在散點圖上實現hover交互效果的基本算法以下:緩存

  1. canvas元素上監聽鼠標移動事件,將鼠標座標轉換爲canvas座標系的座標值。
  2. 遍歷數據點查看是否存在當前鼠標點距離某個數據中心點的距離小於其散點的繪製半徑,若是有則認爲鼠標在該點之上。
  3. 利用以前緩存的該點繪圖數據,調整繪圖樣式,增大數據點的繪圖半徑覆蓋式繪圖便可。
  4. 當鼠標距離任何數據點的距離都大於該點的繪圖半徑,或鼠標從一個hover數據點移動到另外一個hover點時,均須要調用一次resetHover( )方法清除以前的hover狀態。
  5. 爲了恢復hover前的狀態,可使用【離屏canvas技術】緩存首次繪圖後的結果,而後使用drawImage( )方法將對應區域恢復到hover前的狀態。

4.2 參考代碼

hover效果的關鍵代碼以下,完整示例代碼請在demo中獲取,或訪問【個人github倉庫】

/*簡單hover效果*/
canvas.onmousemove = function (event) {
    //轉換鼠標座標爲相對canvas
    let pos = {
        x: event.clientX - rect.left,
        y: event.clientY - rect.top
    }
    //獲取當前hover點座標
    let hoverPoint = checkHover(options, pos);
    /**
     * 若是當前有聚焦點
     */
    if (hoverPoint) {
        //若是當前點和上一次記錄的hover點是不一樣的點,則先調一次reset方法,而後把hover點更改成當前的點
        let samePoint = options.hoverData === hoverPoint ? true : false; 
        if (!samePoint) {
            resetHover();
            options.hoverData = hoverPoint;
        }
        //繪製當前點的hover狀態
        paintHover();
    } else{
        //第一次嘗試手動恢復
        // resetHover();
        //使用離屏canvas恢復
        resetHoverWithOffScreen();
    }
} 


/*檢測是否hover在散點之上*/
function checkHover(options,pos) {
    let data = options.paintingData;
    let found = false;
    for(let i = 0; i < data.length; i++){
        found = false;
        for(let j = 0; j < data[i].length; j++){
            if (Math.sqrt(Math.pow(pos.x - data[i][j].x , 2) + Math.pow(pos.y - data[i][j].y , 2)) < data[i][j].r) {
                found = data[i][j];
                break;
            }
        }
        if (found) break;
    }
    return found;
}

/*繪製hover狀態*/
function paintHover() {
    let {x,y,r,c} = options.hoverData;
    let step = 0.5;
    context.globalAlpha = 1;
    context.fillStyle = c;
    //逐幀增長hover點的繪圖半徑,從新繪製hover狀態的散點
    for(let i = 0 ; i < 30; i++){
       context.beginPath();
       context.arc(x,y,r + i * step, 0 , 2*Math.PI,false);
       context.fill();
       context.closePath();
    }
}

/*首次嘗試的取消高亮狀態的函數*/
function resetHover() {
    if (!options.hoverData) return;
    let {x,y,r,c} = options.hoverData;
    let step = 0.5;
    context.globalAlpha = 1;
    for(let i = 29; i>0; i--){
       context.save();
       //繪製外圓範圍
       context.beginPath();
       context.arc(x,y,r + 30 * step, 0 , 2*Math.PI,false);
       context.closePath();
       //設置剪裁區域
       context.clip();
       //用全局背景色繪製剪裁區背景
       context.globalAlpha = 1;
       context.fillStyle = options.globalGradient;
       context.fill();
       //繪製內圓
       context.beginPath();
       context.arc(x,y,r + i * step, 0 , 2*Math.PI,false);
       context.closePath();
       context.fillStyle = c;
       context.globalAlpha = 0.8;
       //填充內圓
       context.fill();
       context.restore();
    }
    options.hoverData = null;
    console.log('清除hover效果');
}

//利用離屏canvas恢復hover前的狀態
function  resetHoverWithOffScreen() {
    if (!options.hoverData) return;
    let {x,y,r,c} = options.hoverData;
    let step = 0.5;
    context.globalAlpha = 1;
    for(let i = 29; i>0; i--){
       context.save();
       //將hover狀態下數據點圓所在的正方形範圍恢復爲hover前的狀態
       context.drawImage(canvas2, x - r - 30 * step, y - r - 30 * step , 2 * (r + 30 * step),2*(r + 30 * step),x - r - 30 * step, y - r - 30 * step , 2*(r + 30 * step),2*(r + 30 * step));
       //繪製內圓
       context.beginPath();
       context.arc(x,y,r + i * step, 0 , 2*Math.PI,false);
       context.closePath();
       context.fillStyle = c;
       context.globalAlpha = 0.8;
       //填充內圓
       context.fill();
       context.restore();
    }
    options.hoverData = null;
    console.log('清除hover效果');
}

4.3 Demo中的小問題

  1. 爲了簡化代碼,demo中的一些繪圖數據並無參數化,而是採起直接寫死的形式放在代碼裏,尤爲是逐幀繪圖的代碼,通常開發中此處都會配合動畫來進行實現。
  2. 爲了重置某個數據點的hover狀態,筆者最初的實現思路是在每一幀中,使用context.clip( )方法裁切出繪圖區域,先用全局背景繪製出背景圖,縮小數據點半徑,而後再繪製數據點,直到半徑縮小至hover前的值。但在實現後發現這種方式存在一個問題,那就是數據點之間出現重疊時,若是隻是簡單地背景重繪,就會將部分重疊區域清除掉,形成其餘數據點沒法復原,以下圖所示:

圖片描述

因此最終採用離屏canvas的方法,將初次繪製後的數據點先暫存下來,而後在清除hover狀態時,使用context.drawImage( )方法將有關區域的數據複製粘貼過來,以替代原來的使用背景圖填充該區域的作法,這樣就能夠在數據點之間有重疊時重現hover前的狀態。

相關文章
相關標籤/搜索