使用JS繪制一個圓環(huán)統(tǒng)計圖

無意間翻到了之前某次考核做的一個環(huán)形統(tǒng)計圖历谍,今天又搗鼓了一下把它封裝成了一個類日川,可以根據(jù)不同的數(shù)據(jù)切換內(nèi)容淮阐,下面分享一下我的實現(xiàn)思路叮阅。

0.效果預(yù)覽

基本功能

  • 環(huán)形統(tǒng)計圖,按每一項數(shù)據(jù)自動分配比例泣特。
  • 只需簡單地更換數(shù)據(jù)便可以生成新的環(huán)形統(tǒng)計圖(統(tǒng)計圖大小也可以更改)浩姥。
  • 點擊每一項可以關(guān)閉該項,隱藏右側(cè)數(shù)據(jù)群扶,然后再重新生成及刻。

1.該示例的完整JS代碼pieChart.js

window.onload = function(){
    var data1 = [
        {"id":"item0","text":"這是第一行","num":10,"color":"#1e90ff","isdraw":1},
        {"id":"item1","text":"這是第二行","num":20,"color":"#36cbcb","isdraw":1},
        {"id":"item2","text":"這是第三行","num":30,"color":"#2fc25b","isdraw":1},
        {"id":"item3","text":"這是第四行","num":40,"color":"#ffd700","isdraw":1},
        {"id":"item4","text":"這是第五行","num":50,"color":"#ff3030","isdraw":1},
        {"id":"item5","text":"這是第六行","num":60,"color":"#8a2be2","isdraw":1},
    ];
    var apie = new pieChart('這里是標題',data1,150,40);
    apie.add_data();
    apie.draw();
}

function pieChart(title, data, radius, width){
    this.title = title;
    this.data = data;
    this.width = width;
    this.radius = radius;

    this.add_data = function(){
        var width = 2*radius;
        var chart = document.createElement('div');
        chart.style.width = width+'px';
        var top = document.createElement('div');
        top.setAttribute('style','text-align:center;font-weight:bold;width:'+width+'px');
        top.innerText = title;
        var circle = document.createElement('canvas');
        circle.setAttribute('id','circle');
        circle.setAttribute('width',width+"px");
        circle.setAttribute('height',width+"px");
        var list = document.createElement('div');
        list.setAttribute('id','list');
        var ul = document.createElement('ul');
        ul.setAttribute('style','font-family:Simsun;margin:0;padding:0;list-style:none;');
        for(var i=0; i<data.length; i++){
            var li = document.createElement('li');
            li.setAttribute('id',data[i].id);
            li.setAttribute('style','width:'+width+'px;');
            li.style.color = data[i].color;
            li.innerHTML = "<span>? </span><span style='color:black;'>"+data[i].text+"</span><span style='color:gray;float:right'>"+data[i].num+"</span>";
            li.onclick = this.draw;
            ul.appendChild(li);
        }
        list.appendChild(ul);
        chart.appendChild(top);
        chart.appendChild(circle);
        chart.appendChild(list);
        document.body.appendChild(chart);
    }
    this.draw = function(){
        var len = data.length;
        var id = this.id;
        for(var i=0; i<len; i++){
            if(data[i].id == id){
                data[i].isdraw = data[i].isdraw? 0:1;
                this.style.color = this.style.color == 'gray'? data[i].color:'gray';
                var span = this.children[2];
                span.style.display = span.style.display=='none'? 'inline':'none';  
            }
        }
        var canvas = document.getElementById('circle');
        canvas.height = canvas.height;
        if (canvas.getContext) {
            var ctx = canvas.getContext('2d');
            var PI = Math.PI;
            var start = PI*1.5;
            var gap = 0.01;
            var pros;
            var sum = 0;
            var zero = 0;
            for(var i=0; i<len; i++){
                if(data[i].isdraw == 0){
                    zero++;
                    continue;
                }
                sum+=data[i].num;
            }
            if(zero<len-1) {
                pros = 100-len+zero;
            }
            else{
                pros = 100;
            }
            ctx.strokeStyle = 'white';
            for(var i=0; i<len; i++) {
                var num = data[i].num;
                if(data[i].isdraw == 0){
                    continue;
                }
                var a_color= data[i].color;
                var end = start+2*PI*pros/100*num/sum;
                ctx.beginPath();
                ctx.moveTo(radius, radius);
                ctx.arc(radius, radius, radius, start, end, false);
                ctx.stroke();
                start = end + 2 * PI * gap;
                ctx.fillStyle = a_color;
                ctx.fill();
            }
            ctx.beginPath();
            ctx.arc(radius, radius, radius-width, 0, PI * 2, false);
            ctx.stroke();
            ctx.fillStyle = 'white';
            ctx.fill();
            ctx.fillStyle = 'black';
            ctx.font = (radius/5)+'px Simsun';
            ctx.fillText('總計',(canvas.width - ctx.measureText('總計').width)/2,canvas.height/2-radius/10);
            ctx.fillText(sum, (canvas.width - ctx.measureText(sum).width)/2,canvas.height/2+radius/10);
        }
    };
}

