1018 lines
24 KiB
JavaScript
1018 lines
24 KiB
JavaScript
|
|
/*!
|
||
|
|
* @license
|
||
|
|
* chartjs-plugin-datalabels
|
||
|
|
* http://chartjs.org/
|
||
|
|
* Version: 0.3.0
|
||
|
|
*
|
||
|
|
* Copyright 2018 Chart.js Contributors
|
||
|
|
* Released under the MIT license
|
||
|
|
* https://github.com/chartjs/chartjs-plugin-datalabels/blob/master/LICENSE.md
|
||
|
|
*/
|
||
|
|
(function (global, factory) {
|
||
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('chart.js')) :
|
||
|
|
typeof define === 'function' && define.amd ? define(['chart.js'], factory) :
|
||
|
|
(factory(global.Chart));
|
||
|
|
}(this, (function (Chart) { 'use strict';
|
||
|
|
|
||
|
|
Chart = Chart && Chart.hasOwnProperty('default') ? Chart['default'] : Chart;
|
||
|
|
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
var helpers$2 = Chart.helpers;
|
||
|
|
|
||
|
|
var HitBox = function() {
|
||
|
|
this._rect = null;
|
||
|
|
this._rotation = 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
helpers$2.extend(HitBox.prototype, {
|
||
|
|
update: function(center, rect, rotation) {
|
||
|
|
var margin = 1;
|
||
|
|
var cx = center.x;
|
||
|
|
var cy = center.y;
|
||
|
|
var x = cx + rect.x;
|
||
|
|
var y = cy + rect.y;
|
||
|
|
|
||
|
|
this._rotation = rotation;
|
||
|
|
this._rect = {
|
||
|
|
x0: x - margin,
|
||
|
|
y0: y - margin,
|
||
|
|
x1: x + rect.w + margin * 2,
|
||
|
|
y1: y + rect.h + margin * 2,
|
||
|
|
cx: cx,
|
||
|
|
cy: cy,
|
||
|
|
};
|
||
|
|
},
|
||
|
|
|
||
|
|
contains: function(x, y) {
|
||
|
|
var me = this;
|
||
|
|
var rect = me._rect;
|
||
|
|
var cx, cy, r, rx, ry;
|
||
|
|
|
||
|
|
if (!rect) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
cx = rect.cx;
|
||
|
|
cy = rect.cy;
|
||
|
|
r = me._rotation;
|
||
|
|
rx = cx + (x - cx) * Math.cos(r) + (y - cy) * Math.sin(r);
|
||
|
|
ry = cy - (x - cx) * Math.sin(r) + (y - cy) * Math.cos(r);
|
||
|
|
|
||
|
|
return !(rx < rect.x0
|
||
|
|
|| ry < rect.y0
|
||
|
|
|| rx > rect.x1
|
||
|
|
|| ry > rect.y1);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
var helpers$3 = Chart.helpers;
|
||
|
|
|
||
|
|
var utils = {
|
||
|
|
// @todo move this in Chart.helpers.toTextLines
|
||
|
|
toTextLines: function(inputs) {
|
||
|
|
var lines = [];
|
||
|
|
var input;
|
||
|
|
|
||
|
|
inputs = [].concat(inputs);
|
||
|
|
while (inputs.length) {
|
||
|
|
input = inputs.pop();
|
||
|
|
if (typeof input === 'string') {
|
||
|
|
lines.unshift.apply(lines, input.split('\n'));
|
||
|
|
} else if (Array.isArray(input)) {
|
||
|
|
inputs.push.apply(inputs, input);
|
||
|
|
} else if (!helpers$3.isNullOrUndef(inputs)) {
|
||
|
|
lines.unshift('' + input);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return lines;
|
||
|
|
},
|
||
|
|
|
||
|
|
// @todo move this method in Chart.helpers.canvas.toFont (deprecates helpers.fontString)
|
||
|
|
// @see https://developer.mozilla.org/en-US/docs/Web/CSS/font
|
||
|
|
toFontString: function(font) {
|
||
|
|
if (!font || helpers$3.isNullOrUndef(font.size) || helpers$3.isNullOrUndef(font.family)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (font.style ? font.style + ' ' : '')
|
||
|
|
+ (font.weight ? font.weight + ' ' : '')
|
||
|
|
+ font.size + 'px '
|
||
|
|
+ font.family;
|
||
|
|
},
|
||
|
|
|
||
|
|
// @todo move this in Chart.helpers.canvas.textSize
|
||
|
|
// @todo cache calls of measureText if font doesn't change?!
|
||
|
|
textSize: function(ctx, lines, font) {
|
||
|
|
var items = [].concat(lines);
|
||
|
|
var ilen = items.length;
|
||
|
|
var prev = ctx.font;
|
||
|
|
var width = 0;
|
||
|
|
var i;
|
||
|
|
|
||
|
|
ctx.font = font.string;
|
||
|
|
|
||
|
|
for (i = 0; i < ilen; ++i) {
|
||
|
|
width = Math.max(ctx.measureText(items[i]).width, width);
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.font = prev;
|
||
|
|
|
||
|
|
return {
|
||
|
|
height: ilen * font.lineHeight,
|
||
|
|
width: width
|
||
|
|
};
|
||
|
|
},
|
||
|
|
|
||
|
|
// @todo move this method in Chart.helpers.options.toFont
|
||
|
|
parseFont: function(value) {
|
||
|
|
var global = Chart.defaults.global;
|
||
|
|
var size = helpers$3.valueOrDefault(value.size, global.defaultFontSize);
|
||
|
|
var font = {
|
||
|
|
family: helpers$3.valueOrDefault(value.family, global.defaultFontFamily),
|
||
|
|
lineHeight: helpers$3.options.toLineHeight(value.lineHeight, size),
|
||
|
|
size: size,
|
||
|
|
style: helpers$3.valueOrDefault(value.style, global.defaultFontStyle),
|
||
|
|
weight: helpers$3.valueOrDefault(value.weight, null),
|
||
|
|
string: ''
|
||
|
|
};
|
||
|
|
|
||
|
|
font.string = utils.toFontString(font);
|
||
|
|
return font;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns value bounded by min and max. This is equivalent to max(min, min(value, max)).
|
||
|
|
* @todo move this method in Chart.helpers.bound
|
||
|
|
* https://doc.qt.io/qt-5/qtglobal.html#qBound
|
||
|
|
*/
|
||
|
|
bound: function(min, value, max) {
|
||
|
|
return Math.max(min, Math.min(value, max));
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns an array of pair [value, state] where state is:
|
||
|
|
* * -1: value is only in a0 (removed)
|
||
|
|
* * 1: value is only in a1 (added)
|
||
|
|
*/
|
||
|
|
arrayDiff: function(a0, a1) {
|
||
|
|
var prev = a0.slice();
|
||
|
|
var updates = [];
|
||
|
|
var i, j, ilen, v;
|
||
|
|
|
||
|
|
for (i = 0, ilen = a1.length; i < ilen; ++i) {
|
||
|
|
v = a1[i];
|
||
|
|
j = prev.indexOf(v);
|
||
|
|
|
||
|
|
if (j === -1) {
|
||
|
|
updates.push([v, 1]);
|
||
|
|
} else {
|
||
|
|
prev.splice(j, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for (i = 0, ilen = prev.length; i < ilen; ++i) {
|
||
|
|
updates.push([prev[i], -1]);
|
||
|
|
}
|
||
|
|
|
||
|
|
return updates;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
function orient(point, origin) {
|
||
|
|
var x0 = origin.x;
|
||
|
|
var y0 = origin.y;
|
||
|
|
|
||
|
|
if (x0 === null) {
|
||
|
|
return {x: 0, y: -1};
|
||
|
|
}
|
||
|
|
if (y0 === null) {
|
||
|
|
return {x: 1, y: 0};
|
||
|
|
}
|
||
|
|
|
||
|
|
var dx = point.x - x0;
|
||
|
|
var dy = point.y - y0;
|
||
|
|
var ln = Math.sqrt(dx * dx + dy * dy);
|
||
|
|
return {
|
||
|
|
x: ln ? dx / ln : 0,
|
||
|
|
y: ln ? dy / ln : -1
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function aligned(x, y, vx, vy, align) {
|
||
|
|
switch (align) {
|
||
|
|
case 'center':
|
||
|
|
vx = vy = 0;
|
||
|
|
break;
|
||
|
|
case 'bottom':
|
||
|
|
vx = 0;
|
||
|
|
vy = 1;
|
||
|
|
break;
|
||
|
|
case 'right':
|
||
|
|
vx = 1;
|
||
|
|
vy = 0;
|
||
|
|
break;
|
||
|
|
case 'left':
|
||
|
|
vx = -1;
|
||
|
|
vy = 0;
|
||
|
|
break;
|
||
|
|
case 'top':
|
||
|
|
vx = 0;
|
||
|
|
vy = -1;
|
||
|
|
break;
|
||
|
|
case 'start':
|
||
|
|
vx = -vx;
|
||
|
|
vy = -vy;
|
||
|
|
break;
|
||
|
|
case 'end':
|
||
|
|
// keep the natural orientation
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
// clockwise rotation (in degree)
|
||
|
|
align *= (Math.PI / 180);
|
||
|
|
vx = Math.cos(align);
|
||
|
|
vy = Math.sin(align);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
x: x,
|
||
|
|
y: y,
|
||
|
|
vx: vx,
|
||
|
|
vy: vy
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
var positioners = {
|
||
|
|
arc: function(vm, anchor, align) {
|
||
|
|
var angle = (vm.startAngle + vm.endAngle) / 2;
|
||
|
|
var vx = Math.cos(angle);
|
||
|
|
var vy = Math.sin(angle);
|
||
|
|
var r0 = vm.innerRadius;
|
||
|
|
var r1 = vm.outerRadius;
|
||
|
|
var d;
|
||
|
|
|
||
|
|
if (anchor === 'start') {
|
||
|
|
d = r0;
|
||
|
|
} else if (anchor === 'end') {
|
||
|
|
d = r1;
|
||
|
|
} else {
|
||
|
|
d = (r0 + r1) / 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
return aligned(
|
||
|
|
vm.x + vx * d,
|
||
|
|
vm.y + vy * d,
|
||
|
|
vx,
|
||
|
|
vy,
|
||
|
|
align);
|
||
|
|
},
|
||
|
|
|
||
|
|
point: function(vm, anchor, align, origin) {
|
||
|
|
var v = orient(vm, origin);
|
||
|
|
var r = vm.radius;
|
||
|
|
var d = 0;
|
||
|
|
|
||
|
|
if (anchor === 'start') {
|
||
|
|
d = -r;
|
||
|
|
} else if (anchor === 'end') {
|
||
|
|
d = r;
|
||
|
|
}
|
||
|
|
|
||
|
|
return aligned(
|
||
|
|
vm.x + v.x * d,
|
||
|
|
vm.y + v.y * d,
|
||
|
|
v.x,
|
||
|
|
v.y,
|
||
|
|
align);
|
||
|
|
},
|
||
|
|
|
||
|
|
rect: function(vm, anchor, align, origin) {
|
||
|
|
var horizontal = vm.horizontal;
|
||
|
|
var size = Math.abs(vm.base - (horizontal ? vm.x : vm.y));
|
||
|
|
var x = horizontal ? Math.min(vm.x, vm.base) : vm.x;
|
||
|
|
var y = horizontal ? vm.y : Math.min(vm.y, vm.base);
|
||
|
|
var v = orient(vm, origin);
|
||
|
|
|
||
|
|
if (anchor === 'center') {
|
||
|
|
if (horizontal) {
|
||
|
|
x += size / 2;
|
||
|
|
} else {
|
||
|
|
y += size / 2;
|
||
|
|
}
|
||
|
|
} else if (anchor === 'start' && !horizontal) {
|
||
|
|
y += size;
|
||
|
|
} else if (anchor === 'end' && horizontal) {
|
||
|
|
x += size;
|
||
|
|
}
|
||
|
|
|
||
|
|
return aligned(
|
||
|
|
x,
|
||
|
|
y,
|
||
|
|
v.x,
|
||
|
|
v.y,
|
||
|
|
align);
|
||
|
|
},
|
||
|
|
|
||
|
|
fallback: function(vm, anchor, align, origin) {
|
||
|
|
var v = orient(vm, origin);
|
||
|
|
return aligned(
|
||
|
|
vm.x,
|
||
|
|
vm.y,
|
||
|
|
v.x,
|
||
|
|
v.y,
|
||
|
|
align);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
var helpers$1 = Chart.helpers;
|
||
|
|
|
||
|
|
function boundingRects(size, padding) {
|
||
|
|
var th = size.height;
|
||
|
|
var tw = size.width;
|
||
|
|
var tx = -tw / 2;
|
||
|
|
var ty = -th / 2;
|
||
|
|
|
||
|
|
return {
|
||
|
|
frame: {
|
||
|
|
x: tx - padding.left,
|
||
|
|
y: ty - padding.top,
|
||
|
|
w: tw + padding.width,
|
||
|
|
h: th + padding.height,
|
||
|
|
},
|
||
|
|
text: {
|
||
|
|
x: tx,
|
||
|
|
y: ty,
|
||
|
|
w: tw,
|
||
|
|
h: th
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function getScaleOrigin(el) {
|
||
|
|
var horizontal = el._model.horizontal;
|
||
|
|
var scale = el._scale || (horizontal && el._xScale) || el._yScale;
|
||
|
|
|
||
|
|
if (!scale) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (scale.xCenter !== undefined && scale.yCenter !== undefined) {
|
||
|
|
return {x: scale.xCenter, y: scale.yCenter};
|
||
|
|
}
|
||
|
|
|
||
|
|
var pixel = scale.getBasePixel();
|
||
|
|
return horizontal ?
|
||
|
|
{x: pixel, y: null} :
|
||
|
|
{x: null, y: pixel};
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPositioner(el) {
|
||
|
|
if (el instanceof Chart.elements.Arc) {
|
||
|
|
return positioners.arc;
|
||
|
|
}
|
||
|
|
if (el instanceof Chart.elements.Point) {
|
||
|
|
return positioners.point;
|
||
|
|
}
|
||
|
|
if (el instanceof Chart.elements.Rectangle) {
|
||
|
|
return positioners.rect;
|
||
|
|
}
|
||
|
|
return positioners.fallback;
|
||
|
|
}
|
||
|
|
|
||
|
|
function coordinates(el, model, rect) {
|
||
|
|
var point = model.positioner(el._view, model.anchor, model.align, model.origin);
|
||
|
|
var vx = point.vx;
|
||
|
|
var vy = point.vy;
|
||
|
|
|
||
|
|
if (!vx && !vy) {
|
||
|
|
// if aligned center, we don't want to offset the center point
|
||
|
|
return {x: point.x, y: point.y};
|
||
|
|
}
|
||
|
|
|
||
|
|
// include borders to the bounding rect
|
||
|
|
var borderWidth = model.borderWidth || 0;
|
||
|
|
var w = (rect.w + borderWidth * 2);
|
||
|
|
var h = (rect.h + borderWidth * 2);
|
||
|
|
|
||
|
|
// take in account the label rotation
|
||
|
|
var rotation = model.rotation;
|
||
|
|
var dx = Math.abs(w / 2 * Math.cos(rotation)) + Math.abs(h / 2 * Math.sin(rotation));
|
||
|
|
var dy = Math.abs(w / 2 * Math.sin(rotation)) + Math.abs(h / 2 * Math.cos(rotation));
|
||
|
|
|
||
|
|
// scale the unit vector (vx, vy) to get at least dx or dy equal to w or h respectively
|
||
|
|
// (else we would calculate the distance to the ellipse inscribed in the bounding rect)
|
||
|
|
var vs = 1 / Math.max(Math.abs(vx), Math.abs(vy));
|
||
|
|
dx *= vx * vs;
|
||
|
|
dy *= vy * vs;
|
||
|
|
|
||
|
|
// finally, include the explicit offset
|
||
|
|
dx += model.offset * vx;
|
||
|
|
dy += model.offset * vy;
|
||
|
|
|
||
|
|
return {
|
||
|
|
x: point.x + dx,
|
||
|
|
y: point.y + dy
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function drawFrame(ctx, rect, model) {
|
||
|
|
var bgColor = model.backgroundColor;
|
||
|
|
var borderColor = model.borderColor;
|
||
|
|
var borderWidth = model.borderWidth;
|
||
|
|
|
||
|
|
if (!bgColor && (!borderColor || !borderWidth)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.beginPath();
|
||
|
|
|
||
|
|
helpers$1.canvas.roundedRect(
|
||
|
|
ctx,
|
||
|
|
Math.round(rect.x) - borderWidth / 2,
|
||
|
|
Math.round(rect.y) - borderWidth / 2,
|
||
|
|
Math.round(rect.w) + borderWidth,
|
||
|
|
Math.round(rect.h) + borderWidth,
|
||
|
|
model.borderRadius);
|
||
|
|
|
||
|
|
ctx.closePath();
|
||
|
|
|
||
|
|
if (bgColor) {
|
||
|
|
ctx.fillStyle = bgColor;
|
||
|
|
ctx.fill();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (borderColor && borderWidth) {
|
||
|
|
ctx.strokeStyle = borderColor;
|
||
|
|
ctx.lineWidth = borderWidth;
|
||
|
|
ctx.lineJoin = 'miter';
|
||
|
|
ctx.stroke();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function drawText(ctx, lines, rect, model) {
|
||
|
|
var align = model.textAlign;
|
||
|
|
var font = model.font;
|
||
|
|
var lh = font.lineHeight;
|
||
|
|
var color = model.color;
|
||
|
|
var ilen = lines.length;
|
||
|
|
var x, y, i;
|
||
|
|
|
||
|
|
if (!ilen || !color) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
x = rect.x;
|
||
|
|
y = rect.y + lh / 2;
|
||
|
|
|
||
|
|
if (align === 'center') {
|
||
|
|
x += rect.w / 2;
|
||
|
|
} else if (align === 'end' || align === 'right') {
|
||
|
|
x += rect.w;
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.font = model.font.string;
|
||
|
|
ctx.fillStyle = color;
|
||
|
|
ctx.textAlign = align;
|
||
|
|
ctx.textBaseline = 'middle';
|
||
|
|
|
||
|
|
for (i = 0; i < ilen; ++i) {
|
||
|
|
ctx.fillText(
|
||
|
|
lines[i],
|
||
|
|
Math.round(x),
|
||
|
|
Math.round(y),
|
||
|
|
Math.round(rect.w));
|
||
|
|
|
||
|
|
y += lh;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var Label = function(config, ctx, el, index) {
|
||
|
|
var me = this;
|
||
|
|
|
||
|
|
me._hitbox = new HitBox();
|
||
|
|
me._config = config;
|
||
|
|
me._index = index;
|
||
|
|
me._model = null;
|
||
|
|
me._ctx = ctx;
|
||
|
|
me._el = el;
|
||
|
|
};
|
||
|
|
|
||
|
|
helpers$1.extend(Label.prototype, {
|
||
|
|
/**
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
_modelize: function(lines, config, context) {
|
||
|
|
var me = this;
|
||
|
|
var index = me._index;
|
||
|
|
var resolve = helpers$1.options.resolve;
|
||
|
|
var font = utils.parseFont(resolve([config.font, {}], context, index));
|
||
|
|
|
||
|
|
return {
|
||
|
|
align: resolve([config.align, 'center'], context, index),
|
||
|
|
anchor: resolve([config.anchor, 'center'], context, index),
|
||
|
|
backgroundColor: resolve([config.backgroundColor, null], context, index),
|
||
|
|
borderColor: resolve([config.borderColor, null], context, index),
|
||
|
|
borderRadius: resolve([config.borderRadius, 0], context, index),
|
||
|
|
borderWidth: resolve([config.borderWidth, 0], context, index),
|
||
|
|
color: resolve([config.color, Chart.defaults.global.defaultFontColor], context, index),
|
||
|
|
font: font,
|
||
|
|
lines: lines,
|
||
|
|
offset: resolve([config.offset, 0], context, index),
|
||
|
|
opacity: resolve([config.opacity, 1], context, index),
|
||
|
|
origin: getScaleOrigin(me._el),
|
||
|
|
padding: helpers$1.options.toPadding(resolve([config.padding, 0], context, index)),
|
||
|
|
positioner: getPositioner(me._el),
|
||
|
|
rotation: resolve([config.rotation, 0], context, index) * (Math.PI / 180),
|
||
|
|
size: utils.textSize(me._ctx, lines, font),
|
||
|
|
textAlign: resolve([config.textAlign, 'start'], context, index)
|
||
|
|
};
|
||
|
|
},
|
||
|
|
|
||
|
|
update: function(context) {
|
||
|
|
var me = this;
|
||
|
|
var model = null;
|
||
|
|
var index = me._index;
|
||
|
|
var config = me._config;
|
||
|
|
var value, label, lines;
|
||
|
|
|
||
|
|
if (helpers$1.options.resolve([config.display, true], context, index)) {
|
||
|
|
value = context.dataset.data[index];
|
||
|
|
label = helpers$1.valueOrDefault(helpers$1.callback(config.formatter, [value, context]), value);
|
||
|
|
lines = helpers$1.isNullOrUndef(label) ? [] : utils.toTextLines(label);
|
||
|
|
model = lines.length ? me._modelize(lines, config, context) : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
me._model = model;
|
||
|
|
},
|
||
|
|
|
||
|
|
draw: function(ctx) {
|
||
|
|
var me = this;
|
||
|
|
var model = me._model;
|
||
|
|
var rects, center;
|
||
|
|
|
||
|
|
if (!model || !model.opacity) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
rects = boundingRects(model.size, model.padding);
|
||
|
|
center = coordinates(me._el, model, rects.frame);
|
||
|
|
me._hitbox.update(center, rects.frame, model.rotation);
|
||
|
|
|
||
|
|
ctx.save();
|
||
|
|
ctx.globalAlpha = utils.bound(0, model.opacity, 1);
|
||
|
|
ctx.translate(Math.round(center.x), Math.round(center.y));
|
||
|
|
ctx.rotate(model.rotation);
|
||
|
|
|
||
|
|
drawFrame(ctx, rects.frame, model);
|
||
|
|
drawText(ctx, model.lines, rects.text, model);
|
||
|
|
|
||
|
|
ctx.restore();
|
||
|
|
},
|
||
|
|
|
||
|
|
contains: function(x, y) {
|
||
|
|
return this._hitbox.contains(x, y);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @module Options
|
||
|
|
*/
|
||
|
|
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
var helpers$4 = Chart.helpers;
|
||
|
|
|
||
|
|
var defaults = {
|
||
|
|
/**
|
||
|
|
* The label box alignment relative to `anchor` that can be expressed either by a number
|
||
|
|
* representing the clockwise angle (in degree) or by one of the following string presets:
|
||
|
|
* - 'start': before the anchor point, following the same direction
|
||
|
|
* - 'end': after the anchor point, following the same direction
|
||
|
|
* - 'center': centered on the anchor point
|
||
|
|
* - 'right': 0°
|
||
|
|
* - 'bottom': 90°
|
||
|
|
* - 'left': 180°
|
||
|
|
* - 'top': 270°
|
||
|
|
* @member {String|Number|Array|Function}
|
||
|
|
* @default 'center'
|
||
|
|
*/
|
||
|
|
align: 'center',
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The label box alignment relative to the element ('start'|'center'|'end')
|
||
|
|
* @member {String|Array|Function}
|
||
|
|
* @default 'center'
|
||
|
|
*/
|
||
|
|
anchor: 'center',
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The color used to draw the background of the surrounding frame.
|
||
|
|
* @member {String|Array|Function|null}
|
||
|
|
* @default null (no background)
|
||
|
|
*/
|
||
|
|
backgroundColor: null,
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The color used to draw the border of the surrounding frame.
|
||
|
|
* @member {String|Array|Function|null}
|
||
|
|
* @default null (no border)
|
||
|
|
*/
|
||
|
|
borderColor: null,
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The border radius used to add rounded corners to the surrounding frame.
|
||
|
|
* @member {Number|Array|Function}
|
||
|
|
* @default 0 (not rounded)
|
||
|
|
*/
|
||
|
|
borderRadius: 0,
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The border width of the surrounding frame.
|
||
|
|
* @member {Number|Array|Function}
|
||
|
|
* @default 0 (no border)
|
||
|
|
*/
|
||
|
|
borderWidth: 0,
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The color used to draw the label text.
|
||
|
|
* @member {String|Array|Function}
|
||
|
|
* @default undefined (use Chart.defaults.global.defaultFontColor)
|
||
|
|
*/
|
||
|
|
color: undefined,
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Whether to display labels global (boolean) or per data (function)
|
||
|
|
* @member {Boolean|Array|Function}
|
||
|
|
* @default true
|
||
|
|
*/
|
||
|
|
display: true,
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The font options used to draw the label text.
|
||
|
|
* @member {Object|Array|Function}
|
||
|
|
* @prop {String} font.family - defaults to Chart.defaults.global.defaultFontFamily
|
||
|
|
* @prop {Number} font.lineHeight - defaults to 1.2
|
||
|
|
* @prop {Number} font.size - defaults to Chart.defaults.global.defaultFontSize
|
||
|
|
* @prop {String} font.style - defaults to Chart.defaults.global.defaultFontStyle
|
||
|
|
* @prop {Number} font.weight - defaults to 'normal'
|
||
|
|
* @default Chart.defaults.global.defaultFont.*
|
||
|
|
*/
|
||
|
|
font: {
|
||
|
|
family: undefined,
|
||
|
|
lineHeight: 1.2,
|
||
|
|
size: undefined,
|
||
|
|
style: undefined,
|
||
|
|
weight: null
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The distance (in pixels) to pull the label away from the anchor point, the direction
|
||
|
|
* being determined by the `align` value (only applicable if `align` is `start` or `end`).
|
||
|
|
* @member {Number|Array|Function}
|
||
|
|
* @default 4
|
||
|
|
*/
|
||
|
|
offset: 4,
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The label global opacity, including the text, background, borders, etc., specified as
|
||
|
|
* a number between 0.0 (fully transparent) and 1.0 (fully opaque).
|
||
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalAlpha
|
||
|
|
* @member {Number|Array|Function}
|
||
|
|
* @default 1
|
||
|
|
*/
|
||
|
|
opacity: 1,
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The padding (in pixels) to apply between the text and the surrounding frame.
|
||
|
|
* @member {Number|Object|Array|Function}
|
||
|
|
* @prop {Number} padding.top - Space above the text.
|
||
|
|
* @prop {Number} padding.right - Space on the right of the text.
|
||
|
|
* @prop {Number} padding.bottom - Space below the text.
|
||
|
|
* @prop {Number} padding.left - Space on the left of the text.
|
||
|
|
* @default 4 (all values)
|
||
|
|
*/
|
||
|
|
padding: {
|
||
|
|
top: 4,
|
||
|
|
right: 4,
|
||
|
|
bottom: 4,
|
||
|
|
left: 4
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Clockwise rotation of the label relative to its center.
|
||
|
|
* @member {Number|Array|Function}
|
||
|
|
* @default 0
|
||
|
|
*/
|
||
|
|
rotation: 0,
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Text alignment for multi-lines labels ('left'|'right'|'start'|'center'|'end').
|
||
|
|
* @member {String|Array|Function}
|
||
|
|
* @default 'start'
|
||
|
|
*/
|
||
|
|
textAlign: 'start',
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Allows to customize the label text by transforming input data.
|
||
|
|
* @member {Function|null}
|
||
|
|
* @prop {*} value - The data value
|
||
|
|
* @prop {Object} context - The function unique argument:
|
||
|
|
* @prop {Chart} context.chart - The current chart
|
||
|
|
* @prop {Number} context.dataIndex - Index of the current data
|
||
|
|
* @prop {Object} context.dataset - The current dataset
|
||
|
|
* @prop {Number} context.datasetIndex - Index of the current dataset
|
||
|
|
* @default data[index]
|
||
|
|
*/
|
||
|
|
formatter: function(value) {
|
||
|
|
if (helpers$4.isNullOrUndef(value)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
var label = value;
|
||
|
|
var keys, klen, k;
|
||
|
|
if (helpers$4.isObject(value)) {
|
||
|
|
if (!helpers$4.isNullOrUndef(value.label)) {
|
||
|
|
label = value.label;
|
||
|
|
} else if (!helpers$4.isNullOrUndef(value.r)) {
|
||
|
|
label = value.r;
|
||
|
|
} else {
|
||
|
|
label = '';
|
||
|
|
keys = Object.keys(value);
|
||
|
|
for (k = 0, klen = keys.length; k < klen; ++k) {
|
||
|
|
label += (k !== 0 ? ', ' : '') + keys[k] + ': ' + value[keys[k]];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return '' + label;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Event listeners, where the property is the type of the event to listen and the value
|
||
|
|
* a callback with a unique `context` argument containing the same information as for
|
||
|
|
* scriptable options. If a callback explicitly returns `true`, the label is updated
|
||
|
|
* with the current context and the chart re-rendered. This allows to implement visual
|
||
|
|
* interactions with labels such as highlight, selection, etc.
|
||
|
|
*
|
||
|
|
* Event currently supported are:
|
||
|
|
* - 'click': a mouse click is detected within a label
|
||
|
|
* - 'enter': the mouse enters a label
|
||
|
|
* -' leave': the mouse leaves a label
|
||
|
|
*
|
||
|
|
* @member {Object}
|
||
|
|
* @default {}
|
||
|
|
*/
|
||
|
|
listeners: {}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @see https://github.com/chartjs/Chart.js/issues/4176
|
||
|
|
*/
|
||
|
|
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
var helpers = Chart.helpers;
|
||
|
|
var EXPANDO_KEY = '$datalabels';
|
||
|
|
|
||
|
|
Chart.defaults.global.plugins.datalabels = defaults;
|
||
|
|
|
||
|
|
function configure(dataset, options) {
|
||
|
|
var override = dataset.datalabels;
|
||
|
|
var config = {};
|
||
|
|
|
||
|
|
if (override === false) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (override === true) {
|
||
|
|
override = {};
|
||
|
|
}
|
||
|
|
|
||
|
|
return helpers.merge(config, [options, override]);
|
||
|
|
}
|
||
|
|
|
||
|
|
function drawLabels(chart, datasetIndex) {
|
||
|
|
var meta = chart.getDatasetMeta(datasetIndex);
|
||
|
|
var elements = meta.data || [];
|
||
|
|
var ilen = elements.length;
|
||
|
|
var i, el, label;
|
||
|
|
|
||
|
|
for (i = 0; i < ilen; ++i) {
|
||
|
|
el = elements[i];
|
||
|
|
label = el[EXPANDO_KEY];
|
||
|
|
if (label) {
|
||
|
|
label.draw(chart.ctx);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function labelAtXY(chart, x, y) {
|
||
|
|
var items = chart[EXPANDO_KEY].labels;
|
||
|
|
var i, j, labels, label;
|
||
|
|
|
||
|
|
// Until we support z-index, let's hit test in the drawing reverse order
|
||
|
|
for (i = items.length - 1; i >= 0; --i) {
|
||
|
|
labels = items[i] || [];
|
||
|
|
for (j = labels.length - 1; j >= 0; --j) {
|
||
|
|
label = labels[j];
|
||
|
|
if (label.contains(x, y)) {
|
||
|
|
return {dataset: i, label: label};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function dispatchEvent(chart, listeners, target) {
|
||
|
|
var callback = listeners && listeners[target.dataset];
|
||
|
|
if (!callback) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var label = target.label;
|
||
|
|
var context = label.$context;
|
||
|
|
|
||
|
|
if (helpers.callback(callback, [context]) === true) {
|
||
|
|
// Users are allowed to tweak the given context by injecting values that can be
|
||
|
|
// used in scriptable options to display labels differently based on the current
|
||
|
|
// event (e.g. highlight an hovered label). That's why we update the label with
|
||
|
|
// the output context and schedule a new chart render by setting it dirty.
|
||
|
|
chart[EXPANDO_KEY].dirty = true;
|
||
|
|
label.update(context);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function dispatchMoveEvents(chart, listeners, previous, target) {
|
||
|
|
var enter, leave;
|
||
|
|
|
||
|
|
if (!previous && !target) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!previous) {
|
||
|
|
enter = true;
|
||
|
|
} else if (!target) {
|
||
|
|
leave = true;
|
||
|
|
} else if (previous.label !== target.label) {
|
||
|
|
leave = enter = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (leave) {
|
||
|
|
dispatchEvent(chart, listeners.leave, previous);
|
||
|
|
}
|
||
|
|
if (enter) {
|
||
|
|
dispatchEvent(chart, listeners.enter, target);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleMoveEvents(chart, event) {
|
||
|
|
var expando = chart[EXPANDO_KEY];
|
||
|
|
var listeners = expando.listeners;
|
||
|
|
var previous, target;
|
||
|
|
|
||
|
|
if (!listeners.enter && !listeners.leave) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (event.type === 'mousemove') {
|
||
|
|
target = labelAtXY(chart, event.x, event.y);
|
||
|
|
} else if (event.type !== 'mouseout') {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
previous = expando.hovered;
|
||
|
|
expando.hovered = target;
|
||
|
|
dispatchMoveEvents(chart, listeners, previous, target);
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleClickEvents(chart, event) {
|
||
|
|
var handlers = chart[EXPANDO_KEY].listeners.click;
|
||
|
|
var target = handlers && labelAtXY(chart, event.x, event.y);
|
||
|
|
if (target) {
|
||
|
|
dispatchEvent(chart, handlers, target);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Chart.defaults.global.plugins.datalabels = defaults;
|
||
|
|
|
||
|
|
Chart.plugins.register({
|
||
|
|
id: 'datalabels',
|
||
|
|
|
||
|
|
beforeInit: function(chart) {
|
||
|
|
chart[EXPANDO_KEY] = {
|
||
|
|
actives: []
|
||
|
|
};
|
||
|
|
},
|
||
|
|
|
||
|
|
beforeUpdate: function(chart) {
|
||
|
|
var expando = chart[EXPANDO_KEY];
|
||
|
|
expando.listened = false;
|
||
|
|
expando.listeners = {}; // {event-type: {dataset-index: function}}
|
||
|
|
expando.labels = []; // [dataset-index: [labels]]
|
||
|
|
},
|
||
|
|
|
||
|
|
afterDatasetUpdate: function(chart, args, options) {
|
||
|
|
var datasetIndex = args.index;
|
||
|
|
var expando = chart[EXPANDO_KEY];
|
||
|
|
var labels = expando.labels[datasetIndex] = [];
|
||
|
|
var dataset = chart.data.datasets[datasetIndex];
|
||
|
|
var config = configure(dataset, options);
|
||
|
|
var elements = args.meta.data || [];
|
||
|
|
var ilen = elements.length;
|
||
|
|
var ctx = chart.ctx;
|
||
|
|
var i, el, label;
|
||
|
|
|
||
|
|
ctx.save();
|
||
|
|
|
||
|
|
for (i = 0; i < ilen; ++i) {
|
||
|
|
el = elements[i];
|
||
|
|
|
||
|
|
if (el && !el.hidden && !el._model.skip) {
|
||
|
|
labels.push(label = new Label(config, ctx, el, i));
|
||
|
|
label.update(label.$context = {
|
||
|
|
active: false,
|
||
|
|
chart: chart,
|
||
|
|
dataIndex: i,
|
||
|
|
dataset: dataset,
|
||
|
|
datasetIndex: datasetIndex
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
label = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
el[EXPANDO_KEY] = label;
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.restore();
|
||
|
|
|
||
|
|
// Store listeners at the chart level and per event type to optimize
|
||
|
|
// cases where no listeners are registered for a specific event
|
||
|
|
helpers.merge(expando.listeners, config.listeners || {}, {
|
||
|
|
merger: function(key, target, source) {
|
||
|
|
target[key] = target[key] || {};
|
||
|
|
target[key][args.index] = source[key];
|
||
|
|
expando.listened = true;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
// Draw labels on top of all dataset elements
|
||
|
|
// https://github.com/chartjs/chartjs-plugin-datalabels/issues/29
|
||
|
|
// https://github.com/chartjs/chartjs-plugin-datalabels/issues/32
|
||
|
|
afterDatasetsDraw: function(chart) {
|
||
|
|
for (var i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) {
|
||
|
|
drawLabels(chart, i);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
beforeEvent: function(chart, event) {
|
||
|
|
// If there is no listener registered for this chart, `listened` will be false,
|
||
|
|
// meaning we can immediately ignore the incoming event and avoid useless extra
|
||
|
|
// computation for users who don't implement label interactions.
|
||
|
|
if (chart[EXPANDO_KEY].listened) {
|
||
|
|
switch (event.type) {
|
||
|
|
case 'mousemove':
|
||
|
|
case 'mouseout':
|
||
|
|
handleMoveEvents(chart, event);
|
||
|
|
break;
|
||
|
|
case 'click':
|
||
|
|
handleClickEvents(chart, event);
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
afterEvent: function(chart) {
|
||
|
|
var expando = chart[EXPANDO_KEY];
|
||
|
|
var previous = expando.actives;
|
||
|
|
var actives = expando.actives = chart.lastActive || []; // public API?!
|
||
|
|
var updates = utils.arrayDiff(previous, actives);
|
||
|
|
var i, ilen, update, label;
|
||
|
|
|
||
|
|
for (i = 0, ilen = updates.length; i < ilen; ++i) {
|
||
|
|
update = updates[i];
|
||
|
|
if (update[1]) {
|
||
|
|
label = update[0][EXPANDO_KEY];
|
||
|
|
label.$context.active = (update[1] === 1);
|
||
|
|
label.update(label.$context);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ((expando.dirty || updates.length) && !chart.animating) {
|
||
|
|
chart.render();
|
||
|
|
}
|
||
|
|
|
||
|
|
delete expando.dirty;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
})));
|