繼上一篇HTML5 Canvas(實(shí)戰(zhàn):繪制餅圖)之后,筆者研究了一下如何給餅圖加鼠標(biāo)停留時(shí)顯示的提示框。
事件綁定
在開始Coding之前怨愤,筆者能夠想到的最easy的方式,就是給餅圖的每一個(gè)區(qū)域添加mousemove事件孝赫,鼠標(biāo)在其上移動(dòng)時(shí)則顯示對應(yīng)的提示框,so easy!可事實(shí)不是這樣子滴~
我們?nèi)庋凵峡瓷先ナ且粔K一塊的東西,canvas并沒有真的把它們分成一塊一塊的HTMLElement黎做,我們只能給canvas綁定事件。那么如何得知鼠標(biāo)當(dāng)前停留在哪塊區(qū)域呢松忍,可以通過計(jì)算鼠標(biāo)位置與圓心連線與基準(zhǔn)線給的夾角是否在區(qū)域的起始角度與終止角度之間蒸殿,為此,我們需要保存每個(gè)區(qū)域的角度信息挽铁。
為了方便保存,創(chuàng)建一個(gè)構(gòu)造函數(shù)Plot敞掘。
function Plot(start, end, color, data) {
this.start = start;
this.end = end;
this.color = color;
this.data = data;
}
可以將上一篇文章中的繪制圖例方法和繪制餅圖區(qū)域的方法都放進(jìn)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(實(shí)戰(zhàn):繪制餅圖) 可以看出叽掘,在我們的最初設(shè)計(jì)中,Tooltip上顯示的內(nèi)容是可以定制化的玖雁,用戶可以設(shè)定一個(gè)如下的模板:
Year: {{year}}, Data: {{data}}
我們的目標(biāo)是將上面的模板轉(zhuǎn)化成:
Year: 2017, Data: 3000
新建一個(gè)工具方法更扁,接受template
字符串,以及鼠標(biāo)當(dāng)前停留plot
中的數(shù)據(jù)赫冬,返回實(shí)際顯示的字符串:
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;
}
注意浓镜,從代碼中可以看出,不要習(xí)慣性的在{{
和}}
之間加入空格劲厌。
鼠標(biāo)在哪
為了判斷鼠標(biāo)停留的區(qū)域膛薛,我們需要完成如下兩步:
- 計(jì)算鼠標(biāo)位置和圓心之間的弧度
angle
- 遍歷
plots
,判斷angle
是否位于某一個(gè)plot
的startAngle
與endAngle
之間补鼻,如果找到了這個(gè)plot
哄啄,判斷這個(gè)plot
是否是上一次的鼠標(biāo)所在的區(qū)域,如果是风范,說明沒有必要繪制Tooltip咨跌,如果不是,重繪圖表硼婿。假如沒有找到對應(yīng)的區(qū)域锌半,說明鼠標(biāo)不在canvas的餅圖區(qū)域,可能指向圖例寇漫、標(biāo)題或者空白區(qū)域刊殉,此時(shí)應(yīng)該清空全局變量currentPlot
并重繪畫布殉摔。
關(guān)于如何判斷鼠標(biāo)位置與圓心之間的弧度,小編畫了如下的一個(gè)餅圖冗澈,只能幫到這兒了...
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();
}
現(xiàn)在我們知道了鼠標(biāo)當(dāng)前停留的位置钦勘,也可以定制要提示的文字,現(xiàn)在可以繪制提示框啦亚亲,以下代碼有些累贅彻采,計(jì)算過程也有些問題,筆者改天再重新算算~
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時(shí)都需要重繪餅圖捌归,而startAngle
endAngle
在每次繪制時(shí)都會(huì)修改肛响,因此繪制前需要重置。
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;
}
成品圖: