HTML5 Canvas(實(shí)戰(zhàn):繪制餅圖)

有了canvas之后夸盟,我們可以很容易地創(chuàng)建一個簡單圖標(biāo)秽荞,不需要任何插件骤公,不過,有的小伙伴覺得它很難蚂会,筆者仔細(xì)思考一番之后淋样,只能吐嘈一下他們的繪圖技能...
于是在開始繪制之前,我們首先畫一下草圖~

Make It Reusable

為了創(chuàng)建一個可以重用胁住,并且可以靈活地重用的餅圖趁猴,筆者決定最終的創(chuàng)建餅圖方法接收兩個參數(shù),分別是要顯示的數(shù)據(jù)data彪见,繪制參數(shù)options

Data

Data From Server

在實(shí)際應(yīng)用場景中儡司,我們從后端拿到的往往是諸如幾個年份的產(chǎn)量一類的數(shù)據(jù),比如(這里余指,我們?yōu)榱撕喕a捕犬,將顏色也放到了后臺返回的數(shù)據(jù)中):

var data = [
              {
                data: 10,
                color: "red",
                label: "2016"
              },
              {
                data: 15,
                color: "grey",
                label: "2017"
              },
              {
                data: 15,
                color: "black",
                label: "2018"
              }
];
To Process Data

而繪制餅圖時, 我們需要根據(jù)比例"分餅"酵镜, 并且在某些地方顯示出實(shí)際的數(shù)據(jù)(比如tooltip)碉碉,因此我們需要一個如下的數(shù)據(jù)處理函數(shù):

function calculateData(data) {
  if(data instanceof Array) {
    var sum = data.reduce(function(a, b) {
      return a + b.data;
    }, 0);
    var map = data.map(function(a) {
      return {
        label: a.label,
        data: a.data,
        color: a.color,
        portion: a.data/sum
      }
    });
    return map;
  }      
}

Options

另外,即使我們可以根據(jù)不同的數(shù)據(jù)繪制不同的圖表淮韭,恐怕也只能滿足個別需求垢粮,畢竟每個人的喜好都不一樣,我們需要創(chuàng)建一個可以顯示不同數(shù)據(jù)靠粪,又可以擁有不同排版蜡吧、不同布局的圖表,實(shí)現(xiàn)上述目標(biāo)占键,我們需要如下參數(shù)列表:

var options = {
    legend: {
        font: {
          size: 18,
          family: 'Arial',
          weight: 'bold'
        }
    },
    title: {
        text: 'Pie Chart',
        font: {
          size: 18,
          family: 'Arial',
          weight: 'bold'
        }
    },
    tooltip: {
        template: '<div>Year: {{label}}</div><div>Production: {{data}}</div>',
        font: {
          size: 18,
          family: 'Arial',
          weight: 'bold'
        }
    }
}

Canvas

我們的工具函數(shù)不應(yīng)該可以提前知道用戶想要用來繪制圖表的canvas昔善,用戶可能想在頁面中的多個canvas上繪制圖表,因此工具函數(shù)應(yīng)該可以接受一個參數(shù)畔乙,用來確定繪制圖表的canvas君仆,很多開源庫都使用id作為識別canvas的標(biāo)識,筆者認(rèn)為接收element更好一些,因?yàn)椴皇撬械挠脩舳荚敢饨ocanvas添加ID屬性返咱, 有的時候氮帐,用戶想給擁有某一個class屬性的所有canvas批量繪圖,并根據(jù)它們的dataset屬性動態(tài)的生成數(shù)據(jù)洛姑。
綜上,最后我們的工具函數(shù)應(yīng)該長成下面這個樣子:

function drawPie(canvas, data, option) {
// To Do
}

Start Coding

Get Context

首先獲取繪圖上下文皮服,仍要注意先判斷是否存在getContext()方法楞艾。

var canvas = document.getElementById("canvas");
if(canvas.getContext) {
  var ctx = canvas.getContext("2d");
}

Generate Options

然后,我們需要將自定義的參數(shù)和默認(rèn)參數(shù)合并在一起龄广,組成一個新的完整的參數(shù)列表硫眯,原則就是沒有自定義的都采用默認(rèn)值。

function mergeJSON(source1,source2){
  var mergedJSON = JSON.parse(JSON.stringify(source2));
  for (var attrname in source1) {
    if(mergedJSON.hasOwnProperty(attrname)) {
      if ( source1[attrname]!=null && source1[attrname].constructor==Object ) {
        mergedJSON[attrname] = mergeJSON(source1[attrname], mergedJSON[attrname]);
      }
    } else {
      mergedJSON[attrname] = source1[attrname];
    }
  }
    return mergedJSON;
}

function generateOptions(givenOptions, defaultOptions) {
  return mergeJSON(defaultOptions, givenOptions);
}

Draw Title

把標(biāo)題繪制在畫布頂部的中間择同,距離頁面頂部留有20像素的空隙两入,并且根據(jù)參數(shù),繪制具有特定內(nèi)容和樣式的標(biāo)題敲才。

