At present, although there are many excellent visual chart libraries in the front-end community, such as ECharts and AntV, for highly customized business requirements such as market charts, front-end students still need to implement them by themselves. Many students do not know much about graphic visualization technology. Once they are separated from the open source library, they will still be lost. They can only rely on relatively little information on the Internet to slowly explore and step on the pit to complete their needs. This article will introduce the canvas performance optimization scheme that we actually applied in the snowball market chart project, hoping to be helpful to you.

Shanghai Composite Index daily K chart – Snow Ying Securities

Optimized drawing instruction

The K-line is drawn from the opening, high, low, and close prices of each analysis period. To draw the daily K line as an example, first determine the opening and closing prices, and draw the part between them as rectangular entities. If the closing price is higher than the opening price, the K line is called the positive line and is represented by a hollow entity. Conversely, it is called negative line and is represented by solid entity. In the domestic stock and futures market, red is usually used to represent the positive line, green is negative line (but the investors involved in the European and American stock and foreign exchange markets should note: in these markets usually use green on behalf of the positive line, red on behalf of the negative line, and the domestic habit is just opposite). Connect the high and low prices to the entity separately with thinner wires. The line between the highest price and the solid is called the top line, and the line between the lowest price and the solid is called the bottom line.

The candle figure

By looking at the structure of the graph, we can naturally divide it into two vertical lines and a rectangle, so it can be drawn in three sections.

  • Draw a straight line from the highest price to the higher value of the closing price or opening price (photocopying);

  • Draw a hollow wireframe/solid rectangle (solid) composed of closing price and opening price;

  • Draw a straight line from the lowest to the higher value of the closing or opening price (bottom line).

This drawing method looks very clear, but canvas is an instruction drawing system, which realizes the rendering of graphics by changing several states of the context. For example, a stroke is called to draw a line segment whose width and color depend on the lineWidth and strokeStyle Settings.

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.lineWidth = 2;
ctx.strokeStyle = "# 000";
ctx.beginPath();
ctx.moveTo(10.10);
ctx.lineTo(50.10);
ctx.stroke();
ctx.closePath();

Copy the code

We can see that drawing a line segment requires many instructions, and the more corresponding drawing instructions, the greater the performance consumption, so we should optimize the drawing instructions as much as possible to avoid unnecessary changes in the state of the canvas. After careful observation of the graph, we can find that the two straight lines have the same X-axis, which can be reduced to a straight line, and the hollow wire frame can also be understood as a solid rectangle filled with background color, so our new scheme becomes:

  • Draw a black line connecting the highest and lowest prices;

  • Draws a solid stroke rectangle of closing and opening prices.

In this way, each candle reduces the process of drawing a straight line, which is also more consistent with the semantics of K line.

On the other hand, we know that the K line will rise and fall, and each K line will have different properties. If we draw these red and green candles from left to right, we will have to change the brush each time, and we will have to switch the shape of the drawing. Then, can we consider grouping the graph data before drawing, and only draw graphs with the same attributes each time, so that the rendering overhead of the graph is transferred to the calculation? We can try drawing it like this:

  • The data were divided into two groups according to red and green;

  • Set the rising graph attribute, traverse the rising (close flat) candle, draw the rising graph in turn;

  • Set the falling graph property, traversing the falling candle, drawing the falling graph in turn.

In this way, we only need to change the brush twice to achieve the drawing, which greatly reduces the rendering overhead caused by modifying the graphics instructions compared to the original need to change each time traversal. Of course, this drawing method in the example splits the basic primitives, which may affect the interaction form of the product. The specific drawing still needs to be carried out according to the product requirements. The example shows that changing the context properties is not completely free, and we can plan the order of drawing API calls to reduce the number of drawing instructions.

Use caching wisely

The rendering performance is improved by changing the drawing order and drawing method to reduce drawing instructions, but we still call drawing instructions again when we redraw. If we can cache the drawn image, can we directly call drawImage to draw the image during drawing to improve the rendering performance?

// Get the actual canvas node, get the context
const realCanvas = document.getElementById("canvas");
const realCtx = canvas.getContext("2d");
// Create the bufferCanvas node, get the context, you can also use OffscreenCanvas;
const cacheCanvas = document.createElement("canvas");
const cacheCtx = cacheCanvas.getContext("2d");
// Draw graphics in cacheCanvas;
drawKlineShapes(cacheCtx, klineShapes[i]);
// Use drawImage to copy the graph to the real node. The graph is drawn to the page once;
realCtx.drawImage(cacheCanvas, 0.0);
// Update just call drawImage to redraw
requestAnimationFrame(() = > {
realCtx.clearRect(0.0, realCanvas.width, realCanvas.height);
realCtx.drawImage(cacheCanvas, 0.0);
});

Copy the code