在一個空白html文件中導(dǎo)入該js文件即可使用(基本的html,body標簽要有),不需導(dǎo)入其他的css文件竞阐。新建一個pieChart對象缴饭,調(diào)用add_data()draw()方法即可。
下面是實現(xiàn)思路的分享骆莹。

2.實現(xiàn)思路

使用JS繪制圖形颗搂,那自然離不開canvas標簽,這里我們最終繪制的是一個圓環(huán)幕垦,我的大體思路是:

  1. 根據(jù)數(shù)據(jù)計算所占比例丢氢,再根據(jù)所占比例使用較大半徑繪制每部分的扇形。
  2. 所有部分繪制完畢先改,再使用一個較小的半徑繪制一個白底的完整的圓疚察。
  3. 填充文本。

也就是說仇奶,實際上我先是繪制的一個扇形統(tǒng)計圖貌嫡,然后用一個白底的較小的圓將其覆蓋,這樣看上去就是一個環(huán)形統(tǒng)計圖了。
下面結(jié)合代碼詳細講解岛抄;
window.onload外我封裝了一個pieChart類别惦,他有四個變量和兩個方法:

四個變量

title:統(tǒng)計圖的標題。

data:統(tǒng)計圖的數(shù)據(jù)夫椭,每一條數(shù)據(jù)含下面五個內(nèi)容:
①id:該項數(shù)據(jù)在html中的id值掸掸。
②text:該項數(shù)據(jù)在統(tǒng)計圖下方顯示的文本。
③num:該項數(shù)據(jù)的值(數(shù)量)蹭秋。
④color:該項數(shù)據(jù)在統(tǒng)計圖中對應(yīng)的顏色扰付。
⑤isdraw:是否繪制該項數(shù)據(jù),只能填為1或0感凤,默認填1悯周,表示要繪制(在點擊重繪時會用到該值)。

radius:外層圓的半徑陪竿,單位為px禽翼。

width:圓環(huán)寬度,單位為px族跛,可理解為外層圓與內(nèi)層圓半徑的差值闰挡。

兩個方法

add_data()方法:負責添加統(tǒng)計圖下方的每行內(nèi)容。

draw()方法:繪制圓環(huán)的方法礁哄,同時也會綁定到每一行數(shù)據(jù)中长酗。

3.方法詳解

add_data()

    this.add_data = function(){
        var width = 2*radius;//區(qū)域?qū)挾燃礊橹睆降拈L度,畫布區(qū)域(canvas)的寬高等于直徑桐绒,下方每一個li的寬度也等于直徑夺脾。
        /***div 'chart',為整個頁面的父div茉继。***/
        var chart = document.createElement('div');
        chart.style.width = width+'px';
        /***div 'top'咧叭,統(tǒng)計圖標題區(qū)域***/
        var top = document.createElement('div');
        top.setAttribute('style','text-align:center;font-weight:bold;width:'+width+'px');
        top.innerText = title;
        /***canvas 'circle',圓環(huán)區(qū)域***/
        var circle = document.createElement('canvas');
        circle.setAttribute('id','circle');
        circle.setAttribute('width',width+"px");
        circle.setAttribute('height',width+"px");
        /***div 'list'烁竭,數(shù)據(jù)行區(qū)域***/
        var list = document.createElement('div');
        list.setAttribute('id','list');
        var ul = document.createElement('ul');
        ul.setAttribute('style','font-family:Simsun;margin:0;padding:0;list-style:none;');
        /*每次循環(huán)添加data中的一條數(shù)據(jù)*/
        for(var i=0; i<data.length; i++){
            var li = document.createElement('li');
            li.setAttribute('id',data[i].id);
            li.setAttribute('style','width:'+width+'px;');
            li.style.color = data[i].color;
            li.innerHTML = "<span>? </span><span style='color:black;'>"+data[i].text+"</span><span style='color:gray;float:right'>"+data[i].num+"</span>";
            li.onclick = this.draw;//為每一行添加onclick事件
            ul.appendChild(li);
        }
        list.appendChild(ul);
        chart.appendChild(top);
        chart.appendChild(circle);
        chart.appendChild(list);
        document.body.appendChild(chart);
    }

