Home  >  Article  >  Web Front-end  >  Example of implementing histogram in canvas chart in html5

Example of implementing histogram in canvas chart in html5

不言
不言Original
2018-06-05 16:13:433317browse

This article mainly introduces the example of using canvas chart to implement histogram in HTML5. This article uses canvas to implement a chart. The editor thinks it is quite good. Now I will share it with you and give it as a reference.

I used a chart library a few days ago, among which Baidu's ECharts seems to be the best. It uses canvas by default. Canvas charts are better than svg in processing big data. Then I will also use canvas to implement a chart library. It doesn’t feel too difficult. Let’s implement a simple bar chart first.

The effect is as follows:

##The main function points include:

  1. Text drawing

  2. XY axis drawing;

  3. Data group drawing;

  4. Implementation of data animation;

  5. Handling of mouse events.

How to use

First let’s take a look at how to use it. We refer to some of the usage methods of ECharts. First, pass in the html tag to display the chart. , then call init and pass in data during initialization.

var con=document.getElementById('container');
    var chart=new Bar(con);
    chart.init({
        title:'全年降雨量柱状图',
        xAxis:{// x轴
            data:['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']
        },
        yAxis:{//y轴
            name:'水量',
            formatter:'{value} ml'
        },
        series:[//分组数据
            {
                name:'东部降水量',
                data:[62,20,17,45,100,56,19,38,50,120,56,130]
            },
            {
                name:'西部降水量',
                data:[52,10,17,25,60,39,19,48,70,30,56,8]
            },
            {
                name:'南部降水量',
                data:[12,10,17,25,27,39,50,38,100,30,56,90]
            },
            {
                color:'hsla(270,80%,60%,1)',
                name:'北部降水量',
                data:[12,30,17,25,7,39,49,38,60,30,56,10]
            }
        ]
    });

Chart base class, we will also write pie charts and line charts later, so extract the common parts. Note that canvas.style.width and canvas.width are different. The former will stretch the graphics, while the latter is what we use normally and will not stretch the graphics. The purpose of writing first expansion and then reduction here is to solve the problem of blurring when drawing text on canvas.

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=[];
            //通过缩小一倍,解决字体模糊问题
            this.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';
        }
    }

To initialize the histogram, call Object.assign(this,opt) in es6. This is equivalent to the extend method in JQ, which copies the properties to the current instance. At the same time, a tip attribute is also created, which is an html tag and is used to display data information later. Then draw the graphics and bind mouse events.

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('p');
        this.tip.style.cssText='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(){//绘制

    }
    showInfo(){//显示信息

    }
    animate(){//执行动画

    }
    showData(){//显示数据

    }

Draw the XY axis

First draw the title, then the XY axis, then traverse the grouped data series, which has complex calculations, then draw the scale of the XY axis, draw the group label, and finally draw data. The data item series is grouped data, which corresponds to xAxis.data of the X-axis one-to-one. Each item can have a customized name and color. If not specified, the name is given to nunamed and the color is automatically generated. The legend attribute is also used here to record the tag list information, because it is useful for subsequent mouse clicks to determine whether the click is correct.

Canvas main knowledge points:

  1. The grouping tag uses the arcTo method, so that the effect of rounded corners can be drawn.

  2. The measureText method is used to draw text, which can be used to measure the width of the text, so that the position of the next drawing can be adjusted to avoid position conflicts.

  3. translate displacement method can be placed in the drawing context (between save and restore), which can avoid complex position calculations.

  4. 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,//x轴单位数,每个单位长度,x轴总长度
            yl=0,ys=0,ydis=H-padding*2-paddingTop;//y轴单位数,每个单位长度,y轴总长度
    
        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.clearRect(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轴
        ctx.save();
        ctx.beginPath();
        ctx.translate(padding,H-padding);
        ctx.moveTo(0,0);
        ctx.lineTo(W-2*padding,0);
        ctx.stroke();
        // x轴刻度
        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轴
        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.series.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;
                }
                // 赋予没有颜色的项
                if(!item.color){
                    var hsl=i%2?180+20*i/2:20*(i-1);
                    item.color=&#39;hsla(&#39;+hsl+&#39;,70%,60%,1)&#39;;
                }
                item.name=item.name||&#39;unnamed&#39;;
    
                // 画分组标签
                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=&#39;left&#39;;
                ctx.fillStyle=item.color;
                ctx.strokeStyle=item.color;
                roundRect(ctx,i*90+tw,0,60,30,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;//计算字符长度
                ctx.restore();
    
                if(item.hide)continue;
                //计算数据在Y轴刻度
                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;
    
            //画Y轴刻度
            ctx.save();
            ctx.fillStyle=&#39;hsl(200,100%,60%)&#39;;
            ctx.translate(padding,H-padding);
            for(var i=0;i<=yl;i++){
                ctx.beginPath();
                ctx.strokeStyle=&#39;hsl(220,100%,50%)&#39;;
                ctx.moveTo(-10,-Math.floor(ys*i));
                ctx.lineTo(0,-Math.floor(ys*i));
                ctx.stroke();
    
                ctx.beginPath();
                ctx.strokeStyle=&#39;hsla(0,0%,80%,1)&#39;;
                ctx.moveTo(0,-Math.floor(ys*i));
                ctx.lineTo(xdis,-Math.floor(ys*i));
                ctx.stroke();
    
                ctx.textAlign=&#39;right&#39;;
                dim=Math.min(Math.floor(info.step*i),info.max);
                txt=this.yAxis.formatter?this.yAxis.formatter.replace(&#39;{value}&#39;,dim):dim;
                ctx.fillText(txt,-20,-ys*i+10);
            }
            ctx.restore();
            //画数据
            this.showData(xl,xs,info.max);
        }
    }

