threejs热力图的实现(heatmap.js)

4,009 阅读7分钟

1. 基本概念

1.1 热力图是什么

热力图是一种常用的数据可视化方式,它通过颜色编码的方式表示数据在二维平面上的分布情况。热力图主要有以下特点:

  1. 数据呈现方式:

    • 热力图通常以二维矩阵的形式呈现数据,每个单元格代表一个数据点。
    • 每个单元格的颜色深浅表示该位置数据值的大小,颜色越深表示数据值越大。
  2. 应用场景:

    • 热力图适用于表示二维平面上的数据分布,如地图上的人口密度、销售情况等。
    • 也可用于表示任意二维数据,如网页点击热力图、产品使用热力图等。
  3. 制作方式:

    • 热力图通常由专业的数据可视化工具如 D3.js、Echarts、Tableau等生成。
    • 这些工具提供丰富的配色方案和交互功能,用户可根据需求进行定制。
  4. 信息呈现:

    • 热力图能直观地展示数据在二维空间的分布特点,如数据密集区域、高低值区域等。
    • 通过颜色编码,可快速识别数据值的相对大小关系。

总的来说,热力图是一种有效的数据可视化方式,能帮助用户更好地理解和分析二维数据的分布特征。它广泛应用于各行各业,是数据分析和决策支持的重要工具

1.2 热力图插件选择

我们选择heatmap.js官网来生成热力效果,这是一个专注于热力图的轻量级Javascript可视化库。上手简单,适合简单的热力图需求。heatmap代码仓github,这个项目的代码最近的更新时间是八年前……, 但是目前依旧有大量的项目选择它作为热力图的首选插件。

2. 场景搭建

2.1 底图数据

palne.png 构成底图的元素有一组json数据,然后由正方形的平面拼接而成,平面间隔为单位1,在每个平面上,展示对应的value值。

json数据我们这里只展示一层,现实情况可能是多个层的展示,数据及格式如下所示:

const data = [
    {
        "data": {
            "1": [14,22,16,27,18,13,45,45,22,22,15,31],
            "2": [29,26,22,15,14,31,45,18,12,11,15,31],
            "3": [17,19,21,15,11,22,25,12,27,32,15,31],
            "4": [31,14,23,17,16,24,19,21,10,12,15,31]
        }
    }
]

2.2 字体加载

首先要引入

import { FontLoader } from 'three/examples/jsm/loaders/FontLoader';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry';

FontLoader 负责加载字体文件并返回 Font 对象,TextGeometry 则利用该 Font 对象生成 3D 文本几何体。通过结合使用这两个组件,可以在 ThreeJS 场景中渲染 3D 文本。

  1. FontLoader:
    • FontLoader 用于加载字体文件,支持常见的字体格式,如 .ttf.otf 等。
    • 加载字体文件需要一定的时间,因此通常将其放在异步加载的过程中。
    • 字体加载完成后,会返回一个 Font 对象,包含了该字体的相关信息,如字形、基线等。
    • 示例代码如下:
const fontLoader = new THREE.FontLoader();
fontLoader.load('path/to/font.json', (font) => {
         // 使用 font 对象创建 TextGeometry
});
  1. TextGeometry:
    • TextGeometry 是一个 3D 几何体,用于表示由字体渲染而成的文本。

    • 创建 TextGeometry 需要提供以下参数:

      • text: 要渲染的文本内容
      • parameters: 一个对象,包含以下属性:
        • font: 加载完成的 Font 对象
        • size: 文本的大小
        • height: 文本的厚度
        • curveSegments: 曲线细分程度
        • bevelEnabled: 是否启用斜角
        • bevelThickness: 斜角厚度
        • bevelSize: 斜角大小
    • 创建 TextGeometry 的示例代码如下:

   const textGeometry = new THREE.TextGeometry('Hello, ThreeJS!', {
          font: font,
          size: 0.5,
          height: 0.2,
          curveSegments: 12,
          bevelEnabled: true,
          bevelThickness: 0.03,
          bevelSize: 0.02
        });

创建好 TextGeometry 后,可以像使用其他几何体一样进行渲染和材质设置。

//层间距
const layerGap = 2;

//得到所有的层级数目
const layerCount = data.length;
let layerGroup = [];
//为每个层级创建一个group
for(let i = 0; i < layerCount; i++) {
  layerGroup[i] = new THREE.Group();
  initLayer(i);
}

