Frontend Development 14 min read

Design and Implementation of a Generic Map Component with Real‑time Heatmap Rendering Using ThreeJS

This article explains the motivation, architecture, and implementation details of a generic map component that supports real‑time heatmap rendering using ThreeJS, covering GIS layer protocols, canvas limitations, WebGL advantages, and code examples for creating radial gradients, instanced meshes, and custom shaders.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Design and Implementation of a Generic Map Component with Real‑time Heatmap Rendering Using ThreeJS

The author describes the background of creating a map component with heatmap functionality, noting that existing map SDKs (MapBox.js, Leaflet, OpenLayers, ArcGIS.js) are powerful but difficult to replace, and that a higher‑level abstraction is needed to hide differences between various map SDK APIs and layer protocols.

To achieve a unified solution, the component abstracts only the Viewport (coordinates, zoom level, optional rotation and tilt) and relies on the OGC WMS protocol to decouple map SDKs from layer services, enabling a "Write once, run anywhere" approach.

The architecture separates the viewport handling from the underlying map SDK, allowing the component to support massive points, heatmaps, and 3D model rendering. ThreeJS is introduced to render these features, and synchronization with AMap and Mapbox SDKs is briefly mentioned.

For heatmap rendering, the author first analyses the canvas‑based approach used by heatmap.js , explaining the importance of the gradient and radius parameters, the data format ( x , y , count ), and the interference effect when circles overlap.

Because canvas redrawing is inefficient for real‑time updates, the implementation switches to WebGL using ThreeJS. An InstancedMesh is used to draw many radial gradient circles as textures, with additive blending to achieve interference.

var grd = ctx.createRadialGradient(x, y, 0, x, y, radius);
gr.addColorStop(0, `rgba(0, 0, 0, ${count/max})`);
gr.addColorStop(1, `rgba(0, 0, 0, 0)`);
ctx.fillStyle = grd;
ctx.arc(x, y, radius, 0, 2 * Math.PI);

The code creates a radial gradient texture on a hidden canvas, converts it to a CanvasTexture , and uses it for each instance:

const canvas2d = document.createElement('canvas');
canvas2d.width = 100;
canvas2d.height = 100;
const ctx = canvas2d.getContext('2d');
if (ctx) {
  const grd = ctx.createRadialGradient(50, 50, 0, 50, 50, 50);
  grd.addColorStop(0, 'rgba(255,255,255,1');
  grd.addColorStop(1, 'rgba(0,0,0,0)');
  ctx.fillStyle = grd;
  ctx.fillRect(0, 0, 100, 100);
}
this.heatmapTexture = new CanvasTexture(canvas2d);

Heat points are grouped into 20 precision levels (≈5% steps). For each level an InstancedMesh is created with a material that uses the gradient texture, additive blending, and no depth test:

const precision = 20;
pointsArray.forEach((points, index) => {
  const opacity = (index + 1) / precision;
  const mesh = new InstancedMesh(
    this.planeGeometry,
    new MeshBasicMaterial({
      opacity,
      blending: AdditiveBlending,
      depthTest: false,
      transparent: true,
      map: this.heatmapTexture,
    }),
    points.length,
  );
  const obj = new Object3D();
  points.forEach(({ x, y }, i) => {
    obj.position.set(x, y, this.z);
    obj.updateMatrix();
    mesh.setMatrixAt(i, obj.matrix);
  });
  this.heatmapObj3D.add(mesh);
});

A color palette is generated on a 256‑pixel canvas and used as a lookup texture in a custom post‑processing shader that maps the alpha channel (heat intensity) to the final color:

const colors = [
  [0, "rgba(0, 0, 255, 0)"],
  [0.1, "rgba(0, 0, 255, 0.5)"],
  [0.3, "rgba(0, 255, 0, 0.5)"],
  [0.5, "yellow"],
  [1.0, "rgb(255, 0, 0)"],
];
// create 1x256 canvas, fill with linear gradient using colors, then:
const colorTexture = new CanvasTexture(canvasColor);

The shader reads the rendered heatmap texture, extracts the alpha value, looks up the corresponding color from the palette, and outputs the final fragment color with optional global opacity:

uniform float opacity;
uniform sampler2D tDiffuse;
uniform sampler2D colorTexture;
varying vec2 vUv;
void main() {
  vec4 texel = texture2D(tDiffuse, vUv);
  float alpha = texel.a;
  vec4 color = texture2D(colorTexture, vec2(alpha, 0.0));
  gl_FragColor = opacity * step(0.04, alpha) * color;
}

Finally, the author notes that this approach yields a smooth, real‑time heatmap that updates with viewport changes, and invites readers to discuss further improvements.

frontendMapWebGLVisualizationGISheatmapThreeJS
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.