Drawing data

Because the data item needs to perform subsequent animations and display content when the mouse slides over, it is put into the animation queue. animateArr. Here we need to expand the grouped 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, speed, and color. After the data is organized, the animation is executed.

showData(xl,xs,max){
    //画数据
    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;
    // 展开数据项,填入动画队列
    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 animation. It is a self-executing closure function. The principle of animation is to sequentially accumulate the velocity value vy on the y-axis. But remember that when the queue finishes executing the animation, it needs to be stopped, so there is an isStop flag, which is judged every time the queue is finished executing.

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);
    }())
}

Binding event

Event 1: When mousemove, check whether the mouse position is on the group label or the data item, and call isPointInPath(x after drawing the path , y), if true, canvas.style.cursor='pointer'; if it is a data item, the column must be redrawn, set transparency, and distinguished. The content also needs to be displayed. Here is a p that is absolutely positioned relative to the parent container container. It has been established as the tip attribute during initialization. We encapsulate the display part into the showInfo method.

Event 2: During mousedown, determine which group label the mouse clicks on, and then set the hide attribute in the corresponding group data series. If it is true, it means that the item will not be displayed, and then call the draw method to rewrite the rendering drawing. , execute animation.

bindEvent(){
        var that=this,
            canvas=this.canvas,
            ctx=this.ctx;
        this.canvas.addEventListener(&#39;mousemove&#39;,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
            };
            // 分组标签
            for(var i=0,item,len=that.legend.length;i<len;i++){
                item=that.legend[i];
                ctx.save();
                roundRect(ctx,item.x,item.y,item.w,item.h,item.r);
                // 因为缩小了一倍,所以坐标要*2
                if(ctx.isPointInPath(pos.x*2,pos.y*2)){
                    canvas.style.cursor=&#39;pointer&#39;;
                    ctx.restore();
                    isLegend=true;
                    break;
                }
                canvas.style.cursor=&#39;default&#39;;
                ctx.restore();
            }

            if(isLegend) return;
            //选择数据项
            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,that.H-that.padding-item.h,item.w,item.h);
                if(ctx.isPointInPath(pos.x*2,pos.y*2)){
                    //清空后再重新绘制透明度为0.5的图形
                    ctx.clearRect(that.padding+item.x,that.H-that.padding-item.h,item.w,item.h);
                    ctx.globalAlpha=0.5;
                    ctx.fill();
                    canvas.style.cursor=&#39;pointer&#39;;
                    that.showInfo(pos,item);
                    ctx.restore();
                    break;
                }
                canvas.style.cursor=&#39;default&#39;;
                that.tip.style.display=&#39;none&#39;;
                ctx.globalAlpha=1;
                ctx.fill();
                ctx.restore();
            }
            
        },false);

        this.canvas.addEventListener(&#39;mousedown&#39;,function(e){
            e.preventDefault();
            var box=canvas.getBoundingClientRect();
            var pos = {
                x:e.clientX-box.left,
                y:e.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);
                // 因为缩小了一倍,所以坐标要*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);
    }
    //显示数据
    showInfo(pos,obj){
        var txt=this.yAxis.formatter?this.yAxis.formatter.replace(&#39;{value}&#39;,obj.num):obj.num;
        var box=this.canvas.getBoundingClientRect();
        var con=this.container.getBoundingClientRect();
        this.tip.innerHTML = &#39;<p>&#39;+obj.name+&#39;:&#39;+txt+&#39;</p>&#39;;
        this.tip.style.left=(pos.x+(box.left-con.left)+10)+&#39;px&#39;;
        this.tip.style.top=(pos.y+(box.top-con.top)+10)+&#39;px&#39;;
        this.tip.style.display=&#39;block&#39;;
    }

Summary

What is done here is just a basic effect. In fact, there are still many areas that need to be further optimized, such as responsive support, mobile support, and animation. Effect, multi-y-axis support, display content effect, and support for polyline function, etc.

Related recommendations:

Html realizes the report effect of dynamically displaying color blocks (example code)

html5 generates a histogram Example code for (bar chart) effect


##

The above is the detailed content of Example of implementing histogram in canvas chart in html5. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn