Example of bar chart implementation for canvas charts in html5, html5canvas
I used the chart library a few days ago. Baidu's ECharts is the best. By default, it uses canvas, which is better than svg in processing big data. So I also use canvas to implement a chart library. It doesn't feel too difficult. First, we need to implement a simple bar chart.
The effect is as follows:
Main functions include:
- Text Rendering
- Draw the XY axis;
- Draw data groups;
- Data animation implementation;
- Processing of mouse events.
Usage
First, let's take a look at the usage. Refer to the usage of some ECharts, first pass in the html tag to display the chart, and then call init to import data during initialization.
Var con = document. getElementById ('Container'); var chart = new Bar (con); chart. init ({title: 'annual rainfall histogram ', xAxis: {// X axis data: ['August 1', 'August 1', 'August 1', 'August 30', 'August 30 ', 'October ', 'October']}, yAxis: {// y axis name: 'water', formatter: '{value} ml'}, series: [// group data {name: 'Eastern precipitation', data: [, 20, 38, 50,120, 56,130]}, {name: 'precipitation in the West ', data: [, 10, 60, 39,]}, {name: 'precipitation in the south ', data: [60%,]}, {color: 'hsla (, 1) ', name: 'precipitation in the north', data: [,]});
Chart base class. We need to write a pie chart and a line chart later, so we can extract the public part. Note that canvas. style. width is different from canvas. width. The former will stretch the image, and the latter will be used normally and will not stretch the image. In this case, the write is expanded first and then reduced to solve the problem of blurring when the canvas draws the text.
Class Chart {constructor (container) {this. container = container; this. canvas = document. createElement ('canvas '); this. ctx = this. canvas. getContext ('2d '); this. W = 1000*2; this. H = 600*2; this. padding = 120; this. paddingTop = 50; this. title = ''; this. legend = []; this. series = []; // solve the font blur problem by doubling the font size. canvas. width = this. w; this. canvas. height = this. h; this. canvas. style. width = this. w/2 + 'px '; this. canvas. style. height = this. h/2 + 'px ';}}
Initialize the bar chart and call Object. assign (this, opt) in es6. this is equivalent to the extend method in JQ and copies the attributes to the current instance. At the same time, a tip attribute is created, which is an html Tag and is used for displaying data information. Draw the image and bind the mouse event.
Class Bar extends Chart {constructor (container) {super (container); this. xAxis ={}; this. yAxis = []; this. animateArr = [];} init (opt) {Object. assign (this, opt); if (! This. container) return; this. container. style. position = 'relative '; this. tip = document. createElement ('div '); this.tip.style.css Text = 'display: none; position: absolute; opacity: 0.5; background: #000; color: # fff; border-radius: 5px; padding: 5px; font-size: 8px; z-index: 99; '; this. container. appendChild (this. canvas); this. container. appendChild (this. tip); this. draw (); this. bindEvent ();} draw () {// draw} showInfo () {// display information} animate () {// execute animation} showData () {// display data}
Draw XY axis
First, draw the title, then the XY axis, and then traverse the grouped data series, which contains complex calculations, then draw the scale of the XY axis, draw the grouping label, and finally draw the data. Data items in series are grouped data, which correspond one to one with xAxis. data on the X axis. Each item can have a custom name and color. If it is not specified, the name is assigned to nunamed and the color is automatically generated. The legend attribute is also used to record the tag list information, because the cursor is clicked later to determine whether the tag list is used.
Canvas knowledge points:
- The arcTo method is used for grouping tags to produce the rounded corner.
- The measureText method is used to draw text, which can be used to measure the width of the text. In this way, the position of the next painting can be adjusted to avoid location conflicts.
- The translate displacement method can be placed in the drawing context (between save and restore) to avoid complicated positional operations.
Draw () {var that = this, ctx = this. ctx, canvas = this. canvas, W = this. w, H = this. h, padding = this. padding, paddingTop = this. paddingTop, xl = 0, xs = 0, xdis = W-padding * 2, // Number of X axis units, length of each unit, total length of X axis yl = 0, ys = 0, ydis = H-padding * 2-paddingTop; // number of y axis units, each unit length, and the total y axis length ctx. fillStyle = 'hsla (0, 0%, 20%, 1) '; ctx. strokeStyle = 'hsla (0, 0%, 10%, 1) '; ctx. lineWidth = 1; ctx. textAlign = 'center'; ctx. textBaseLine = 'middle'; ctx. font = '24px arial'; ctx. cl EarRect (0, 0, W, H); if (this. title) {ctx. save (); ctx. textAlign = 'left'; ctx. font = 'bold 40px arial'; ctx. fillText (this. title, padding-50, 70); ctx. restore ();} if (this. yAxis & this. yAxis. name) {ctx. fillText (this. yAxis. name, padding, padding + paddingTop-30);} // X axis ctx. save (); ctx. beginPath (); ctx. translate (padding, H-padding); ctx. moveTo (0, 0); ctx. lineTo (W-2 * padding, 0); ctx. stroke (); // X axis scale if (this. xAxis &&( Xl = this. xAxis. data. length) {xs = (W-2 * padding)/xl; this. xAxis. data. forEach (obj, I) => {var x = xs * (I + 1); ctx. moveTo (x, 0); ctx. lineTo (x, 10); ctx. stroke (); ctx. fillText (obj, x-xs/2,40) ;});} ctx. restore (); // y axis ctx. save (); ctx. beginPath (); ctx. strokeStyle = 'hsl (220,100%, 50%) '; ctx. translate (padding, H-padding); ctx. moveTo (0, 0); ctx. lineTo (0, 2 * padding + paddingTop-H); ctx. stroke (); ctx. restore (); if (this. se Ries. length) {var curr, txt, dim, info, item, tw = 0; for (var I = 0; I <this. series. length; I ++) {item = this. series [I]; if (! Item. data |! Item. data. length) {this. series. splice (I --, 1); continue;} // assign an item without color if (! Item. color) {var hsl = I % 2? 180 + 20 * I/* (I-1); item. color = 'hsla ('+ hsl +', 70%, 60%, 1) ';} item. name = item. name | 'unnamed'; // draws the group tag ctx. save (); ctx. translate (padding + W/4, paddingTop + 40); that. legend. push ({hide: item. hide | false, name: item. name, color: item. color, x: padding + that. w/4 + I * 90 + tw, y: paddingTop + 40, w: 60, h: 30, r: 5}); ctx. textAlign = 'left'; ctx. fillStyle = item. color; ctx. strokeStyle = item. color; roundRect (ctx, I * 90 + tw, 0, 60, 3 0, 5); ctx. globalAlpha = item. hide? 0.3: 1; ctx. fill (); ctx. fillText (item. name, I * 90 + tw + 70,26); tw + = ctx. measureText (item. name ). width; // calculates the length of the string ctx. restore (); if (item. hide) continue; // calculate the if (! Info) {info = calculateY (item. data. slice (0, xl);} curr = calculateY (item. data. slice (0, xl); if (curr. max> info. max) {info = curr ;}} if (! Info) return; yl = info. num; ys = ydis/yl; // draw the Y axis scale ctx. save (); ctx. fillStyle = 'hsl (200,100%, 60%) '; ctx. translate (padding, H-padding); for (var I = 0; I <= yl; I ++) {ctx. beginPath (); ctx. strokeStyle = 'hsl (220,100%, 50%) '; ctx. moveTo (-10,-Math. floor (ys * I); ctx. lineTo (0,-Math. floor (ys * I); ctx. stroke (); ctx. beginPath (); ctx. strokeStyle = 'hsla (0, 0%, 80%, 1) '; ctx. moveTo (0,-Math. floor (ys * I); ctx. lineTo (xdis,-Math. floor (Ys * I); ctx. stroke (); ctx. textAlign = 'right'; dim = Math. min (Math. floor (info. step * I), info. max); txt = this. yAxis. formatter? This. yAxis. formatter. replace ('{value}', dim): dim; ctx. fillText (txt,-20,-ys * I + 10);} ctx. restore (); // draw data this. showData (xl, xs, info. max );}}
Draw data
Because the data item needs to be displayed when the animation is executed and the mouse slides, it is put into the animation queue animateArr. Here we need to expand the grouping data, convert the previous two nested arrays into one layer, and calculate the attributes of each data item, such as name, x coordinate, y coordinate, width, and speed, color. After the data is organized, the animation is executed.
ShowData (xl, xs, max) {// draw data var that = this, ctx = this. ctx, ydis = this. h-this.padding * 2-this.paddingTop, sl = this. series. filter (s =>! S. hide ). length, sp = Math. max (Math. pow (10-sl, 2)/3-4,5), w = (xs-sp * (sl + 1)/sl, h, x, index = 0; that. animateArr. length = 0; // expand the data item and fill in the animation queue for (var I = 0, item, len = this. series. length; I <len; I ++) {item = this. series [I]; if (item. hide) continue; item. data. slice (0, xl ). forEach (d, j) => {h = d/max * ydis; x = xs * j + w * index + sp * (index + 1); that. animateArr. push ({index: I, name: item. name, num: d, x: Math. round (x), y: 1, w: Math. round (w), h: Math. floor (h + 2), vy: Math. max (300, Math. floor (h * 2)/100, color: item. color}) ;}); index ++;} this. animate ();}
Execute Animation
There is nothing to say about executing an animation. It is a self-executed closure function. The animation principle is to accumulate the speed value vy in sequence for the Y axis. But remember to stop the queue after the animation is executed, so there is an isStop sign, which is determined every time the queue is executed.
animate(){ var that=this, ctx=this.ctx, isStop=true; (function run(){ isStop=true; for(var i=0,item;i<that.animateArr.length;i++){ item=that.animateArr[i]; if(item.y-item.h>=0.1){ item.y=item.h; } else { item.y+=item.vy; } if(item.y<item.h){ ctx.save(); // ctx.translate(that.padding+item.x,that.H-that.padding); ctx.fillStyle=item.color; ctx.fillRect(that.padding+item.x,that.H-that.padding-item.y,item.w,item.y); ctx.restore(); isStop=false; } } if(isStop)return; requestAnimationFrame(run); }())}
Bind event
Event 1: When mousemove is performed, check whether the mouse position is on the group label or data item. Call isPointInPath (x, y) after drawing the path. If it is true, canvas is used. style. cursor = 'pointer '; if it is a data item, re-draw the column, set transparency, and distinguish it. You also need to display the content. Here is a div with absolute positioning relative to the parent container. It is already created as the tip attribute during initialization. We encapsulate the display part into the showInfo method.
Event 2: When mousedown, determine the group tag that the mouse clicks, and set the hide attribute in the series of the corresponding group data. If it is true, the item is not displayed, and then call the draw method, rewrite rendering and animation.
BindEvent () {var that = this, canvas = this. canvas, ctx = this. ctx; this. canvas. addEventListener ('mousemove ', function (e) {var isLegend = false; // pos = WindowToCanvas (canvas, e. clientX, e. clientY); var box = canvas. getBoundingClientRect (); var pos = {x: e. clientX-box.left, y: e. clientY-box.top}; // group label for (var I = 0, item, len = that. legend. length; I <len; I ++) {item = that. legend [I]; ctx. save (); roundRect (ctx, item. x, ite M. y, item. w, item. h, item. r); // because it is doubled, the coordinates must be * 2 if (ctx. isPointInPath (pos. x * 2, pos. y * 2) {canvas. style. cursor = 'pointer '; ctx. restore (); isLegend = true; break;} canvas. style. cursor = 'default'; ctx. restore () ;}if (isLegend) return; // select the data item for (var I = 0, item, len = that. animateArr. length; I <len; I ++) {item = that. animateArr [I]; ctx. save (); ctx. fillStyle = item. color; ctx. beginPath (); ctx. rect (that. padding + item. x, th At. h-that.padding-item.h, item. w, item. h); if (ctx. isPointInPath (pos. x * 2, pos. y * 2) {// clear the image and re-draw the ctx image with the transparency of 0.5. clearRect (that. padding + item. x, that. h-that.padding-item.h, item. w, item. h); ctx. globalAlpha = 0.5; ctx. fill (); canvas. style. cursor = 'pointer '; that. showInfo (pos, item); ctx. restore (); break;} canvas. style. cursor = 'default'; that. tip. style. display = 'none'; ctx. globalAlpha = 1; ctx. fill (); ctx. restore (); }}, False); this. canvas. addEventListener ('mousedown ', function (e) {e. preventDefault (); var box = canvas. getBoundingClientRect (); var pos = {x: e. clientX-box.left, y: e. the clientY-box.top}; for (var I = 0, item, len = that. legend. length; I <len; I ++) {item = that. legend [I]; roundRect (ctx, item. x, item. y, item. w, item. h, item. r); // because it is doubled, the coordinates must be * 2 if (ctx. isPointInPath (pos. x * 2, pos. y * 2) {that. series [I]. hide =! That. series [I]. hide; that. animateArr. length = 0; that. draw (); break ;}}, false) ;}// display showInfo (pos, obj) {var txt = this. yAxis. formatter? This. yAxis. formatter. replace ('{value}', obj. num): obj. num; var box = this. canvas. getBoundingClientRect (); var con = this. container. getBoundingClientRect (); this. tip. innerHTML = '<p>' + obj. name + ':' + txt + '</p>'; this. tip. style. left = (pos. x + (box. left-con.left) + 10) + 'px '; this. tip. style. top = (pos. y + (box. top-con.top) + 10) + 'px '; this. tip. style. display = 'block ';}
Summary
What we have done here is only a basic effect. In fact, there are still many areas to be further optimized, such as responsive support, mobile support, animation effect, and support for multiple y axes, displays the effect of the content and supports line breaks.
The above is all the content of this article. I hope it will be helpful for your learning and support for helping customers.