【Canvas】绘制风速热力图

前言

  大家好,我是南木元元,热衷分享有趣实用的文章,希望大家多多支持,一起进步!

  个人主页:南木元元


目录

风速热力图

前期工作

数据准备

数据稀疏问题

双线性插值

绘制色卡

绘制热力图

ImageData对象

获取颜色列表

填充像素

结语 


风速热力图

开始之前,大家先来了解一下风速热力图的概念。

风速热力图:用于显示区域内风速的分布情况。它通过在图表上使用颜色编码来表示风速的强度,可以更好地展示风速的变化规律。

风速热力图的特点就是用不同的颜色来表示风速大小,比如较高的风速会用较深的颜色表示,而较低的风速则用较浅的颜色表示。

前期工作

在上篇文章中,我们已经使用canvas绘制出了风场的空间分布图,效果如下:

【Canvas】绘制风速热力图_第1张图片

本文就来聊一下如何在风场上叠加一个风速热力图,最终的效果如下:

【Canvas】绘制风速热力图_第2张图片

和之前相比,还多了一个最左边的色卡,色卡用于表示不同数值和颜色的映射关系。

数据准备

绘制色卡需要准备两组数据:

  • 数值列表
  • 颜色值列表
//存储色卡数据
let heatmapLegend = {
  colorList: [],
  valueList: []
};
//数值列表
heatmapLegend.valueList = [6, 7, 8, ..., 65];
//颜色值列表
heatmapLegend.colorList = [[ 0, 0, 127, 255 ], [ 0, 0, 160, 255 ], [ 0, 0, 194, 255 ], ..., [ 160, 0, 0, 255 ]];

上述两个数组中的数据是一一对应的关系,如数值6对应rgba颜色值[ 0, 0, 127, 255 ]。

还需要的当然是网格点上的风速数据了,如下:

[
   8.9, 10.3, 11.1, 13.6, 16.4, 17.8, 19.6, 16.6, 13.9, 14,  
   4.6, 8.1, 10.6, 12.6, 12.6, 18.1, 18, 20.1, 23, 27.2, 
   ... 
]

我们的网格是10 * 12的维度,所以是120个格点数据。

数据稀疏问题

其实目前还存在一个问题,就是当前的数据比较稀疏和分散,如果直接进行数值到颜色的映射,那么只会在每个网格点上形成颜色点,无法渲染出一个平滑的热力图,这就需要我们进行相应的插值,来得到比较密集的点。

【Canvas】绘制风速热力图_第3张图片

什么是插值呢?

简单来说,插值指利用已知的点来“猜”未知的点。

双线性插值

我们这里采取双线性插值算法,思路也很简单,就是在两个方向分别进行一次线性插值。

【Canvas】绘制风速热力图_第4张图片

双线性插值的目的就是为了得到密集的数据,从而渲染出高精度、平滑的热力图。具体的插值过程不是本文的重点,这里就不再详细展开,感兴趣的可以去看一篇文章为你讲透双线性插值。

绘制色卡

绘制色卡可以分成两部分,一部分是渲染左侧的颜色列表,另一部分是渲染右侧的文字和背景区。大体思路就是循环遍历颜色列表,使用fillRect填充矩形的方式去渲染每个颜色方块,循环遍历数值列表,使用fillText绘制文字的方式去绘制字体。

// 色卡配置项
let colorCard = {
    posX: 10,//色卡起始x坐标
    posY: 20,//色卡起始y坐标
    width: 5,//每个方块宽度
    height: 5//每个方块高度
}
// 渲染左侧颜色列表
for (let i = 0; i < this.heatmapLegend.colorList.length; i++) {
  this.ctx.fillStyle =
    "rgba(" +
    this.heatmapLegend.colorList[i][0] +
    "," +
    this.heatmapLegend.colorList[i][1] +
    "," +
    this.heatmapLegend.colorList[i][2] +
    "," +
    this.heatmapLegend.colorList[i][3] +
    ")";
  this.ctx.fillRect(
    colorCard.posX,
    colorCard.posY + colorCard.height * i,
    colorCard.width,
    colorCard.height
  );
}
// 渲染右侧文字背景区
this.ctx.fillStyle = "rgb(0, 0, 0)";
this.ctx.fillRect(
  colorCard.posX + colorCard.width,
  colorCard.posY,
  colorCard.width,
  colorCard.height * this.heatmapLegend.colorList.length
);
// 渲染右侧文字
this.ctx.fillStyle = "rgb(255, 255, 255)";
this.ctx.font = 5 + "px Microsoft YaHei";
for (let i = 0; i < this.heatmapLegend.valueList.length; i++) {
  this.ctx.fillText(
    this.heatmapLegend.valueList[i],
    colorCard.posX + (3 * colorCard.width) / 2,
    colorCard.posY + colorCard.height * i
  );
}