實際上最終的頁面結(jié)構(gòu)為一個父div菲茬,包含三個部分:標題區(qū)域div 'top',圖形區(qū)域canvas 'circle'派撕,數(shù)據(jù)區(qū)域div 'list';數(shù)據(jù)區(qū)域div 'list'包含一個無序列表ul婉弹,根據(jù)創(chuàng)建對象的數(shù)據(jù)條數(shù)創(chuàng)建一個個li,向其寫入數(shù)據(jù)并綁定onclick事件draw();每個li又含三個span標簽终吼,分別代表:數(shù)據(jù)前綴(指定該行和統(tǒng)計圖中哪個顏色對應(yīng))镀赌,數(shù)據(jù)文本,文本對應(yīng)的數(shù)量际跪。
頁面結(jié)構(gòu)如下:

<div>
    <div></div>
    <canvas></canvas>
    <div>
        <ul>
            <li><span></span><span></span><span></span></li>
            <li><span></span><span></span><span></span></li>
            <li><span></span><span></span><span></span></li>
            <!--...-->
        </ul>
    </div>
</div>

draw()

代碼片段一:

        var id = this.id;
        for(var i=0; i<len; i++){
            if(data[i].id == id){
                data[i].isdraw = data[i].isdraw? 0:1;
                this.style.color = this.style.color == 'gray'? data[i].color:'gray';
                var span = this.children[2];
                span.style.display = span.style.display=='none'? 'inline':'none';  
            }
        }

實際上draw()是為數(shù)據(jù)區(qū)域中的每一個li量身定做的商佛,每一次對li標簽的點擊都會重繪圖形蛙粘,這時this指向的是被點擊的li標簽,如果它正在統(tǒng)計圖中顯示將會去除它威彰,前綴變?yōu)榛疑瑪?shù)據(jù)值隱藏穴肘,isdraw標記為0歇盼,接下來的重繪將跳過對此項的繪制。
雖說draw()方法是為li服務(wù)的评抚,但第一次生成統(tǒng)計圖時也不用擔心報錯豹缀,這時的this標簽并不會指向任何一個li標簽,id賦值為undefined慨代,雖然也會進入for循環(huán)邢笙,但始終不會進入if語句,對繪制不會產(chǎn)生任何影響侍匙。

代碼片段二:

canvas.height = canvas.height;

每次重繪都會先清空畫布氮惯,這里有個小技巧,重新設(shè)置畫布的寬度或高度都會使畫布清空想暗。

