3D heat map (thermal cloud) based on HTML5 Canvas
Tags: javascript, heatmap, webdev, programming
Published: 2022-12-30 06:52:21

[[Click here to see more demonstrations including Digital Twins, GIS, VR, BIM, etc.]](https://www.hightopo.com/demos/en-index.html)

**Foreword**
Data contains value, while the value of data needs to be discovered and explored with IT technology. Visualization technology can help people better analyze the data . The quality of information depends largely on how it is presented. In the data analysis, heat map is undoubtedly a good way.
At present, people can find many heat map effects on the Internet. But many of them are 2D effects or pseudo 3D. Some people may know the 3D point cloud, while it's not very good for performance experience. In this article, we written a new 3D heat map effect here.
Final Effects:
![Heat map effects](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/86q5c9xnxd52hudzmtih.gif)

**Application Scenario**
A heat map of the distribution of people in the building:
![Heat map building](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/heexwr0flry63glnupi6.gif)
Look at the heat map in the building. The more people the area has, the deeper color it gives us. This scenario can be applied to security surveillance in buildings . In the event of an emergency, it can help people to make scientifically and efficiently formulate diversion strategies to provide strong help and support, and finally reduce losses. It can also be used for early warning of fire disaster and real-time temperature monitoring of the area.
Indoor equipment temperature thermodynamic map:
![Indoor equipment temperature thermodynamic map](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z20u8o6pearat998aog3.gif)
The traditional data center reporting method is boring and monotonous, with little sense of reality and poor interaction . With the help of the 3D heat map, the operation and maintenance engineers can greatly improve their work efficiency and reduce the probability of problems in the data center.

**Overall Procedure**
After the scene has been deserialized, set the initial parameters of the heat map . Add the heat map model into the scene to simulate the 3D heat map effect, and finally add functions such as scanning, skin changing, and temperature prompts.

**1. Data preparation**
Draw the area of the heat map in the scene, as shown in the figure
![Draw the area of the heat map](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iughq15qn8jvovzlfrnl.png)
At first let's determine the areaNode of the area to generate the heat map, and then randomly generate 20 points to simulate the source of heat, including the coordinate position (the coordinate is a vertex relative to the red cube) and the thermal value temperature .
The following is the main code of this part:

```
function getTemplateList(areaNode, hot, num) {
  let heatRect = areaNode.getRect();
  let { width, height } = heatRect;
  let rackTall = areaNode.getTall();
  hot = hot + this.random(20);
  let templateList = [];
  for (let i = 0; i < num; i++) {
      templateList.push({
          position: {
              x: 0.2 * width + this.random(0.6 * width),
              y: 0.2 * height + this.random(0.6 * height),
              z: 0.1 * rackTall + this.random(0.8 * rackTall)
          },
          temperature: hot
      });
  }
  return templateList;
}
let heatMapArea_1 = dm.getDataByTag('heatMapArea_1');
let templateList_1 = this.getTemplateList(
  heatMapArea_1,
  70,
  20
);
```

**2. Initialization**
Let's use the ht-thermodynamic.js plugin to generate heat maps.
After the data of the heat source (in step 1) is ready, configure the parameters of the heat map. The parameters are described as following.

```
// The default configuration
let config = {
  hot: 45,
  min: 20,
  max: 55,
  size: 50,
  pointNum: 20,
  radius: 150,
  opacity: 0.05,
  colorConfig: {
      0: 'rgba(0,162,255,0.14)',
      0.2: 'rgba(48,255,183,0.60)',
      0.4: 'rgba(255,245,48,0.70)',
      0.6: 'rgba(255,73,18,0.90)',
      0.8: 'rgba(217,22,0,0.95)',
      1: 'rgb(179,0,0)'
  },
  colorStopFn: function (v, step) { return v * step * step },
};
// get the size info of areaNode
let rackTall = areaNode.getTall();
let heatRect = areaNode.getRect();
let { width, height } = heatRect;
if (width === 0 || height === 0) return;
// init the heat map
let thd = this.thd = new ht.thermodynamic.Thermodynamic3d(g3d, {
  // the 3D box used by heat map
  box: new ht.Math.Vector3(width, height, rackTall),
  // min and max temperature
  min: config.min,
  max: config.max,
  // the render interval of each piece
  interval: 40,
  // false means use the max temperature (not sum) during crossed temperature regions
  remainMax: false,
  // opacity of each piece
  opacity: config.opacity,
  // color step function
  colorStopFn: config.colorStopFn,
  // color range 
  gradient: config.colorConfig
});
```

**3. Load the heat map**

Set the data object of thd with the heat source generated in the first step, and call thd.createThermodynamicNode() to generate the 3D node of the thermodynamic map. Set its related information and add this primitive to the 3D scene. Such a simple 3D heat map is complete.

```
// load the heat map
function loadThermodynamic(thd, areaNode, templateList, config) {
  thd.setData(templateList);
  // x,y,z faces
  let node = this.heatNode = thd.createThermodynamicNode(config.size, config.size, config.size);
  let p3 = areaNode.p3();
  node.setAnchorElevation(0);
  node.p3(p3);
  node.s({
      'interactive': true,
      'preventDefaultWhenInteractive': false,
      '3d.movable': false,
      "wf.visible": false
  });
  g3d.dm().add(node);
}
```
The introduction of the main body is done. Now let's talk about some other useful functions of the demo.

**4. Temperature Label**
The temperature label is used to show the real time temperature under the mouse.

![Temperature Label](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yh4g4j75nc0uckzdqg57.gif)


Since in the 3D scene, it is difficult to judge the current mouse coordinates (x, y, z), so we put the tip panel on the 2D canvas, and embed the 2D drawing on the upper layer of the 3D scene. By listening to the onMove event in the 3D scene, you can control the visibility and value change of the tip panel. 
tip visibility control: when the mouse moves into the heat map area, the tip is displayed, otherwise it is hidden. We can monitor the onLeave event to know when to hide the tip. 
Tip value control: We can call the ht-thermodynamic.js method thd.getHeatMapValue(e.event,'middle') to get the temperature value of the current mouse relative to the heat map area, and change the value attribute of the tip panel in real time. 
Code:

```
g3d.mi(e => {
  if (e.kind === 'onMove') {
      let { clientX, clientY } = e.event;
      if (this.templateTip) {
          let value1 = this.thd1.getHeatMapValue(e.event, 'middle');
          let value2 = this.thd2.getHeatMapValue(e.event, 'middle');
          if (value1 || value1 === 0 || value2 || value2 === 0) {
              let position = g2d.getLogicalPoint({ x: clientX, y: clientY })
              this.templateTip.a('value', value1 || value2 || 0)
              let { width, height } = this.templateTip.getRect()
              this.templateTip.setPosition({ x: position.x + width / 2, y: position.y - height / 2 })
          }
      }
  } else if (kind === 'onLeave') {
      let tag = data.getTag()
      if (tag && tag.hasOwnProperty('hoverBlock') > -1) {
          this.g2d.getView().style.cursor = 'default';
      }
      this.templateTip && this.setVisible(this.templateTip, false)
  }
})
```

**5. Scan**

![Scan](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/shdxkwckm64xym5csf29.gif)

Replace thd.createThermodynamicNode() in the third step. When generating a heat map object, instead of directly returning a model, select a certain direction for "cutting". Divide the length of this direction into n parts, and use the thd.getHeatMap() method to obtain the thermal image of each piece . The value of n can theoretically take any value . But for a better rendering effect, here we take 50, so that it will not be use too much time during the first rendering. Every time a surface is cut out, we dynamically create a ht.Node at the relative position of the thermal area. And then use ht.Default.setImage() to register the cut out surface as a picture and set it as the texture of the node (only need to set the two faces in the cutting direction). Finally, add all nodes to dataModel (the model that carries all nodes in ht).
There're two ways to finish the scan effect. The first is to create only one node instead of n nodes when doing cutting in step 3, and then dynamically set the texture and coordinates of the node to simulate the scanning effect; the second method still creates n nodes, and then hidden all of them. We display each of then at different times. Here we use the second way, as the first one needs to frequently modify multiple attributes, and the second one only needs to control the '3d.visible' attribute of the node.
The main code is as follows:

```
let length;
if (dir === 'z') {
    length = rackTall;
}
else if (dir === 'x') {
    length = width;
}
else if (dir === 'y') {
    length = height;
}
let size = config.size;
for (let index = 0; index < size; index++) {
    const offset = length / size;
    let timer = setTimeout(() => {
        let ctx = thd.getHeatMap(index * offset, dir, colorConfig);
        let floor = this.getHeatFloor(
            areaNode,
            dir,
            ctx,
            index,
            size,
            config
        );
        this.floors.push(floor);
        dm.add(floor);
    }, 0);
    this.timers.push(timer);
}
function start() {
    this.hide();
    this.anim = true;
    this.count = 0;
    let frames = this.floors.length;
    let params = {
        frames, // animation frames
        interval: 50, // interval between frames
        easing: t => {
            return t;
        },
        finishFunc: () => {
            if (this.anim) {
                this.start();
            }
        },
        action: (v, t) => {
            this.count++;
            this.show(this.count);
        }
    };
    this.scanning = ht.Default.startAnim(params);
}
function hide(index) {
    if (index || index === 0) {
        this.floors.forEach((i, j) => {
            if (index === j) {
                i.s('3d.visible', false);
            }
            else {
                i.s('3d.visible', true);
            }
        });
    }
    else {
        this.floors.forEach(i => {
            i.s('3d.visible', false);
        });
    }
}
function show(index) {
    if (index || index === 0) {
        this.floors.forEach((i, j) => {
            if (index === j) {
                i.s('3d.visible', true);
            }
            else {
                i.s('3d.visible', false);
            }
        });
    }
    else {
        this.floors.forEach(i => {
            i.s('3d.visible', true);
        });
    }
}
```

**6. Switch the Theme**

![Switch the Theme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r5p8hs0f81rbcarkha0c.gif)

The way of skin change: dynamically modify the background color and wall color of ht.graph3d.Graph3dView according to different scene values.
code:

```
function changeSkin() {
        let backgroundColor = this.g3d.dm().getBackground(),
            dark_bg = this.g3d.dm().getDataByTag('dark_skin'),
            light_bg = this.g3d.dm().getDataByTag('light_skin');
        if (backgroundColor !== 'rgb(255,255,255)') {
            this.g3d.dm().setBackground('rgb(255,255,255)');
        } else {
            this.g3d.dm().setBackground('rgb(0,0,0)');
        }
 
        dark_bg.s('2d.visible', !dark_bg.s('2d.visible'));
        dark_bg.s('3d.visible', !dark_bg.s('3d.visible'));
 
        light_bg.s('2d.visible', !light_bg.s('2d.visible'));
        light_bg.s('3d.visible', !light_bg.s('3d.visible'));
    }
```

**The End**
This is all about the 3D heat map. Readers who are interested in 2D /3D visualization can click on the link below to browse other examples. HT will give you a lot of incredible things.
https://www.hightopo.com/demos/en-index.html