instructions

This morning I took a look at the source code of heatmap.js to see how it generates heat maps. Here we leave aside the data processing part and focus on heatMap drawing.

If the thermal map of a point is to be drawn, createRadialGradient can be simply used to achieve it. However, if the thermal map of two points overlaps, the overlapped part is certainly not a simple overlay. In this case, we can certainly use pixel-level operation to combine the thermal map of two points and obtain the covered thermal map through complex calculation, but it is obviously too complicated.

If we look at the heat map, it’s really just a gradient of some colors, a little bit darker in the middle, a little bit lighter in the outside, and we’re actually coloring it according to the weight. Let’s say we have a point at [80, 80], like the radiation around radius 10, and we set the weight of the center of mass to 100, and the outermost part to 10, and it’s easy to think of a monochrome drawing. The most convenient is to use gray, only need to use transparency, its pixel RGB value is 0, such data is easy to process, as shown in the following figure.

So the procedure is to use this grayscale first to draw on a canvas, where the RGBA of each point is between (0, 0, 0, 0) and (0, 0, 0, 255). You can now color it according to its alpha value. There is now a gradient color card as follows, and the corresponding relationship is that alpha is 0 on the left side of the color card and 255 on the right.

A simple way is to use gradient to draw a canvas with a width of 256, get the colors of these 256 points, and then make one-to-one correspondence with the canvas. For example, if the alpha value of a pixel on our main canvas is 100, change the store color to the color of the 100th (programmer count) point on the color card.

The specific implementation process is as follows:

  1. An off-screen canvas draws a black (RGB 0, easy to handle) alpha channel circle
  2. Draw points on the main canvas through the off-screen canvas
  3. Draw an off-screen canvas with a width of 256 and a height of 1, and draw the gradient onto the canvas to get the color card
  4. throughgetImageDataMethod to obtain the canvas data, and obtain the corresponding RGB in the color card through the data of alpha value in the data, and fill in the corresponding RGB
  5. Finally, the canvas data is filled to the main canvas;

Note: 1. Set the color depth for each point according to the value value and modify globalAlpha accordingly. 2. It is not necessary to draw grayscale canvas to the main canvas, off-screen canvas can also be used. The last step is to draw the result to the main canvas (heatmap.js is the case). 3. Grayscale data can be calculated using Uint8ClampedArray, and it is not necessary to draw a gray canvas to obtain data. Calculation is not complicated.

That’s the idea, and here’s a simple way to do it.

interfaceHeatMapConfig { gradient? : object; radius? :number; width? :number; height? :number; min? :number; max? :number;
    container: HTMLElement
}

interface PointData{
    x: number;
    y: number;
    value: number;
}

class HeatMap {
    static defaultConfig = {
        gradient: {
            0.3: "blue".0.5: "lime".0.7: "yellow".1: "red"
        },
        min: 0,
        max: 100,
        radius: 40,
        width: 400,
        height: 400
    }
    private config: HeatMapConfig;
    private canvas = this.createCanvas();
    private ctx = this.canvas.getContext('2d');
    private data: PointData[] = [];
    constructor(config: HeatMapConfig) {
        this.initConfig(config);
    }

    private initConfig(config: HeatMapConfig) {
        if(! config.container) {throw Error('no container');
        }
        this.config = { ... HeatMap.defaultConfig, ... config };const {width, height} = this.config;
        this.canvas.width = width;
        this.canvas.height = height;
        this.config.container.appendChild(this.canvas);
    }

    initData(data: PointData[]) {
        this.data = data;
        this.render();
    }

    private render() {
        this.renderAlpha();
        this.putColor()
    }

    // Draw the alpha channel circle
    private renderAlpha(){
        const shadowCanvas = this.createShadowTpl();
        const {min, max} = this.config;
        for(let point of this.data) {
            const alpha = (point.value - min) / (max - min);
            this.ctx.globalAlpha = alpha;
            this.ctx.drawImage(shadowCanvas, point.x, point.y); }}// Color the alpha channel circles
    private putColor() {
        const colorData = this.createColordata();
        const imgData = this.ctx.getImageData(0.0.this.canvas.width, this.canvas.height);
        const {data} = imgData

        for(let i = 0; i < data.length; i++) {
            const value = data[i];
            if(value) {
                data[i - 3] = colorData[4 * value];
                data[i - 2] = colorData[4 * value + 1];
                data[i - 1] = colorData[4 * value + 2]; }}this.ctx.putImageData(imgData, 0.0);
    }

    private createCanvas(){
        return document.createElement('canvas')}private createColordata(){
        const cCanvas = this.createCanvas();
        const cCtx = cCanvas.getContext('2d');
        cCanvas.width = 256;
        cCanvas.height = 1;
        const tuple: [number.number.number.number] =
            [0.0, cCanvas.width, cCanvas.height]

        constgrd = cCtx.createLinearGradient(... tuple);const {gradient} = this.config;
        for(let key in gradient) {
            grd.addColorStop(parseFloat(key), gradient[key]);
        }
        cCtx.fillStyle = grd;
        cCtx.fillRect(0.0, cCanvas.width, cCanvas.height);
        returncCtx.getImageData(... tuple).data; }/** * The off-screen canvas draws a circle */ in the alpha channel of black (RGB is 0 for easy handling)
    private createShadowTpl() {
        const tCanvas = this.createCanvas();
        const tCtx = tCanvas.getContext('2d');
        const blur = 0;
        const radius = this.config.radius;
        tCanvas.width = 2 * radius;
        tCanvas.height = 2 * radius;
        const grd = tCtx.createRadialGradient(radius, radius, blur, radius, radius, radius);
        grd.addColorStop(0.'rgba (0,0,0,1)');
        grd.addColorStop(1.'rgba (0,0,0,0)');
        tCtx.fillStyle = grd;
        tCtx.fillRect(0.0.2 * radius, 2 * radius);
        returntCanvas; }}const heatmap = new HeatMap({
    container: document.body
});

const data: PointData[] = [];
for(var i = 0; i < 100; i++) {
    data.push({
        x: Math.random() * 400,
        y : Math.random() * 400,
        value: Math.random() * 100
    })
}

heatmap.initData(data);
Copy the code