下图就是我们渲染出的色卡:

绘制热力图

在正式绘制热力图之前,我们需要了解下canvas的ImageData对象。

ImageData对象

MDN上的定义。

ImageData描述canvas元素的一个隐含像素数据的区域。用于表示画布上的像素数据,可以通过获取和设置像素值来进行图像处理。

它有三个属性:

  • data:描述了一个一维数组,包含以 RGBA 顺序的数据,数据使用0~255的整数表示。
  • width:ImageData的宽度
  • height:ImageData的高度

接下来看看如何使用它,现在我们想要实现下面的效果。

【Canvas】绘制风速热力图_第5张图片

 代码如下:

let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
//创建一个新的、空白的100*100的ImageData对象
let imgData = ctx.createImageData(100, 100);
//每个像素都用红色去填充
for (let i = 0; i < imgData.data.length; i += 4) {
  imgData.data[i+0] = 255;
  imgData.data[i+1] = 0;
  imgData.data[i+2] = 0;
  imgData.data[i+3] = 255;
}
ctx.putImageData(imgData,10,10);

上述代码创建了一个100*100像素的ImageData对象,每个像素被设置为红色,是不是很简单。

获取颜色列表

看了上面的例子,不难发现,绘制热力图,我们只需要得到每个像素的颜色值,然后去填充即可。

//存储ImageData对象的每个像素对应的颜色
let colorValue = [];
getColorValue() {
    //data为插值后的风速数据
    for (let i = 0; i < data.length; i++) {
        let color = this.getColor(data[i]);
        colorValue.push(color[0]);
        colorValue.push(color[1]);
        colorValue.push(color[2]);
        colorValue.push(color[3]);
    }
}

//获取对应数值的颜色值
getColor(value) {
    let length = heatmapLegend.valueList.length;
    let color = [255, 255, 255, 0];
    //超过数值列表中最大值的数值为该最大值
    if (value >= heatmapLegend.valueList[length - 1]) {
      return heatmapLegend.colorList[length - 1];
    //小于数值列表中最小值的数值为0
    } else if (value < heatmapLegend.valueList[0]) {
      return color;
    }
    //根据值去查找颜色
    for (let i = 0; i < length; i++) {
      if (value < heatmapLegend.valueList[i]) {
        color = heatmapLegend.colorList[i];
        break;
      }
    }
    return color;
}

通过上述代码,我们可以得到大致如下的数据:

[0, 0, 227, 255, 0, 6, 227, 255, ...]

填充像素

最后,遍历上述数组,用对应颜色的像素去填充我们新建的ImageData对象就可以了。

//绘制热力图
drawHeatmap() {
    // 热力图的宽高为整个canvas的宽高减去边上一圈格子的宽高
    let heatmapWidth = this.canvas2d.width - 2 * this.offsetX;
    let heatmapHeight = this.canvas2d.height - 2 * this.offsetY;
    let imgData = this.ctx.createImageData(heatmapWidth, heatmapHeight);
    for (let i = 0; i < imgData.data.length; i += 4) {
      imgData.data[i + 0] = colorValue[i + 0];
      imgData.data[i + 1] = colorValue[i + 1];
      imgData.data[i + 2] = colorValue[i + 2];
      imgData.data[i + 3] = colorValue[i + 3];
    }
    this.ctx.putImageData(imgData, this.offsetX, this.offsetY);
}

至此,热力图就已经绘制完成了:

【Canvas】绘制风速热力图_第6张图片

再叠加上之前绘制的风场就可以得到我们想要的最终效果了。

【Canvas】绘制风速热力图_第7张图片

结语 

本文分享了如何使用canvas来绘制色卡和风速热力图,以及整个过程中所涉及到的数据处理。

如果此文对你有帮助的话,欢迎关注点赞、⭐收藏✍️评论支持一下博主~ 

你可能感兴趣的:(#,canvas,前端,canvas,可视化)