It is important to note, however, that off-screen rendering should be used with caution if the product requires continuous creation and destruction of graphics, since off-screen rendering actually performs the drawing process and consumes browser resources. The advantage of using cache is not only to reduce drawing instructions to optimize rendering, but also a key application when drawing K-line is to use the clipping function of drawImage to realize dragging loading, zooming and other functions of graph.

// Set the size of cacheCanvas to be larger than the realCanvas if there is drag and zoom.
cacheCanvas.width = 10000;
cacheCanvas.height = 10000;
// Take the center area as an example
var x = cacheCanvas.width / 2 - realCanvas.width / 2;
var y = cacheCanvas.height / 2 - realCanvas.height / 2;
var w = realCanvas.width;
var h = realCanvas.height;
// Initial drawing, crop out the middle part of the cacheCanvas graphic
context.drawImage(cacheCanvas, x, y, w, h);
// Listen for onMouseDown, onMousemove, onMouseup events to implement drag-and-drop
// onmouseDown Records the address mouseP(mx, my) where the mouse is pressed and sets the move flag to true
// onMousemove When move flag is true, get newMouseP(newX, newY), calculate the parameters required for drawImage
// onmouseup sets movement flag to false
// Compute the coordinates and crop out the moved figure
realCtx.drawImage(cacheCanvas, x + (newX - mX), y + (newY - mY), w, h, 0.0, w, h);
// Listen on onMouseWheel and use deltaY to calculate the zoom factor s
realCtx.drawImage(bufferCanvas, x, y, w / s, h / s, 0.0, w, h);

Copy the code

In addition, we can also use cache to achieve the graphics pick up, because the off-screen graphics are hidden, we can convert a hexadecimal color value according to the graphics index as the graphics color to draw, click the operation through getImageData API to obtain the color value of the selected graphics. Convert to graph index to get the selected graph.

// Draw the displayed graph
drawShapes(shapesArray);
// Draw hidden graphics
drawCacheShapes(shapesArray);
// Get the data for the cached canvas
var cacheImageData = cacheContext.getImageData(0.0, width, height);
// Listen for click events to get the corresponding graph index of color values
canvas.onClick = function(ev{
 var point = getPoint(ev.clientX, ev.ClientY);
 var color = getCacheColor(point);
 var index = colorToNumber(color);
 var shape = shapesArray[index];
}

Copy the code

This method is simple to implement and accurate to select. However, because the graphics are drawn twice, rendering overhead is high. The performance of getImageData is also dependent on the size of the canvas, so this approach is rarely used on more complex canvases.

Layered rendering

In most scenarios, we map the static graphics, also needs to be updated frequently and re-paint in response to user actions, but for most of the graphics, they actually is the same, we can only after the data update need to redraw time, absolutely every time there is no need to move the mouse just redraw once all the graphics. In that case we may need to use layered rendering.

Canvas layered rendering is easy to implement. We can create multiple Canvases based on the canvas rendering frequency, drawing unchanged elements in background-layer and changing elements in UI-layer. When drawing the K line, the axis can be drawn at background-layer, the K line at main-layer, and the graphics generated by the interaction can be drawn at uI-layer.

<div id="stage">
 <canvas id="ui-layer" width="480" height="320"></canvas>
 <canvas id="main-layer" width="480" height="320"></canvas>
 <canvas id="background-layer" width="480" height="320"></canvas>
</div>
Copy the code
 canvas { position: absolute; }
 #ui-layer { z-index3} `#main-layer { z-index2 }
 #background-layer { z-index1 }

Copy the code

For example, when we realize the interaction of hovering the mouse over the K line to display the specific information of the K line and highlighting the K line, we do not need to change the background and main-layer, but only need to draw the cross line and the highlighted K line in response to the mouse event on the UI-layer. The mouse movement process also only need to erase and redraw the UI layer graphics.

Layered rendering is mainly used to solve the problem of re-rendering static graphics, but for some complex scenes with static and static mixture, layered rendering is not easy to achieve, so we need to dynamically divide the redrawing area to achieve local redrawing.

summary

Canvas drawing is very powerful. When the graph we want to draw is not too large or there are not too many elements, most of them cannot touch the drawing bottleneck. However, we still need to be cautious in the drawing process. For millions of shareholders every minute, the lag of market charts has a great impact on user experience. This sharing mainly describes how to improve rendering performance by optimizing drawing instructions, and how to optimize graph redrawing by layering and cache drawing. For more performance optimization guidelines, please refer to the optimization of MDN Canvas. You are also welcome to visit snowball Web terminal or download Xueying PC terminal to experience snowball’s market function.

One more thing

Snowball business is developing by leaps and bounds, and the engineer team is looking forward to joining us. If you are interested in “being the premier online wealth management platform for Chinese people”, we hope you can join us. Click “Read the article” to check out the hot jobs.

Hot positions: Big front-end architect, Android/iOS/FE engineer, recommendation algorithm engineer, Java development engineer.