Brief introduction of thermal map

Location data is an important information resource connecting the line and the line. It is an important means of data analysis to effectively present the data in the front end with the help of graphical means. Based on this, we developed map-based data visualization components, which are added to JSAPI in the form of additional libraries, including thermal maps, scatter maps, region maps and migration maps.

A thermal map is a visual type that shows the strength and distribution trend of data by color, as shown in the upper left corner of the figure above. The thermal map can be applied to population density analysis, activity analysis, etc. The data presented in the thermal diagram mainly include discrete coordinate points and corresponding strength values.

Thermal map realization

Data preparation

This paper only focuses on the basic realization of the thermal map. No matter you use it for maps, web focus analysis or other scenes, you need to convert the coordinates of the corresponding scene into two-dimensional coordinates on Canvas. The data format we need is as follows:

// x, y represent 2d coordinates; Var data = [{x: 471, y: 277, value: 25}, {x: 438, y: 375, value: 97}, {x: 373, y: 19, value: 71}, {x: 473, y: 42, value: 63}, {x: 463, y: 95, value: 97}, {x: 590, y: 437, value: 34}, {x: 377, y: 442, value: 66}, {x: 171, y: 254, value: 20}, {x: 6, y: 582, value: 64}, {x: 387, y: 477, value: 14}, {x: 300, y: 300, value: 80} ];Copy the code

Realize the principle of

Let us deduce from the results how we should achieve the thermal map.

  1. In a thermal map, each data point appears as a circle filled with radial gradients (a gradual change from the center of the circle as the radius increases), and this gradient circle represents the radiative effect of the data decreasing from strong to weak
  2. Two circles can be superimposed on each other in a linear superposition, which essentially represents the superposition of strong and weak data
  3. The strength of the data is mapped to the color one by one, usually showing a linear gradient of red strong blue weak, of course you can design your own strength chromatography

Based on our intuition, what we need to do is:

  1. Map each data to a circle
  2. Select a linear dimension to indicate the strength of the data, gradient the color to that dimension and fill the circle
  3. Overlay the circle
  4. Color mapping using intensity chromatography

It is important to note that in step 2, we did not directly fill the circle with intensity chromatography, because the color obtained in this way is three dimensions and is not linear when superimposing. In this article, alpha, the transparency in color, is chosen as the dimension of strength and weakness. You can also choose R or G or whatever. The benefits of choosing alpha will be explained later.

implement

Draw a circular

To draw arcs or circles on Canvas, use the Arc () method:

arc(x, y, radius, startAngle, endAngle, anticlockwise)
Copy the code

X and y correspond to the data coordinates. Radius can be set freely. StartAngle and endAngle indicate the start and end angles, 0 and 2 * math. PI respectively, and anticlockwise indicate whether to anticlockwise.

gradient

You can create gradients on Canvas using the canvasGradient object, It is divided into linear gradient createLinearGradient(X1, Y1, X2, Y2) and radial gradient createRadialGradient(X1, Y1, R1, X2, Y2, R2), which is adopted by us. Creating radial gradients requires defining two circles with the color gradient in the region between the two circles, so we set the centers of both circles at the coordinates of the data, with the radius of the first circle set to 0 and the radius of the second to match the radius of the circle we want to draw.

Then we need to define the rules for the color gradient between the two circles by addColorStop(position, color). The effect we want to achieve is that the value of color on a certain dimension gradually decreases from the center with the increase of radius, and at the same time, the value of this dimension is positively correlated with the value of the data, otherwise all data points will be drawn exactly the same graph. Therefore, we chose alpha as the change dimension because we can use globalAlpha to set a global transparency that is positively related to value, so that we can use rGBA (R, G, B,1) and RGBA (R, G, B,0) for the center and radius edges.

So we implement the above two steps with the following code:

