HTML5 Canvas(实战:绘制饼图)

有了canvas之后,我们可以很容易地创建一个简单图标,不需要任何插件,不过,有的小伙伴觉得它很难,笔者仔细思考一番之后,只能吐嘈一下他们的绘图技能...
于是在开始绘制之前,我们首先画一下草图~

Make It Reusable

为了创建一个可以重用,并且可以灵活地重用的饼图,笔者决定最终的创建饼图方法接收两个参数,分别是要显示的数据data,绘制参数options

Data

Data From Server

在实际应用场景中,我们从后端拿到的往往是诸如几个年份的产量一类的数据,比如(这里,我们为了简化代码,将颜色也放到了后台返回的数据中):

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

而绘制饼图时, 我们需要根据比例"分饼", 并且在某些地方显示出实际的数据(比如tooltip),因此我们需要一个如下的数据处理函数:

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

另外,即使我们可以根据不同的数据绘制不同的图表,恐怕也只能满足个别需求,毕竟每个人的喜好都不一样,我们需要创建一个可以显示不同数据,又可以拥有不同排版、不同布局的图表,实现上述目标,我们需要如下参数列表:

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

我们的工具函数不应该可以提前知道用户想要用来绘制图表的canvas,用户可能想在页面中的多个canvas上绘制图表,因此工具函数应该可以接受一个参数,用来确定绘制图表的canvas,很多开源库都使用id作为识别canvas的标识,笔者认为接收element更好一些,因为不是所有的用户都愿意给canvas添加ID属性, 有的时候,用户想给拥有某一个class属性的所有canvas批量绘图,并根据它们的dataset属性动态的生成数据。
综上,最后我们的工具函数应该长成下面这个样子:

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

然后,我们需要将自定义的参数和默认参数合并在一起,组成一个新的完整的参数列表,原则就是没有自定义的都采用默认值。

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

把标题绘制在画布顶部的中间,距离页面顶部留有20像素的空隙,并且根据参数,绘制具有特定内容和样式的标题。

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

笔者决定使饼图距离标题有30像素的空隙,距离左边框和底部分别留有20像素的空隙,因此它的半径和圆心分别是:

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

Legend

图例的高设置为图例字体大小的1.2倍,宽设置为图例字体大小的2.5倍,距离饼图40像素的间隙,第一个图例顶部距离页面顶端80像素,文字距离图例5像素,垂直居中,于是图例的大体信息总结如下:

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

遍历数据绘图。

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!

我们的工具函数已经做到一半啦,可以画出一个带有图例的饼图,并且标题和图例文字大小 粗细 字体均可配置,下面试一下灵不灵~

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的绘制哦,那部分比较复杂,默默地给自己加油~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容