canvas圖表(1) - 柱狀圖

原文地址:canvas圖表(1) - 柱狀圖
圖表一般使用到svg或canvas函匕,其中canvas圖表在處理大數(shù)據(jù)方面比svg要好践啄。我這里用canvas實現(xiàn)圖表庫衔蹲,模仿的是百度Echart,本章實現(xiàn)的是簡單的柱狀圖构罗。

效果請看:柱狀圖

chartbar

主要功能點包括:

  1. 文本的繪制
  2. XY軸的繪制;
  3. 數(shù)據(jù)分組繪制鲜屏;
  4. 數(shù)據(jù)動畫的實現(xiàn)吱抚;
  5. 鼠標(biāo)事件的處理。

使用方式

首先我們看一下使用方式,參考了部分ECharts的使用方式昙篙,先傳入要顯示圖表的html標(biāo)簽腊状,接著調(diào)用init,初始化的同時傳入數(shù)據(jù)苔可。

    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:[//分組數(shù)據(jù)
            {
                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]
            }
        ]
    });

圖表基類缴挖,我們后面還要寫餅圖,折線圖焚辅,所以把公共的部分抽出來映屋。注意canvas.style.width與canvas.width是不一樣的,前者會拉伸圖形同蜻,后者才是我們正常用的棚点,不會拉伸圖形。在這里這樣寫先擴(kuò)大再縮小是為了解決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';
        }
    }

柱狀圖初始化,調(diào)用es6中的Object.assign(this,opt)默责,這個相當(dāng)于JQ中的extend方法贬循,把屬性復(fù)制到當(dāng)前實例。同時還建了個tip屬性桃序,這是個html標(biāo)簽杖虾,后面顯示數(shù)據(jù)信息用。接著繪制圖形葡缰,然后綁定鼠標(biāo)事件亏掀。

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.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(){//執(zhí)行動畫

    }
    showData(){//顯示數(shù)據(jù)

    }

繪制XY軸

首先繪制標(biāo)題,接著XY軸泛释,然后遍歷分組數(shù)據(jù)series滤愕,里面有復(fù)雜的計算,然后繪制XY軸的刻度怜校,繪制分組標(biāo)簽间影,最后是繪制數(shù)據(jù)。數(shù)據(jù)項series中是分組數(shù)據(jù)茄茁,它跟X軸的xAxis.data一一對應(yīng)魂贬。每個項可以自定義名稱和顏色,沒有指定的話裙顽,名稱賦予nunamed和自動生成顏色付燥。這里還用legend屬性記錄下了標(biāo)簽列表信息,因為后續(xù)鼠標(biāo)點擊判斷是否點中用的上愈犹。
canvas主要知識點:

  1. 分組標(biāo)簽使用了arcTo方法键科,這樣就能繪制出圓角的效果闻丑。
  2. 繪制文本使用了measureText方法,可以用來測量文字所占寬度勋颖,這樣就可以調(diào)整下一次繪制的位置嗦嗡,避免位置沖突。
  3. translate位移方法饭玲,可以放在繪制上下文(save和restore的中間)中侥祭,這樣可以避免復(fù)雜的位置運算。
    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軸單位數(shù)茄厘,每個單位長度矮冬,x軸總長度
            yl=0,ys=0,ydis=H-padding*2-paddingTop;//y軸單位數(shù),每個單位長度次哈,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='hsla('+hsl+',70%,60%,1)';
                }
                item.name=item.name||'unnamed';

                // 畫分組標(biāo)簽
                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,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;
                //計算數(shù)據(jù)在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='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();
            //畫數(shù)據(jù)
            this.showData(xl,xs,info.max);
        }
    }

繪制數(shù)據(jù)

因為數(shù)據(jù)項需要后續(xù)執(zhí)行動畫和鼠標(biāo)滑過的時候顯示內(nèi)容欢伏,所以把它放進(jìn)動畫隊列animateArr中。這里要把分組數(shù)據(jù)展開亿乳,把之前的兩次嵌套的數(shù)組轉(zhuǎn)為一層,并計算好每個數(shù)據(jù)項的屬性径筏,比如名稱葛假,x坐標(biāo),y坐標(biāo)滋恬,寬度聊训,速度,顏色恢氯。數(shù)據(jù)組織完畢后带斑,接著執(zhí)行動畫。

    showData(xl,xs,max){
        //畫數(shù)據(jù)
        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;
        // 展開數(shù)據(jù)項勋拟,填入動畫隊列
        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();
    }