/* * radius: draw radius, please set your own * min, Max: strong or weak threshold, can set your own, also can set the minimum maximum data */ data.forEach(point => {let{x, y, value} = point; context.beginPath(); context.arc(x, y, radius, 0, 2 * Math.PI); context.closePath(); // Create gradient: r,g,b are free values, we only focus on alphaletradialGradient = context.createRadialGradient(x, y, 0, x, y, radius); RadialGradient. AddColorStop (0.0,"Rgba (0,0,0,1)"); RadialGradient. AddColorStop (1.0,"Rgba (0,0,0,0)"); context.fillStyle = radialGradient; // Set globalAlpha: The value must be between 0 and 1letglobalAlpha = (value - min) / (max - min); context.globalAlpha = Math.max(Math.min(globalAlpha, 1), 0); // Fill the color context.fill(); });Copy the code

In the example, min is 0 and Max is the maximum value of data. So far, the graph we get is as follows:

The color map

It can be seen that the transparency in the figure can represent data strength and radiation effect, and linear superposition is carried out at the intersection. We now want to color the graph by pixelating the image using the ImageData object, reading the transparency of each pixel, and then rewriting the ImageData value with its mapped color.

Before we rush to understand how pixel manipulation works, we first need to determine the mapping between transparency values and colors. The transparency values in ImageData are integers between [0, 255], and we will create a discrete mapping function so that 0 corresponds to the weakest color (light blue in the example, which you can set freely) and 255 to the strongest color (positive red in the example). However, this gradient process is not a single dimensional increment. Fortunately, we already have a tool to solve the gradient problem, namely the createLinearGradient(X1, y1, X2, y2) introduced above.

As shown in the image above, we can create a straight gradient 256 pixels across and fill it with a 256*1 rectangle, which acts as a palette. On this palette, pixels at (0, 0) present the weakest color, while pixels at (255, 0) present the strongest color. Therefore, for transparency A, pixel color at (a, 0) is its mapping color. The code is as follows:

const defaultColorStops = {
    0: "#0ff"And 0.2:"#0f0"And 0.4:"#ff0",
    1: "#f00"}; const width = 20, height = 256;function Palette(opts) {
    Object.assign(this, opts);
    this.init();
}

Palette.prototype.init = function() {
    letcolorStops = this.colorStops || defaultColorStops; / / create a canvaslet canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    let ctx = canvas.getContext("2d"); // Create linear gradientslet linearGradient = ctx.createLinearGradient(0, 0, 0, height);
    for (const key incolorStops) { linearGradient.addColorStop(key, colorStops[key]); } // Draw a gradient bar ctx.fillstyle = linearGradient; ctx.fillRect(0, 0, width, height); This.imagedata = ctx.getimageData (0, 0, 1, height).data; this.canvas = canvas; }; /** * color picker * @param {Number} position Pixel position * @return {Array.<Number>} [r, g, b]
 */
Palette.prototype.colorPicker = function(position) {
    return this.imageData.slice(position * 4, position * 4 + 3);
};
Copy the code

Pixel shader

A brief introduction to the ImageData object, which stores the real pixel data of the Canvas object, including width, height and data. We can:

  • throughcreateImageData(anotherImageData | width, height)To create a new object
  • orgetImageData(left, top, width, height)To create an object with pixel data for a specific region of the Canvas Canvas
  • useputImageData(myImageData, left, top)Writes pixel data to the Canvas Canvas

Based on this, we first get the canvas data, traverse the pixels to read the transparency, get the transparency mapping color, rewrite the pixel data and finally write to the canvas.

// Pixel shadinglet imageData = context.getImageData(0, 0, width, height);
let data = imageData.data;
for (var i = 3; i < data.length; i+=4) {
    let alpha = data[i];
    let color = palette.colorPicker(alpha);
    data[i - 3] = color[0];
    data[i - 2] = color[1];
    data[i - 1] = color[2];
}
context.putImageData(imageData, 0, 0);
Copy the code

At this point, we have completed the thermal map drawing, look at the effect:

Performance optimization

Off-screen rendering

If we present a thermal map on a map, the coordinates of the data points will change as the map moves, but the corresponding circular image will not change. So to avoid repeatedly creating gradients, setting globalAlpha, drawing, filling, etc., when updating coordinates, we can pre-draw the image of each data point, save it in a Canvas that is not in the document flow, and draw it onto the Canvas with drawImage during re-rendering:

function Radiation(opts) {
    Object.assign(this, opts);
    this.init();
}

Radiation.prototype.init = function() {
    let{radius, globalAlpha} = this; / / create a canvaslet canvas = document.createElement("canvas"); canvas.width = canvas.height = radius * 2; // Get the context and initialize the Settingslet ctx = canvas.getContext("2d"); ctx.translate(radius, radius); ctx.globalAlpha = Math.max(Math.min(globalAlpha, 1), 0); // Create radial gradient: grayscale from strong to weakletradialGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radius); RadialGradient. AddColorStop (0.0,"Rgba (0,0,0,1)"); RadialGradient. AddColorStop (1.0,"Rgba (0,0,0,0)"); ctx.fillStyle = radialGradient; // Draw circle ctx.arc(0, 0, radius, 0, math.pi * 2); ctx.fill(); this.canvas = canvas; }; Radiation.prototype.draw =function(context) {
    let {canvas, x, y, radius} = this;
    context.drawImage(canvas, x - radius, y - radius);
};
Copy the code

Update 2019.1.14: After performance testing, the above statement was found to be incorrect. Off-screen rendering is mainly used in scenes where the local rendering process is complicated and the local part is repeatedly drawn. Also make sure that the off-screen canvas is of a moderate size, because copying a canvas that is too large will cause a significant performance loss. In the above article, the local rendering process is actually quite simple, taking about the same time as using the drawImage directly, so there is no need to use the off-screen rendering.

Avoid floating point coordinates

If floating point coordinates are used when drawImage is used, the browser will do extra calculations to render the sub-pixels for anti-aliasing. So try to use integer coordinates.