/*! * @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; } }); })));