laxexue 2019-06-27
继上一篇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(); }
在上一篇文章 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; }
注意,从代码中可以看出,不要习惯性的在{{
和}}
之间加入空格。
为了判断鼠标停留的区域,我们需要完成如下两步:
angle
plots
,判断angle
是否位于某一个plot
的startAngle
与endAngle
之间,如果找到了这个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; }
成品图: