示例代碼托管在: "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交互效果的基本演算法如下:
- 在
canvas
元素上監聽滑鼠移動事件,將滑鼠坐標轉換為canvas坐標系的坐標值。 - 遍曆數據點查看是否存在當前滑鼠點距離某個數據中心點的距離小於其散點的繪製半徑,如果有則認為滑鼠在該點之上。
- 利用之前緩存的該點繪圖數據,調整繪圖樣式,增大數據點的繪圖半徑覆蓋式繪圖即可。
- 當滑鼠距離任何數據點的距離都大於該點的繪圖半徑,或滑鼠從一個hover數據點移動到另一個hover點時,均需要調用一次
resetHover( )
方法清除之前的hover狀態。 - 為了恢復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中的小問題
為了簡化代碼,demo中的一些繪圖數據並沒有參數化,而是採取直接寫死的形式放在代碼里,尤其是逐幀繪圖的代碼,一般開發中此處都會配合動畫來進行實現。
為了重置某個數據點的hover狀態,筆者最初的實現思路是在每一幀中,使用
context.clip( )
方法裁切出繪圖區域,先用全局背景繪製出背景圖,縮小數據點半徑,然後再繪製數據點,直到半徑縮小至hover前的值。但在實現後發現這種方式存在一個問題,那就是數據點之間出現重疊時,如果只是簡單地背景重繪,就會將部分重疊區域清除掉,造成其他數據點無法複原,如下圖所示:
所以最終採用離屏canvas的方法,將初次繪製後的數據點先暫存下來,然後在清除hover狀態時,使用context.drawImage( )
方法將有關區域的數據複製粘貼過來,以替代原來的使用背景圖填充該區域的做法,這樣就可以在數據點之間有重疊時重現hover前的狀態。