HTML5 Canvas(实战:绘制饼图2 Tooltip)

继上一篇HTML5 Canvas(实战:绘制饼图)之后,笔者研究了一下如何给饼图加鼠标停留时显示的提示框。

事件绑定

在开始Coding之前,笔者能够想到的最easy的方式,就是给饼图的每一个区域添加mousemove事件,鼠标在其上移动时则显示对应的提示框,so easy!可事实不是这样子滴~

我们肉眼上看上去是一块一块的东西,canvas并没有真的把它们分成一块一块的HTMLElement,我们只能给canvas绑定事件。那么如何得知鼠标当前停留在哪块区域呢,可以通过计算鼠标位置与圆心连线与基准线给的夹角是否在区域的起始角度与终止角度之间,为此,我们需要保存每个区域的角度信息。
为了方便保存,创建一个构造函数Plot。

function Plot(start, end, color, data) {
  this.start = start;
  this.end = end;
  this.color = color;
  this.data = data;
}

可以将上一篇文章中的绘制图例方法和绘制饼图区域的方法都放进Plot的原型链中


Plot.prototype.drawLegend = function() {
  ctx.fillRect(legend_posX, legend_posY, legend_width, legend_height);
  ctx.font = 'bold 12px Arial';
  var percent = this.data.label + ' : ' + (this.data.portion * 100).toFixed(2) + '%';
  ctx.fillText(percent, legend_textX, legend_textY);
}
Plot.prototype.drawPlot = function() {
  ctx.fillStyle = this.color;
  ctx.beginPath();
  ctx.moveTo(center.x, center.y);
  ctx.arc(center.x, center.y, radius, this.start, this.end, false);
  ctx.closePath();
  ctx.fill();
}

定制的Tooltip

在上一篇文章 HTML5 Canvas(实战:绘制饼图) 可以看出,在我们的最初设计中,Tooltip上显示的内容是可以定制化的,用户可以设定一个如下的模板:

Year: {{year}}, Data: {{data}}

我们的目标是将上面的模板转化成:

Year: 2017, Data: 3000

新建一个工具方法,接受template字符串,以及鼠标当前停留plot中的数据,返回实际显示的字符串:

function replaceAttr(text, data) {
    while (text.indexOf("{{") != -1) {
      var start = text.indexOf("{{"),
          end = text.indexOf("}}"),
          attr = text.substring(start + 2, end);
      text = text.replace("{{" + attr + "}}", data[attr]);
    }
    return text;
}

注意,从代码中可以看出,不要习惯性的在{{}}之间加入空格。

鼠标在哪

为了判断鼠标停留的区域,我们需要完成如下两步:

  1. 计算鼠标位置和圆心之间的弧度angle
  2. 遍历plots,判断angle是否位于某一个plotstartAngleendAngle之间,如果找到了这个plot,判断这个plot是否是上一次的鼠标所在的区域,如果是,说明没有必要绘制Tooltip,如果不是,重绘图表。假如没有找到对应的区域,说明鼠标不在canvas的饼图区域,可能指向图例、标题或者空白区域,此时应该清空全局变量currentPlot并重绘画布。
    关于如何判断鼠标位置与圆心之间的弧度,小编画了如下的一个饼图,只能帮到这儿了...
function getAngle(cx, cy, mx, my) {
    var x = Math.abs(cx - mx),
        y = Math.abs(cy - my),
        z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)),
        cos = y / z,
        radina = Math.acos(cos);

    if (mx > cx && my > cy) {
      return radina;
    } else if (mx < cx && my > cy) {
      return Math.PI / 2 + radina;
    } else if (mx > cx && my < cy) {
      return 3 * Math.PI / 2 - radina
    } else {
      return 3 * Math.PI / 2 + radina
    }
}
function onMouseMove(e) {
    var ex = e.pageX - cv.offsetLeft,
        ey = e.pageY - cv.offsetTop;
    var angle = getAngle(center.x, center.y, ex, ey);
    for (let i = 0; i < plots.length; i++) {
      if (plots[i].start < angle && plots[i].end > angle) {
        if (currentPlot != plots[i]) {
          currentPlot = plots[i];
          draw();
        }
        return;
      }
    }
    currentPlot = null;
    draw();
  }

现在我们知道了鼠标当前停留的位置,也可以定制要提示的文字,现在可以绘制提示框啦,以下代码有些累赘,计算过程也有些问题,笔者改天再重新算算~

