位置數據是鏈接線上線下的重要信息資源,在前端藉助於圖形化的手段將數據有效呈現是進行數據分析的重要手段。基於此,咱們開發了基於地圖的數據可視化組件,以附加庫的形式加入到JSAPI中,目前主要包括熱力圖、散點圖、區域圖、遷徙圖。前端
熱力圖是以顏色來表現數據強弱大小及分佈趨勢的可視化類型,如上圖左上角所示,熱力圖可應用於人口密度分析、活躍度分析等。呈現熱力圖的數據主要包括離散的座標點及對應的強弱數值。canvas
本文只關心熱力圖的基礎實現,不管你是用於地圖,仍是網頁焦點分析仍是其餘場景,均需將對應場景的座標轉化爲Canvas畫布上的二維座標,最終咱們須要的數據格式以下:瀏覽器
// x, y 表示二維座標; value表示強弱值
var data = [
{x: 471, y: 277, value: 25},
{x: 438, y: 375, value: 97},
{x: 373, y: 19, value: 71},
{x: 473, y: 42, value: 63},
{x: 463, y: 95, value: 97},
{x: 590, y: 437, value: 34},
{x: 377, y: 442, value: 66},
{x: 171, y: 254, value: 20},
{x: 6, y: 582, value: 64},
{x: 387, y: 477, value: 14},
{x: 300, y: 300, value: 80}
];
複製代碼
讓咱們從結果來反推咱們應該如何實現熱力圖。 性能優化
咱們能夠直觀的感覺到:根據咱們的直觀感覺,咱們須要作的是:bash
以上步驟中須要注意的是第2步,咱們並不是直接以強度色譜填充圓形,由於這樣獲得的顏色是3個維度的,在疊加的時候不是線性的。本文選取了alpha
即顏色中的透明度做爲表示強弱的維度,你也能夠選取r
或者g
或者其餘,後文會解釋選擇alpha
的好處。函數
Canvas 中繪製弧線或者圓形可使用arc()
方法:工具
arc(x, y, radius, startAngle, endAngle, anticlockwise)
複製代碼
x
和y
對應到數據的座標,radius
可自由設置,startAngle
和endAngle
表示起止角度,分別取0
和2 * Math.PI
,anticlockwise
表示是否逆時針,可不設置。性能
Canvas 中可使用canvasGradient
對象建立漸變色,分爲直線漸變createLinearGradient(x1, y1, x2, y2)
和徑向漸變createRadialGradient(x1, y1, r1, x2, y2, r2)
,咱們採用後者。建立徑向漸變色須要定義兩個圓,顏色在兩個圓之間的區域進行漸變,故而咱們將兩個圓心都設置在數據的座標點,而第一個圓半徑取0,第二個半徑同咱們須要繪製的圓形半徑一致。測試
而後咱們須要經過addColorStop(position, color)
定義在兩個圓之間顏色漸變的規則。咱們要達到的效果是顏色在某一個維度上的數值從中心隨半徑增長逐漸變小,並且同時,該維度的數值與數據的value
正相關,不然全部數據點繪製出的圖形都會如出一轍。因此,咱們選擇alpha
做爲變化維度,由於咱們可使用globalAlpha
來設置一個全局的透明度,這個透明度與value
正相關,這樣的話咱們就能夠統一使用rgba(r,g,b,1)
和rgba(r,g,b,0)
做爲中心點和半徑邊緣的顏色。優化
那麼咱們經過如下代碼來實現以上兩個步驟:
/*
* radius: 繪製半徑,請自行設置
* min, max: 強弱閾值,可自行設置,也可取數據最小最大值
*/
data.forEach(point => {
let {x, y, value} = point;
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI);
context.closePath();
// 建立漸變色: r,g,b取值比較自由,咱們只關注alpha的數值
let radialGradient = context.createRadialGradient(x, y, 0, x, y, radius);
radialGradient.addColorStop(0.0, "rgba(0,0,0,1)");
radialGradient.addColorStop(1.0, "rgba(0,0,0,0)");
context.fillStyle = radialGradient;
// 設置globalAlpha: 需注意取值需規範在0-1之間
let globalAlpha = (value - min) / (max - min);
context.globalAlpha = Math.max(Math.min(globalAlpha, 1), 0);
// 填充顏色
context.fill();
});
複製代碼
在示例中min
爲0,max
爲數據最大值,至此,咱們獲得的圖形以下:
可見圖中的透明度已能表明數據強弱及輻射效果,且在相交處進行了線性的疊加。咱們如今要給圖形上色,須要使用ImageData
對象對圖像進行像素操做,讀取每一個像素點的透明度,而後使用其映射後的顏色改寫ImageData
數值。
先不急着瞭解像素操做如何進行,咱們首先要肯定的是透明度數值到顏色的映射關係。ImageData
中的透明度數值是取值在[0, 255]之間的整數,咱們要建立一個離散的映射函數,使0對應到最弱色(示例中爲淺藍色,你也能夠自由設置),255對應到最強色(示例中爲正紅色)。而這個漸變的過程並非單一維度的遞增,好在咱們已有工具解決漸變的問題,即上文已介紹過的createLinearGradient(x1, y1, x2, y2)
。
如上圖所示,咱們能夠建立一個跨度爲 256 像素的直線漸變色,用其填充一個 256*1 的矩形,至關於一個調色盤。在這個調色盤上(0, 0)位置的像素呈現最弱色,(255, 0)位置的像素呈現最強色,因此對於透明度a,(a, 0)位置的像素顏色即爲其映射顏色。代碼以下:
const defaultColorStops = {
0: "#0ff",
0.2: "#0f0",
0.4: "#ff0",
1: "#f00",
};
const width = 20, height = 256;
function Palette(opts) {
Object.assign(this, opts);
this.init();
}
Palette.prototype.init = function() {
let colorStops = this.colorStops || defaultColorStops;
// 建立canvas
let canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext("2d");
// 建立線性漸變色
let linearGradient = ctx.createLinearGradient(0, 0, 0, height);
for (const key in colorStops) {
linearGradient.addColorStop(key, colorStops[key]);
}
// 繪製漸變色條
ctx.fillStyle = linearGradient;
ctx.fillRect(0, 0, width, height);
// 讀取像素數據
this.imageData = ctx.getImageData(0, 0, 1, height).data;
this.canvas = canvas;
};
/**
* 取色器
* @param {Number} position 像素位置
* @return {Array.<Number>} [r, g, b]
*/
Palette.prototype.colorPicker = function(position) {
return this.imageData.slice(position * 4, position * 4 + 3);
};
複製代碼
簡單介紹一下ImageData
對象,其存儲着Canvas對象真實的像素數據,包括width
, height
, data
三個屬性。咱們能夠:
createImageData(anotherImageData | width, height)
來建立一個新對象getImageData(left, top, width, height)
來建立帶有Canvas畫布中特定區域的像素數據的對象putImageData(myImageData, left, top)
來向Canvas畫布寫入像素數據基於此,咱們先獲取畫布數據,遍歷像素點讀取透明度,獲取透明度映射顏色,改寫像素數據並最終寫入畫布便可。
// 像素着色
let imageData = context.getImageData(0, 0, width, height);
let data = imageData.data;
for (var i = 3; i < data.length; i+=4) {
let alpha = data[i];
let color = palette.colorPicker(alpha);
data[i - 3] = color[0];
data[i - 2] = color[1];
data[i - 1] = color[2];
}
context.putImageData(imageData, 0, 0);
複製代碼
至此,咱們已經完成了熱力圖的繪製,看看效果吧:
若是咱們在地圖上呈現熱力圖,隨着地圖的移動,數據點的座標會變化,但其對應的圓形圖像實際上是不變的。因此爲了不更新座標時重複地建立漸變色、設置globalAlpha
、繪製及填充顏色等,咱們能夠預先繪製好每一個數據點的圖像,經過一個不在文檔流中的Canvas保存下來,在從新渲染的時候經過drawImage
將其繪製到畫布上:
function Radiation(opts) {
Object.assign(this, opts);
this.init();
}
Radiation.prototype.init = function() {
let {radius, globalAlpha} = this;
// 建立canvas
let canvas = document.createElement("canvas");
canvas.width = canvas.height = radius * 2;
// 獲取上下文,初始化設置
let ctx = canvas.getContext("2d");
ctx.translate(radius, radius);
ctx.globalAlpha = Math.max(Math.min(globalAlpha, 1), 0);
// 建立徑向漸變色:灰度由強到弱
let radialGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radius);
radialGradient.addColorStop(0.0, "rgba(0,0,0,1)");
radialGradient.addColorStop(1.0, "rgba(0,0,0,0)");
ctx.fillStyle = radialGradient;
// 畫圓
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fill();
this.canvas = canvas;
};
Radiation.prototype.draw = function(context) {
let {canvas, x, y, radius} = this;
context.drawImage(canvas, x - radius, y - radius);
};
複製代碼
2019.1.14更新:通過性能測試發現,上文所述有誤。離屏渲染主要應用於局部繪製過程較複雜,而該局部又被重複繪製的場景下;同時應保證這個離屏的畫布大小適中,由於複製過大的畫布會帶來很大的性能損耗。在上文中,局部繪製過程其實很簡單,與直接使用drawImage
的耗時相差無幾,因此無需使用離屏渲染。
使用drawImage
時若是使用了浮點數座標,瀏覽器爲了達到抗鋸齒的效果,會作額外計算,渲染子像素。因此儘可能使用整數座標。