var width = canvas.width,
    height = canvas.height,
    op = generateOptions(options, defaultOptions),
    title_text = op.title.text裹纳,
    title_position = {};
ctx.font = op.title.font.weight + " " + op.title.font.size+"px " + op.title.font.family;
title_position .x = (width - title_width)/2;
title_position.y = 20 + op.title.font.size;
title_width = ctx.measureText(title_text).width, title_height = op.title.font.size;
ctx.fillText(title_text, title_position.x, title_position.y);

Radius & Center

筆者決定使餅圖距離標(biāo)題有30像素的空隙,距離左邊框和底部分別留有20像素的空隙紧武,因此它的半徑和圓心分別是:

var radius = (height - title_height - title_position.y - 20) / 2 ;
var center = {
  x: radius + 20,
  y: radius + 30 + title_position.y
};

Legend

圖例的高設(shè)置為圖例字體大小的1.2倍剃氧,寬設(shè)置為圖例字體大小的2.5倍,距離餅圖40像素的間隙阻星,第一個圖例頂部距離頁面頂端80像素朋鞍,文字距離圖例5像素,垂直居中妥箕,于是圖例的大體信息總結(jié)如下:

var legend_width = op.legend.font.size * 2.5, 
    legend_height = op.legend.font.size * 1.2,
    legend_posX = center.x * 2 +20, 
    legend_posY = 80,
    legend_textX = legend_posX + legend_width + 5, 
    legend_textY = legend_posY + op.legend.font.size * 0.9;

Draw Pie & Legends

Border

先給圖表加一個邊框

  ctx.strokeStyle = 'grey';
  ctx.lineWidth = 3;
  ctx.strokeRect(0, 0, canvas.width, canvas.height);
Pie & Legends

遍歷數(shù)據(jù)繪圖滥酥。

var data_c = calculateData(data);
var startAngle = 0, endAngle = 0;
for(var i=0, len=data.length; i<len; i++) {
    endAngle += data_c[i].portion * 2*Math.PI;
    ctx.fillStyle = data_c[i].color;
    ctx.beginPath();
    ctx.moveTo(center.x, center.y);
    ctx.arc(center.x, center.y, radius, startAngle, endAngle, false);
    ctx.closePath();
    ctx.fill();
    startAngle = endAngle;
    ctx.fillRect(legend_posX, legend_posY + (10 + legend_height) * i, legend_width, legend_height);
    ctx.font = 'bold 12px Arial';
    var percent = data_c[i].label + ' : ' + (data_c[i].portion*100).toFixed(2) + '%';
    ctx.fillText(percent, legend_textX, legend_textY + (10 + legend_height) * i);
}

Let's try it!

我們的工具函數(shù)已經(jīng)做到一半啦,可以畫出一個帶有圖例的餅圖畦幢,并且標(biāo)題和圖例文字大小 粗細(xì) 字體均可配置坎吻,下面試一下靈不靈~

var init = function(){
    var data = [
              {
                data: 10,
                color: "red",
                label: "2016"
              },
              {
                data: 15,
                color: "grey",
                label: "2017"
              },
              {
                data: 15,
                color: "black",
                label: "2018"
              }
    ];
    var options = {
        title: {
            text: 'Production By Year',
            font: {
                  size: 30
            }
        }
    }
    drawCircle(data, document.getElementById("drawing"), options);
};
init();

畫出來的餅圖長這個樣子~

下一篇筆者會加上Tooltip的繪制哦,那部分比較復(fù)雜呛讲,默默地給自己加油~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末禾怠,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子贝搁,更是在濱河造成了極大的恐慌吗氏,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雷逆,死亡現(xiàn)場離奇詭異弦讽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進(jìn)店門往产,熙熙樓的掌柜王于貴愁眉苦臉地迎上來被碗,“玉大人,你說我怎么就攤上這事仿村∪衿樱” “怎么了?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵蔼囊,是天一觀的道長焚志。 經(jīng)常有香客問我,道長畏鼓,這世上最難降的妖魔是什么酱酬? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮云矫,結(jié)果婚禮上膳沽,老公的妹妹穿的比我還像新娘。我一直安慰自己让禀,他們只是感情好挑社,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著巡揍,像睡著了一般滔灶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吼肥,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天录平,我揣著相機(jī)與錄音,去河邊找鬼缀皱。 笑死斗这,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的啤斗。 我是一名探鬼主播表箭,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼钮莲!你這毒婦竟也來了免钻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤崔拥,失蹤者是張志新(化名)和其女友劉穎极舔,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體链瓦,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拆魏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年盯桦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渤刃。...
    茶點(diǎn)故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡拥峦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出卖子,到底是詐尸還是另有隱情略号,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布洋闽,位于F島的核電站璃哟,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏喊递。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一阳似、第九天 我趴在偏房一處隱蔽的房頂上張望骚勘。 院中可真熱鬧,春花似錦撮奏、人聲如沸俏讹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泽疆。三九已至,卻和暖如春玲献,著一層夾襖步出監(jiān)牢的瞬間殉疼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工捌年, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瓢娜,地道東北人。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓礼预,卻偏偏與公主長得像眠砾,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子托酸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評論 2 355

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