Plot.prototype.drawTooltip = function() {
    var text = replaceAttr(op.tooltip.template, this.data);
    var width_tooltipText = ctx.measureText(text).width,
        height_tooltipText = parseInt(op.tooltip.font.size, 10),
        angle = (this.start + this.end) / 2 / (2 * Math.PI) *360;
    var tan = Math.tanh(angle),
        x = 0,
        y = 0;

    if (angle < 90)((x = radius / 2 * tan + center.x) || true) && ((y = -radius / 2 + center.y) || true)
    else if (angle > 90 && angle < 180)((x = radius / 2 * tan + center.x) || true) && ((y = radius / 2 + center.y) || true)
    else if (angle > 180 && angle < 270)((x = -radius / 2 * tan + center.x) || true) && ((y = radius / 2 + center.y) || true)
    else if (angle > 270 && angle < 360)((x = -radius / 2 * tan + center.x) || true) && ((y = -radius / 2 + center.y) || true)
    var tooltip_box_x = x - radius / 4,
        tooltip_box_y = y,
        tooltip_box_width = width_tooltipText + 10,
        tooltip_box_height = height_tooltipText + 10,
        tooltip_text_x = x - radius / 4 + 5,
        tooltip_text_y = y + 10 + 2;
    ctx.fillStyle = 'white';
    ctx.fillRect(tooltip_box_x, tooltip_box_y, tooltip_box_width, tooltip_box_height);
    ctx.fillStyle = '#000';
    ctx.fillText(text, tooltip_text_x, tooltip_text_y);
}

每次重绘Tooltip时都需要重绘饼图,而startAngle endAngle在每次绘制时都会修改,因此绘制前需要重置。

function clear() {
    ctx.clearRect(0, 0, cv.width, cv.height);
    startAngle = 0;
    endAngle = 0;
    cv.onmousemove = null;
}

最终我们的draw方法~

function draw() {
    clear();
    title_text = op.title.text;
    ctx.font = op.title.font.weight + " " + op.title.font.size + "px " + op.title.font.family;
    title_width = ctx.measureText(title_text).width;
    title_height = op.title.font.size;
    title_position = {
      x: (width, title_width) / 2,
      y: 20 + title_height
    };
    ctx.fillText(title_text, title_position.x, title_position.y);
    radius = (height - title_height - title_position.y - 20) / 2;
    center = {
      x: radius + 20,
      y: radius + 30 + title_position.y
    };
    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;
    ctx.strokeStyle = 'grey';
    ctx.lineWidth = 3;
    ctx.strokeRect(0, 0, width, height);

    for (var i = 0, len = data_c.length; i < len; i++) {
      endAngle += data_c[i].portion * 2 * Math.PI;
      var plot = new Plot(startAngle, endAngle, data_c[i].color, data_c[i])
      plots.push(plot);
      plot.drawPlot();
      startAngle = endAngle;
      legend_posY += (10 + legend_height);
      legend_textY += (10 + legend_height);
      plot.drawLegend();
    }
    if (currentPlot) {
      currentPlot.drawTooltip();
    }
    cv.onmousemove = onMouseMove;
}

成品图:


源码地址:https://github.com/Sue1024/canvas2

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

推荐阅读更多精彩内容

  • 有了canvas之后,我们可以很容易地创建一个简单图标,不需要任何插件,不过,有的小伙伴觉得它很难,笔者仔细思考一...
    Sue1024阅读 6,301评论 1 3
  • 一:canvas简介 1.1什么是canvas? ①:canvas是HTML5提供的一种新标签 ②:HTML5 ...
    GreenHand1阅读 4,676评论 2 32
  • 一、canvas简介 1.1 什么是canvas?(了解) 是HTML5提供的一种新标签 Canvas是一个矩形区...
    Looog阅读 3,940评论 3 40
  • 一、canvas简介 1.1 什么是canvas?(了解) 是HTML5提供的一种新标签 Canvas是一个矩形区...
    J_L_L阅读 1,513评论 0 4
  • 桃花在宴里盛开,留下一地嫣红,我在风雨中飘零,内心依然坚定——赵晓旭 打开知乎、天涯、百度,几乎到处都是关于安全感...
    旭公子的半满人生阅读 932评论 0 0