代碼片段三:

        /*繪制部分*/
        if (canvas.getContext) {
            var ctx = canvas.getContext('2d');
            var PI = Math.PI;
            var start = PI*1.5;//繪制開始位置
            var gap = 0.01;//兩項數(shù)據(jù)間取的間隙妇汗,每個間隙占比1%
            var pros;//除去空隙后內(nèi)容所占比例
            var sum = 0;//總和
            var zero = 0;//isdraw值為0數(shù)據(jù)個數(shù)(重繪過程不顯示的數(shù)據(jù)個數(shù))
            
            /*計算總和(sum)*/
            for(var i=0; i<len; i++){
                if(data[i].isdraw == 0){
                    zero++;
                    continue;
                }
                sum+=data[i].num;
            }
            
            /*在顯示數(shù)據(jù)數(shù)大于等于二時,間隙數(shù)等于數(shù)據(jù)數(shù)说莫,如只剩一個顯示數(shù)據(jù)杨箭,間隙數(shù)為0*/
            if(zero<len-1) {
                pros = 100-len+zero;
            }
            else{
                pros = 100;
            }
            ctx.strokeStyle = 'white';
            
            /*一次循環(huán)即完成一個扇形的繪制*/
            for(var i=0; i<len; i++) {
                var num = data[i].num;
                if(data[i].isdraw == 0){
                    continue;
                }
                var a_color= data[i].color;
                var end = start+2*PI*pros/100*num/sum;//繪制結(jié)束的位置
                ctx.beginPath();
                ctx.moveTo(radius, radius);//移動至圓心坐標
                ctx.arc(radius, radius, radius, start, end, false);
                ctx.stroke();
                start = end + 2 * PI * gap;//下一次繪制開始的位置
                ctx.fillStyle = a_color;
                ctx.fill();
            }

            /*繪制小圓覆蓋扇形的部分區(qū)域,使之最終為圓環(huán)的效果*/
            ctx.beginPath();
            ctx.arc(radius, radius, radius-width, 0, PI * 2, false);
            ctx.stroke();
            ctx.fillStyle = 'white';
            ctx.fill();

            /*填充文本*/
            ctx.fillStyle = 'black';
            ctx.font = (radius/5)+'px Simsun';
            ctx.fillText('總計',(canvas.width - ctx.measureText('總計').width)/2,canvas.height/2-radius/10);
            ctx.fillText(sum, (canvas.width - ctx.measureText(sum).width)/2,canvas.height/2+radius/10);
        }

務(wù)必理解到pros這個變量的意義储狭,在之前的預(yù)覽圖里大家可以看到互婿,每兩個數(shù)據(jù)間是有空隙的,每次扇形比例的計算都是在去掉這些空白區(qū)域的前提下進行的辽狈,pros為【全部內(nèi)容】在圓環(huán)上的比例慈参,即除去空隙部分后剩余部分所占比例。
當數(shù)據(jù)顯示項大于1時,間隙數(shù)等于數(shù)據(jù)數(shù)特姐,只剩一項數(shù)據(jù)顯示時用押,間隙數(shù)為0,該項內(nèi)容占比100%僧凤。
若你對canvas的用法不夠熟悉,建議先作一定了解元扔,過程中多百度也是很好的選擇躯保。

對代碼有疑問,歡迎評論澎语;若你有其他的實現(xiàn)一個圓環(huán)的方式途事,歡迎評論验懊;若你發(fā)現(xiàn)我文章中的錯誤,歡迎評論尸变!感激不盡~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末义图,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子召烂,更是在濱河造成了極大的恐慌碱工,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奏夫,死亡現(xiàn)場離奇詭異怕篷,居然都是意外死亡,警方通過查閱死者的電腦和手機酗昼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門廊谓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人麻削,你說我怎么就攤上這事蒸痹。” “怎么了呛哟?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵电抚,是天一觀的道長。 經(jīng)常有香客問我竖共,道長蝙叛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任公给,我火速辦了婚禮借帘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘淌铐。我一直安慰自己肺然,他們只是感情好,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布腿准。 她就那樣靜靜地躺著际起,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吐葱。 梳的紋絲不亂的頭發(fā)上街望,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機與錄音弟跑,去河邊找鬼灾前。 笑死,一個胖子當著我的面吹牛孟辑,可吹牛的內(nèi)容都是我干的哎甲。 我是一名探鬼主播蔫敲,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼炭玫!你這毒婦竟也來了奈嘿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤吞加,失蹤者是張志新(化名)和其女友劉穎指么,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體榴鼎,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年晚唇,在試婚紗的時候發(fā)現(xiàn)自己被綠了巫财。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡哩陕,死狀恐怖平项,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情悍及,我是刑警寧澤闽瓢,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站心赶,受9級特大地震影響扣讼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜缨叫,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一椭符、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧耻姥,春花似錦销钝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至婉商,卻和暖如春似忧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背丈秩。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工橡娄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人癣籽。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓挽唉,卻偏偏與公主長得像滤祖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瓶籽,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355