執(zhí)行動畫

執(zhí)行動畫也沒啥好說的勋磕,里面就是個自執(zhí)行閉包函數(shù)。動畫原理就是給y軸依次累加速度值vy敢靡。但記得當(dāng)隊列執(zhí)行完動畫后挂滓,要停止它,所以有個isStop的標(biāo)志啸胧,每次執(zhí)行完隊列的時候就判斷赶站。

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

綁定事件

事件一:mousemove的時候,看看鼠標(biāo)位置是不是處于分組標(biāo)簽還是數(shù)據(jù)項上纺念,繪制路徑后調(diào)用isPointInPath(x,y)贝椿,true則鼠標(biāo)設(shè)置為手形;如果是數(shù)據(jù)項的話陷谱,還要把該柱形重新繪制烙博,設(shè)置透明度,區(qū)分出來。還需要把內(nèi)容顯示出來习勤,這里是一個相對父容器container為絕對定位的div踪栋,初始化的時候已經(jīng)建立為tip屬性了。我們把顯示部分封裝成showInfo方法图毕。

事件二:mousedown的時候夷都,一樣繪制路徑調(diào)用isPointInPath判斷鼠標(biāo)點擊哪個分組標(biāo)簽,然后設(shè)置對應(yīng)分組數(shù)據(jù)series中的hide屬性予颤,如果是true囤官,表示不顯示該項,然后調(diào)用draw方法蛤虐,重寫渲染繪制党饮,執(zhí)行動畫。

    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
            };
            // 分組標(biāo)簽
            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);
                // 因為縮小了一倍驳庭,所以坐標(biāo)要*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;
            //選擇數(shù)據(jù)項
            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='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.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);
                // 因為縮小了一倍刑顺,所以坐標(biāo)要*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);
    }
    //顯示數(shù)據(jù)
    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';
    }

總結(jié)

所有圖表代碼請看chart.js。這里完成的只是個基本的效果饲常,其實還有很多地方要進(jìn)一步優(yōu)化蹲堂,比如響應(yīng)式的支持,移動端的支持贝淤,動畫的效果柒竞,多y軸的支持,顯示內(nèi)容的效果播聪,同時支持折線功能等朽基。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市离陶,隨后出現(xiàn)的幾起案子稼虎,更是在濱河造成了極大的恐慌,老刑警劉巖招刨,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渡蜻,死亡現(xiàn)場離奇詭異,居然都是意外死亡计济,警方通過查閱死者的電腦和手機(jī)茸苇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沦寂,“玉大人学密,你說我怎么就攤上這事〈兀” “怎么了腻暮?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵彤守,是天一觀的道長。 經(jīng)常有香客問我哭靖,道長具垫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任试幽,我火速辦了婚禮筝蚕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘铺坞。我一直安慰自己起宽,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布济榨。 她就那樣靜靜地躺著坯沪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪擒滑。 梳的紋絲不亂的頭發(fā)上腐晾,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機(jī)與錄音丐一,去河邊找鬼赴魁。 笑死,一個胖子當(dāng)著我的面吹牛钝诚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播榄棵,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼凝颇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了疹鳄?” 一聲冷哼從身側(cè)響起拧略,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瘪弓,沒想到半個月后垫蛆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡腺怯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年袱饭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呛占。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡虑乖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晾虑,到底是詐尸還是另有隱情疹味,我是刑警寧澤仅叫,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站糙捺,受9級特大地震影響诫咱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜洪灯,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一坎缭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧婴渡,春花似錦幻锁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至柠并,卻和暖如春岭接,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背臼予。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工鸣戴, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人粘拾。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓窄锅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缰雇。 傳聞我的和親對象是個殘疾皇子入偷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內(nèi)容