ここの記事「ハードコア乾燥の次の場所テンセント公共サービスの数より転載!グース工場フロントエンドエンジニアは、どのように熱マップを達成する方法を教えて!"
著者:テンセントの位置情報サービス
リンク:https://mp.weixin.qq.com/s/bgS7uFlyLtK8WtusKfv8lA
出典:マイクロチャンネル公衆数
著作権は著者が保有しました。著者は認可商業転載してください接触、非商用の転載は、ソースを明記してください。
我々はデータ視覚化コンポーネントの今年初めに立ち上げたときに少しの友人が、私はまだ覚えていますか?神の視点「ヘルプあなたはターン」「新しいライン上のデータ視覚化コンポーネントを。」地図データ視覚化コンポーネントに基づくものは、主に熱、散乱、領域、移行図を含む、JSAPIに追加のライブラリの形で添加されます。
私は「神の視点」は、それを開く方法を知っているしたいですか?その背後にあるビジュアルコンポーネントのこれらの原則の実現のために?テンセントロケーションサービスのWeb開発の第一線のエンジニア、学生トトロ皆の秘密のための美しさと知恵の共存をしてみましょう。
限られたスペース、紙、ヒートマップのためには、例えば、その背後にある原則の実施を説明します。
ヒートマップについて
ヒートマップはトレンドデータのサイズと分布の強度、熱力学的ダイアグラム分析は人口密度、活性分析に適用することができる表示する可視化型の色です。データは、主に熱離散強度値と座標点対応を含み、図に示されています。
達成するための熱力学の図
データ準備の
記事唯一の懸念の根拠は、あなたが他のマップのためのシナリオ、またはウェブ解析や焦点であるかどうか、達成ヒートマップ、シーンは最終的に、我々は次のようにデータ形式がある必要がある、キャンバスキャンバス上の二次元座標に対応する座標を必要と:
// 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}
];
注:特定のシーンの使用は、そのような地図アプリケーションとして、画素座標にAPIの緯度と経度の座標をマッピングする必要があります。
原則
結果は、我々は熱力学的ダイアグラムを達成する方法を逆推力装置とします。
[図の熱力学的原理]
私たちは、直感的に感じることができます。
図1に示すように、熱延伸、円形状の放射グラデーション(すなわち、いわゆるラジアル勾配が中心から増加する半径とともに徐々に変化する)、データの性能ながら勾配円が充填されて提示され、各データポイント弱に強いの放熱効果
二つの円が互いに重ね合わせることができ、および性能データの本質の線形重ね合わせは、強度が重畳されている間に2、
3、1つのマッピングへの色データ1の強度値は、赤と青の強い弱い直線勾配の一般的なパフォーマンスは、もちろんあなたも、あなた自身の強クロマトグラフィーを設計することができます
根据我们的直观感受,我们需要做的是:
1、将每一个数据映射为一个圆形
2、选定一个线性维度表示数据强度值,圆形区域内该维度在圆心处达到最大值,沿着半径逐渐变小,直至边缘处为最小值
3、将圆形内的强度值进行叠加
4、以强度色谱进行颜色映射
往往有人对第2、3步有疑问,为什么不直接以强度色谱填充圆形呢?
因为没有alpha通道时不会进行混色,重叠的时候颜色会相互覆盖而非叠加;且即使在强度色谱上设置了alpha值,叠加时也是rgb三个通道上分别进行计算,简单来说就是无法将蓝色与蓝色叠加出现红色。
那需要开一个二维数组存储强度值进行叠加计算吗?
也不用。其实canvas画布本身就可以看作一个二维数组,可以选取alpha单通道作为表示强弱的维度,虽然alpha通道并非严格的线性叠加,其为a = a1 + a2 - a1 * a2,但也可以满足我们的需求,如下图所示,其与a = a1 + a2所表示的平面比较贴近。
[ 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三个属性。我们可以:
1、 通过createImageData(anotherImageData | width, height)来创建一个新对象
2、或者getImageData(left, top, width, height)来创建带有Canvas画布中特定区域的像素数据的对象
3、使用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);
至此,我们已经完成了热力图的绘制,看看效果吧:
[ 热力图 ]
性能优化
离屏渲染
离屏渲染是指在文档流外的canvas中预先绘制好所需图形,然后将其作为纹理绘制到画布上,主要应用于局部绘制过程较复杂,而该局部又被重复绘制的场景下;同时应保证这个离屏的画布大小适中,因为复制过大的画布会带来很大的性能损耗。
那么热力图是否可以使用离屏渲染提升性能呢?考虑一下,如果我们在地图上呈现热力图,随着地图的移动,数据点的坐标会变化,但其对应的圆形图像其实是不变的。所以为了避免更新坐标时重复地创建渐变色、设置globalAlpha、绘制及填充颜色等,我们可以使用离屏渲染预先绘制好每个数据点的图像,
在重新渲染的时候通过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);
};
然而经过性能测试发现,热力图局部绘制过程其实比较简单,与直接使用drawImage的耗时相差无几,所以无需使用离屏渲染。
避免浮点数坐标
使用drawImage时如果使用了浮点数坐标,浏览器为了达到抗锯齿的效果,会做额外计算,渲染子像素。所以尽量使用整数坐标。
怎么样?看完我们tototo同学的细致介绍,不知道你有没有掌握可视化组件背后的秘密?如果有任何问题欢迎在下方直接留言。
当然,如果你对这些底层的技术不是那么关心,那也没有关系。我们腾讯位置服务的愿景就是为了降低开发者门槛,减少开发者成本,解放开发者生产力。所以,totoro同学和她的小伙伴们才把这些复杂的底层实现包装成了组件的形式,方便大家调用。
だから何も躊躇何ですか?今すぐクリックしてここに直接立ち上がります!すべてのビジュアルコンポーネントへの私たちの最初の呼び出し、それは確かに「チョン」と彼女の小さな友人のハードワークです。
最後に、ビジュアルコンポーネントのWebGL開発の3Dバージョンに基づいて、その前方のスポイラーは、オンラインについても、効果を示すことは、よりクールで、またジュニアパートナー、開発者が集中することを続けてください!