/*! * chartjs-plugin-datalabels v0.6.0 * https://chartjs-plugin-datalabels.netlify.com * (c) 2019 Chart.js Contributors * Released under the MIT license */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('chart.js')) : typeof define === 'function' && define.amd ? define(['chart.js'], factory) : (global = global || self, global.ChartDataLabels = factory(global.Chart)); }(this, function (Chart) { 'use strict'; Chart = Chart && Chart.hasOwnProperty('default') ? Chart['default'] : Chart; var helpers = Chart.helpers; var devicePixelRatio = (function() { if (typeof window !== 'undefined') { if (window.devicePixelRatio) { return window.devicePixelRatio; } // devicePixelRatio is undefined on IE10 // https://stackoverflow.com/a/20204180/8837887 // https://github.com/chartjs/chartjs-plugin-datalabels/issues/85 var screen = window.screen; if (screen) { return (screen.deviceXDPI || 1) / (screen.logicalXDPI || 1); } } return 1; }()); 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.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.isNullOrUndef(font.size) || helpers.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.valueOrDefault(value.size, global.defaultFontSize); var font = { family: helpers.valueOrDefault(value.family, global.defaultFontFamily), lineHeight: helpers.options.toLineHeight(value.lineHeight, size), size: size, style: helpers.valueOrDefault(value.style, global.defaultFontStyle), weight: helpers.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; }, /** * https://github.com/chartjs/chartjs-plugin-datalabels/issues/70 */ rasterize: function(v) { return Math.round(v * devicePixelRatio) / devicePixelRatio; } }; 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 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 }; } // Line clipping (Cohen–Sutherland algorithm) // https://en.wikipedia.org/wiki/Cohen–Sutherland_algorithm var R_INSIDE = 0; var R_LEFT = 1; var R_RIGHT = 2; var R_BOTTOM = 4; var R_TOP = 8; function region(x, y, rect) { var res = R_INSIDE; if (x < rect.left) { res |= R_LEFT; } else if (x > rect.right) { res |= R_RIGHT; } if (y < rect.top) { res |= R_TOP; } else if (y > rect.bottom) { res |= R_BOTTOM; } return res; } function clipped(segment, area) { var x0 = segment.x0; var y0 = segment.y0; var x1 = segment.x1; var y1 = segment.y1; var r0 = region(x0, y0, area); var r1 = region(x1, y1, area); var r, x, y; // eslint-disable-next-line no-constant-condition while (true) { if (!(r0 | r1) || (r0 & r1)) { // both points inside or on the same side: no clipping break; } // at least one point is outside r = r0 || r1; if (r & R_TOP) { x = x0 + (x1 - x0) * (area.top - y0) / (y1 - y0); y = area.top; } else if (r & R_BOTTOM) { x = x0 + (x1 - x0) * (area.bottom - y0) / (y1 - y0); y = area.bottom; } else if (r & R_RIGHT) { y = y0 + (y1 - y0) * (area.right - x0) / (x1 - x0); x = area.right; } else if (r & R_LEFT) { y = y0 + (y1 - y0) * (area.left - x0) / (x1 - x0); x = area.left; } if (r === r0) { x0 = x; y0 = y; r0 = region(x0, y0, area); } else { x1 = x; y1 = y; r1 = region(x1, y1, area); } } return { x0: x0, x1: x1, y0: y0, y1: y1 }; } function compute(range, config) { var anchor = config.anchor; var segment = range; var x, y; if (config.clamp) { segment = clipped(segment, config.area); } if (anchor === 'start') { x = segment.x0; y = segment.y0; } else if (anchor === 'end') { x = segment.x1; y = segment.y1; } else { x = (segment.x0 + segment.x1) / 2; y = (segment.y0 + segment.y1) / 2; } return aligned(x, y, range.vx, range.vy, config.align); } var positioners = { arc: function(vm, config) { 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; return compute({ x0: vm.x + vx * r0, y0: vm.y + vy * r0, x1: vm.x + vx * r1, y1: vm.y + vy * r1, vx: vx, vy: vy }, config); }, point: function(vm, config) { var v = orient(vm, config.origin); var rx = v.x * vm.radius; var ry = v.y * vm.radius; return compute({ x0: vm.x - rx, y0: vm.y - ry, x1: vm.x + rx, y1: vm.y + ry, vx: v.x, vy: v.y }, config); }, rect: function(vm, config) { var v = orient(vm, config.origin); var x = vm.x; var y = vm.y; var sx = 0; var sy = 0; if (vm.horizontal) { x = Math.min(vm.x, vm.base); sx = Math.abs(vm.base - vm.x); } else { y = Math.min(vm.y, vm.base); sy = Math.abs(vm.base - vm.y); } return compute({ x0: x, y0: y + sy, x1: x + sx, y1: y, vx: v.x, vy: v.y }, config); }, fallback: function(vm, config) { var v = orient(vm, config.origin); return compute({ x0: vm.x, y0: vm.y, x1: vm.x, y1: vm.y, vx: v.x, vy: v.y }, config); } }; var helpers$1 = Chart.helpers; var rasterize = utils.rasterize; function boundingRects(model) { var borderWidth = model.borderWidth || 0; var padding = model.padding; var th = model.size.height; var tw = model.size.width; var tx = -tw / 2; var ty = -th / 2; return { frame: { x: tx - padding.left - borderWidth, y: ty - padding.top - borderWidth, w: tw + padding.width + borderWidth * 2, h: th + padding.height + borderWidth * 2 }, 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 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, rasterize(rect.x) + borderWidth / 2, rasterize(rect.y) + borderWidth / 2, rasterize(rect.w) - borderWidth, rasterize(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 textGeometry(rect, align, font) { var h = font.lineHeight; var w = rect.w; var x = rect.x; var y = rect.y + h / 2; if (align === 'center') { x += w / 2; } else if (align === 'end' || align === 'right') { x += w; } return { h: h, w: w, x: x, y: y }; } function drawTextLine(ctx, text, cfg) { var shadow = ctx.shadowBlur; var stroked = cfg.stroked; var x = rasterize(cfg.x); var y = rasterize(cfg.y); var w = rasterize(cfg.w); if (stroked) { ctx.strokeText(text, x, y, w); } if (cfg.filled) { if (shadow && stroked) { // Prevent drawing shadow on both the text stroke and fill, so // if the text is stroked, remove the shadow for the text fill. ctx.shadowBlur = 0; } ctx.fillText(text, x, y, w); if (shadow && stroked) { ctx.shadowBlur = shadow; } } } function drawText(ctx, lines, rect, model) { var align = model.textAlign; var color = model.color; var filled = !!color; var font = model.font; var ilen = lines.length; var strokeColor = model.textStrokeColor; var strokeWidth = model.textStrokeWidth; var stroked = strokeColor && strokeWidth; var i; if (!ilen || (!filled && !stroked)) { return; } // Adjust coordinates based on text alignment and line height rect = textGeometry(rect, align, font); ctx.font = font.string; ctx.textAlign = align; ctx.textBaseline = 'middle'; ctx.shadowBlur = model.textShadowBlur; ctx.shadowColor = model.textShadowColor; if (filled) { ctx.fillStyle = color; } if (stroked) { ctx.lineJoin = 'round'; ctx.lineWidth = strokeWidth; ctx.strokeStyle = strokeColor; } for (i = 0, ilen = lines.length; i < ilen; ++i) { drawTextLine(ctx, lines[i], { stroked: stroked, filled: filled, w: rect.w, x: rect.x, y: rect.y + rect.h * i }); } } var Label = function(config, ctx, el, index) { var me = this; me._config = config; me._index = index; me._model = null; me._rects = null; me._ctx = ctx; me._el = el; }; helpers$1.extend(Label.prototype, { /** * @private */ _modelize: function(display, 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)); var color = resolve([config.color, Chart.defaults.global.defaultFontColor], context, index); return { align: resolve([config.align, 'center'], context, index), anchor: resolve([config.anchor, 'center'], context, index), area: context.chart.chartArea, 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), clamp: resolve([config.clamp, false], context, index), clip: resolve([config.clip, false], context, index), color: color, display: display, 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), textShadowBlur: resolve([config.textShadowBlur, 0], context, index), textShadowColor: resolve([config.textShadowColor, color], context, index), textStrokeColor: resolve([config.textStrokeColor, color], context, index), textStrokeWidth: resolve([config.textStrokeWidth, 0], context, index) }; }, update: function(context) { var me = this; var model = null; var rects = null; var index = me._index; var config = me._config; var value, label, lines; // We first resolve the display option (separately) to avoid computing // other options in case the label is hidden (i.e. display: false). var display = helpers$1.options.resolve([config.display, true], context, index); if (display) { 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); if (lines.length) { model = me._modelize(display, lines, config, context); rects = boundingRects(model); } } me._model = model; me._rects = rects; }, geometry: function() { return this._rects ? this._rects.frame : {}; }, rotation: function() { return this._model ? this._model.rotation : 0; }, visible: function() { return this._model && this._model.opacity; }, model: function() { return this._model; }, draw: function(chart, center) { var me = this; var ctx = chart.ctx; var model = me._model; var rects = me._rects; var area; if (!this.visible()) { return; } ctx.save(); if (model.clip) { area = model.area; ctx.beginPath(); ctx.rect( area.left, area.top, area.right - area.left, area.bottom - area.top); ctx.clip(); } ctx.globalAlpha = utils.bound(0, model.opacity, 1); ctx.translate(rasterize(center.x), rasterize(center.y)); ctx.rotate(model.rotation); drawFrame(ctx, rects.frame, model); drawText(ctx, model.lines, rects.text, model); ctx.restore(); } }); var helpers$2 = Chart.helpers; var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; function rotated(point, center, angle) { var cos = Math.cos(angle); var sin = Math.sin(angle); var cx = center.x; var cy = center.y; return { x: cx + cos * (point.x - cx) - sin * (point.y - cy), y: cy + sin * (point.x - cx) + cos * (point.y - cy) }; } function projected(points, axis) { var min = MAX_INTEGER; var max = MIN_INTEGER; var origin = axis.origin; var i, pt, vx, vy, dp; for (i = 0; i < points.length; ++i) { pt = points[i]; vx = pt.x - origin.x; vy = pt.y - origin.y; dp = axis.vx * vx + axis.vy * vy; min = Math.min(min, dp); max = Math.max(max, dp); } return { min: min, max: max }; } function toAxis(p0, p1) { var vx = p1.x - p0.x; var vy = p1.y - p0.y; var ln = Math.sqrt(vx * vx + vy * vy); return { vx: (p1.x - p0.x) / ln, vy: (p1.y - p0.y) / ln, origin: p0, ln: ln }; } var HitBox = function() { this._rotation = 0; this._rect = { x: 0, y: 0, w: 0, h: 0 }; }; helpers$2.extend(HitBox.prototype, { center: function() { var r = this._rect; return { x: r.x + r.w / 2, y: r.y + r.h / 2 }; }, update: function(center, rect, rotation) { this._rotation = rotation; this._rect = { x: rect.x + center.x, y: rect.y + center.y, w: rect.w, h: rect.h }; }, contains: function(point) { var me = this; var margin = 1; var rect = me._rect; point = rotated(point, me.center(), -me._rotation); return !(point.x < rect.x - margin || point.y < rect.y - margin || point.x > rect.x + rect.w + margin * 2 || point.y > rect.y + rect.h + margin * 2); }, // Separating Axis Theorem // https://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem--gamedev-169 intersects: function(other) { var r0 = this._points(); var r1 = other._points(); var axes = [ toAxis(r0[0], r0[1]), toAxis(r0[0], r0[3]) ]; var i, pr0, pr1; if (this._rotation !== other._rotation) { // Only separate with r1 axis if the rotation is different, // else it's enough to separate r0 and r1 with r0 axis only! axes.push( toAxis(r1[0], r1[1]), toAxis(r1[0], r1[3]) ); } for (i = 0; i < axes.length; ++i) { pr0 = projected(r0, axes[i]); pr1 = projected(r1, axes[i]); if (pr0.max < pr1.min || pr1.max < pr0.min) { return false; } } return true; }, /** * @private */ _points: function() { var me = this; var rect = me._rect; var angle = me._rotation; var center = me.center(); return [ rotated({x: rect.x, y: rect.y}, center, angle), rotated({x: rect.x + rect.w, y: rect.y}, center, angle), rotated({x: rect.x + rect.w, y: rect.y + rect.h}, center, angle), rotated({x: rect.x, y: rect.y + rect.h}, center, angle) ]; } }); function coordinates(view, model, geometry) { var point = model.positioner(view, model); 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}; } var w = geometry.w; var h = geometry.h; // 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 collide(labels, collider) { var i, j, s0, s1; // IMPORTANT Iterate in the reverse order since items at the end of the // list have an higher weight/priority and thus should be less impacted // by the overlapping strategy. for (i = labels.length - 1; i >= 0; --i) { s0 = labels[i].$layout; for (j = i - 1; j >= 0 && s0._visible; --j) { s1 = labels[j].$layout; if (s1._visible && s0._box.intersects(s1._box)) { collider(s0, s1); } } } return labels; } function compute$1(labels) { var i, ilen, label, state, geometry, center; // Initialize labels for overlap detection for (i = 0, ilen = labels.length; i < ilen; ++i) { label = labels[i]; state = label.$layout; if (state._visible) { geometry = label.geometry(); center = coordinates(label._el._model, label.model(), geometry); state._box.update(center, geometry, label.rotation()); } } // Auto hide overlapping labels return collide(labels, function(s0, s1) { var h0 = s0._hidable; var h1 = s1._hidable; if ((h0 && h1) || h1) { s1._visible = false; } else if (h0) { s0._visible = false; } }); } var layout = { prepare: function(datasets) { var labels = []; var i, j, ilen, jlen, label; for (i = 0, ilen = datasets.length; i < ilen; ++i) { for (j = 0, jlen = datasets[i].length; j < jlen; ++j) { label = datasets[i][j]; labels.push(label); label.$layout = { _box: new HitBox(), _hidable: false, _visible: true, _set: i, _idx: j }; } } // TODO New `z` option: labels with a higher z-index are drawn // of top of the ones with a lower index. Lowest z-index labels // are also discarded first when hiding overlapping labels. labels.sort(function(a, b) { var sa = a.$layout; var sb = b.$layout; return sa._idx === sb._idx ? sa._set - sb._set : sb._idx - sa._idx; }); this.update(labels); return labels; }, update: function(labels) { var dirty = false; var i, ilen, label, model, state; for (i = 0, ilen = labels.length; i < ilen; ++i) { label = labels[i]; model = label.model(); state = label.$layout; state._hidable = model && model.display === 'auto'; state._visible = label.visible(); dirty |= state._hidable; } if (dirty) { compute$1(labels); } }, lookup: function(labels, point) { var i, state; // IMPORTANT Iterate in the reverse order since items at the end of // the list have an higher z-index, thus should be picked first. for (i = labels.length - 1; i >= 0; --i) { state = labels[i].$layout; if (state && state._visible && state._box.contains(point)) { return { dataset: state._set, label: labels[i] }; } } return null; }, draw: function(chart, labels) { var i, ilen, label, state, geometry, center; for (i = 0, ilen = labels.length; i < ilen; ++i) { label = labels[i]; state = label.$layout; if (state._visible) { geometry = label.geometry(); center = coordinates(label._el._view, label.model(), geometry); state._box.update(center, geometry, label.rotation()); label.draw(chart, center); } } } }; var helpers$3 = Chart.helpers; var formatter = function(value) { if (helpers$3.isNullOrUndef(value)) { return null; } var label = value; var keys, klen, k; if (helpers$3.isObject(value)) { if (!helpers$3.isNullOrUndef(value.label)) { label = value.label; } else if (!helpers$3.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; }; /** * IMPORTANT: make sure to also update tests and TypeScript definition * files (`/test/specs/defaults.spec.js` and `/types/options.d.ts`) */ var defaults = { align: 'center', anchor: 'center', backgroundColor: null, borderColor: null, borderRadius: 0, borderWidth: 0, clamp: false, clip: false, color: undefined, display: true, font: { family: undefined, lineHeight: 1.2, size: undefined, style: undefined, weight: null }, formatter: formatter, listeners: {}, offset: 4, opacity: 1, padding: { top: 4, right: 4, bottom: 4, left: 4 }, rotation: 0, textAlign: 'start', textStrokeColor: undefined, textStrokeWidth: 0, textShadowBlur: 0, textShadowColor: undefined }; /** * @see https://github.com/chartjs/Chart.js/issues/4176 */ var helpers$4 = Chart.helpers; var EXPANDO_KEY = '$datalabels'; function configure(dataset, options) { var override = dataset.datalabels; var config = {}; if (override === false) { return null; } if (override === true) { override = {}; } return helpers$4.merge(config, [options, override]); } function dispatchEvent(chart, listeners, target) { var callback = listeners && listeners[target.dataset]; if (!callback) { return; } var label = target.label; var context = label.$context; if (helpers$4.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 = layout.lookup(expando._labels, event); } else if (event.type !== 'mouseout') { return; } previous = expando._hovered; expando._hovered = target; dispatchMoveEvents(chart, listeners, previous, target); } function handleClickEvents(chart, event) { var expando = chart[EXPANDO_KEY]; var handlers = expando._listeners.click; var target = handlers && layout.lookup(expando._labels, event); if (target) { dispatchEvent(chart, handlers, target); } } // https://github.com/chartjs/chartjs-plugin-datalabels/issues/108 function invalidate(chart) { if (chart.animating) { return; } // `chart.animating` can be `false` even if there is animation in progress, // so let's iterate all animations to find if there is one for the `chart`. var animations = Chart.animationService.animations; for (var i = 0, ilen = animations.length; i < ilen; ++i) { if (animations[i].chart === chart) { return; } } // No render scheduled: trigger a "lazy" render that can be canceled in case // of hover interactions. The 1ms duration is a workaround to make sure an // animation is created so the controller can stop it before any transition. chart.render({duration: 1, lazy: true}); } Chart.defaults.global.plugins.datalabels = defaults; var plugin = { 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._datasets = []; // per dataset labels: [[Label]] expando._labels = []; // layouted labels: [Label] }, afterDatasetUpdate: function(chart, args, options) { var datasetIndex = args.index; var expando = chart[EXPANDO_KEY]; var labels = expando._datasets[datasetIndex] = []; var visible = chart.isDatasetVisible(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 (visible && 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$4.merge(expando._listeners, config.listeners || {}, { merger: function(key, target, source) { target[key] = target[key] || {}; target[key][args.index] = source[key]; expando._listened = true; } }); }, afterUpdate: function(chart, options) { chart[EXPANDO_KEY]._labels = layout.prepare( chart[EXPANDO_KEY]._datasets, options); }, // 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) { layout.draw(chart, chart[EXPANDO_KEY]._labels); }, 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]; if (label) { label.$context.active = (update[1] === 1); label.update(label.$context); } } } if (expando._dirty || updates.length) { layout.update(expando._labels); invalidate(chart); } delete expando._dirty; } }; // TODO Remove at version 1, we shouldn't automatically register plugins. // https://github.com/chartjs/chartjs-plugin-datalabels/issues/42 Chart.plugins.register(plugin); return plugin; }));