//初始化每一层的数据
function initLayer(currentLayer) {
  let points = [];
  //获取第一层的数据
  const layerData = data[currentLayer].data;
  //获取第一层的行和列
  const layer1Row = Object.keys(layerData).length;
  const layer1Col = Object.values(layerData)[0].length;
  //从data中获取数据
  for(let row = 0; row < layer1Row; row++) {
      for(let col = 0; col < layer1Col; col++) {
          let temperature = layerData[row + 1][col];

          points.push({ x: col + 1,y: row + 1,value: temperature })
          let color = 0x158fec
          let geometry = new THREE.PlaneGeometry(2,2);
          let material = new THREE.MeshBasicMaterial({ color,side: THREE.DoubleSide });
          let plane = new THREE.Mesh(geometry,material);
          plane.position.set(col * 2.2 + 1 - layer1Col * 1.1,row * 2.2 + 1 - layer1Row * 1.1,currentLayer * layerGap);

          let textMesh
          new FontLoader().load('https://esm.sh/three@0.166.1/examples/fonts/gentilis_regular.typeface.json',font => {
              const textGeometry = new TextGeometry(temperature.toString(),{
                  font: font,
                  size: 0.5,
                  depth: 0.01,
                  curveSegments: 0.12,//指定曲线的分段数,这会影响文本曲线的光滑程度。分段数越大,曲线越光滑
                  bevelEnabled: false,//指定是否启用斜角(bevel),即是否给文本添加倒角效果
              });
              const textMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
              textMesh = new THREE.Mesh(textGeometry,textMaterial)
              //更改文字在z轴上的位置
              textMesh.position.z = 0.1 + currentLayer * layerGap;  // 将文本位置设置为2
              textMesh.position.x = col * 2.2 - layer1Col * 1.1 + 0.35;
              textMesh.position.y = row * 2.2 - layer1Row * 1.1 + 0.5;
              layerGroup[currentLayer].add(textMesh)
          })
          layerGroup[currentLayer].add(plane)

      }
  }
  scene.add(layerGroup[currentLayer])
}

3. 上热力图

3.1 热力图的引入

方式一(通过npm方式安装依赖包):

安装

npm install heatmap.js

引用

import h337 from 'heatmap.js';

使用

 h337.create({
    container: document.getElementById("heatmap"),
     blur: '.8',
     radius: 30
})

但是这种方式会报错,需要到node_module里面修改对应包的源码内容。不建议使用

方式二:本地加载

文件下载地址github.com/pa7/heatmap.js/blob/master/build/heatmap.js 将源码js移动到项目当中,再来更改源码,在文件中找到下列代码

img.data = imgData
this.ctx.putImageData(img,x,y);
this._renderBoundaries = [1000,1000,0,0];

img.data=imgData注释掉,就解决了。

方式三:

在index.html引入

<script src="https://cdn.bootcdn.net/ajax/libs/heatmap.js/2.0.0/heatmap.min.js"></script>

在js文件中可以直接使用。

热力图实现的方式

heatmap.js生成canvas图,然后将canvas图作为贴图盖到3d场景中。有个主要的点就是canvas坐标系和3d右手坐标系的y轴是相反的

在 ThreeJS 中,ShaderMaterial 是一种非常强大和灵活的材质类型,它允许开发者直接使用自定义的 GLSL 着色器程序来控制材质的渲染过程。我们这里定义

相关的片元着色器和顶点着色器的代码如下所示:

let fragmentShader =/*glsl*/`
	varying float hValue;
    varying vec3 cl;
    void main() {
        float v = abs(hValue - 1.);
        gl_FragColor = vec4(cl, .8 - v * v) ;
     }
`
let vertexShader =/*glsl*/ `
 uniform sampler2D heightMap;
 uniform float heightRatio;
 varying vec2 vUv;
 varying float hValue;
 varying vec3 cl;
 void main() {
     vUv = uv;
     vec3 pos = position;
     cl = texture2D(heightMap, vUv).rgb;
     hValue = texture2D(heightMap, vUv).r;
     pos.z = hValue * heightRatio;
     gl_Position = projectionMatrix * modelViewMatrix * vec4(pos,1.0);
 }
`

heightRatio定义贴图z方向的高度,heightMap对应canvas贴图。

//添加热力图
function TDHeatMap(points = [],row,col) {
    //points要进行比例换算,x值对应的高度,y值对应的宽度,value对应的温度
    //获取id为heatmap的宽度和高度
    const height = document.getElementById('heatmap').clientHeight;
    const width = document.getElementById('heatmap').clientWidth;

    //3d贴图与2d贴图的y坐标的值是相反的
    const newPoints = points.map((item) => {
        let newItems = {}
        newItems.x = (item.x-0.7) * width/col;
        newItems.y = (4.5-item.y) * (height / (row*1.0));
        newItems.value = item.value;
        return newItems
    })
    // 热力图
    var heatmap = h337.create({
        container: document.getElementById("heatmap"),
        blur: '.8',
        radius: 30
    });
    var data = {
        max: 40,
        min: 0,
        data: newPoints,
        width,
        height
    };
    heatmap.setData(data);

    //使用上图的canvas作为贴图
    let texture = new THREE.CanvasTexture(heatmap._renderer.canvas);
    let geometry = new THREE.PlaneGeometry(col * 2.2,row * 2.2,2000,2000);
    let material = new THREE.ShaderMaterial({
        uniforms: {
            heightMap: { value: texture },
            heightRatio: { value: 2 }
        },
        vertexShader: vertexShader,
        fragmentShader: fragmentShader,
        transparent: true,
    });
    let mesh = new THREE.Mesh(geometry,material);
    scene.add(mesh);
}

其中有段代码拎出来看下

   //3d贴图与2d贴图的y坐标的值是相反的
    const newPoints = points.map((item) => {
        let newItems = {}
        newItems.x = (item.x-0.7) * width/col;
        newItems.y = (4.5-item.y) * (height / (row*1.0));
        newItems.value = item.value;
        return newItems
    })

这里的数字0.7只是我设置的一个偏移量,让图层居中展示;4.5是取y的最大值4加上了一个0.5的偏移量。可以根据blur和radius参数,和实际情况进行微调。

4. 总结

八青妹这个案例只是一个简易的小demo,现实中使用场景比这个要复杂得多,高楼大厦也是平地起,咱先找到思路,然后对症下药。