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

来源:https://www.cnblogs.com/dashnowords/archive/2019/03/24/10590032.html
-Advertisement-
Play Games

示例代碼托管在: "http://www.github.com/dashnowords/blogs" 博客園地址: "《大史住在大前端》原創博文目錄" 華為雲社區地址: "【你要的前端打怪升級指南】" [TOC] 一. 任務說明 使用原生 繪製散點圖。(截圖以及數據來自於百度Echarts官方示例庫 ...


目錄

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

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

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

一. 任務說明

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

二. 重點提示

學習過折線圖的繪製後,如果數據點只有坐標數據,則通過基本的坐標轉換在對應的點上繪製出散點並不難實現。而在氣泡圖中,當我們直接將百度Echarts示例中的數據拿來經過一定的線性縮小後作為半徑直接繪製散點時,就會出現一些問題,數據集的範圍跨度較大,導致大部分點呈現後都非常小,這個時候就需要使用某種方法從真實數據值映射到散點圓半徑進行映射,來縮小它們之間的差異,否則一旦數據集中有一個偏離度較大的點,就會造成其他點所對應的散點半徑都很大或者都很小,對數據呈現來說是不可取的。例如在下麵的示例中,當使用幾種不同的映射方法來處理數據後,可以看到繪製的散點圖是不一樣的。

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

所繪製出的散點圖如下所示:

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

三. 示例代碼

氣泡散點圖繪製示例代碼(坐標軸的繪製過程在前述博文中已經出現過很多次,故不再贅述,有需要的小伙伴可以直接翻看這個系列之前的博文或者查看本篇的demo):

/*數據點來自於百度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][1] - 55) / (85 - 55);

             //直接數值
             r = data[i][j][2] * 5 / 100000000;
             //求對數
             r = Math.log(data[i][j][2]);
             //開根號
             r = Math.pow(data[i][j][2],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前的狀態。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 準備 語言:TypeScript 工具:Visual Studio Code 演示:Audio Visualiazer 小明告訴我,他希望打開一個網頁,立即聽到他喜歡的音樂,如果有視覺特效就更棒了。 第一節 本地音頻 “可是我的電腦里沒有 MP3 文件!” “為什麼需要打開一個本地文件?”小明問。 ...
  • webpack 什麼是webpack 官方解釋:webpack 是一個現代 JavaScript 應用程式的靜態模塊打包器(module bundler)。什麼意思呢?就是可以把你開發項目時用到的所有資源全部打包成一個js文件,然後項目在上線階段引入這個js文件就行了,它會自動幫你展出你... ...
  • js的運算符 算術運算符 加法 + 減法 - 乘法 * 除法 / 餘數 % number類型的和number類型的 number類型和boolean類型(true--1,false--0) number類型和string類型(+,-,*,/) string類型和string類型的數字(+,-,*,/ ...
  • 問題: 在網頁的發展歷程中,發現網頁不能對用戶的數據進行自動校驗,和提供一些特效。 解決: 使用javascript。 作用 可以讓網頁和用戶進行直接簡單的交互。 可以讓網頁製作特效和動畫。 聲明js代碼域 1.<script type="text/javascript"></script> 2.< ...
  • 本文介紹了Canvas引入跨域的圖片導致toDataURL()報錯的問題的解決,分享給大家,具體如下: 【場景】 用戶打開網頁,則請求騰訊COS(圖片伺服器)上的圖片js代碼。使用canvas繪圖。 然後,用戶可以重新選擇圖片、裁剪、上傳。 【問題】 圖片首次載入,選擇新圖片後裁剪、繪製都沒有問題。 ...
  • 以下為你簡略介紹javaScript語法中創建對象的內容。 主要包括:字面量模式創建、調用系統構造函數創建、工廠模式創建、自定義構造函數創建、原型模式創建共五種。 ...
  • 有一天我們的UI設計師找到我說,要把頁面中我自己用程式寫的動畫,換成他們給的json動畫,原因是有的動畫很複雜,自己寫起來達不到他們的預期效果(寫到這裡我突然想到一個問題,這麼複雜的動畫為什麼不使用gif。。。。坐我對面的安卓開發小哥答因為gif播放的時候可能質量不高不流暢,好吧我信了) 我:??? ...
  • 在理解javascript的this之前,首先先瞭解一下作用域。 作用域分為兩種: 詞法作用域和動態作用域的區別是:詞法作用域是在寫代碼或定義時確定的;動態作用域是在運行時確定的。 this的綁定規則 this是在調用時被綁定,取決於函數的調用位置。由此可以知道,一般情況下(非嚴格模式下),this ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...