/*! * chartjs-plugin-datalabels v2.2.0 * https://chartjs-plugin-datalabels.netlify.app * (c) 2017-2022 chartjs-plugin-datalabels contributors * Released under the MIT license */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('chart.js/helpers'), require('chart.js')) : typeof define === 'function' && define.amd ? define(['chart.js/helpers', 'chart.js'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ChartDataLabels = factory(global.Chart.helpers, global.Chart)); })(this, (function (helpers, chart_js) { 'use strict'; 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 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 }; }, /** * 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$1(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(el, config) { var angle = (el.startAngle + el.endAngle) / 2; var vx = Math.cos(angle); var vy = Math.sin(angle); var r0 = el.innerRadius; var r1 = el.outerRadius; return compute$1({ x0: el.x + vx * r0, y0: el.y + vy * r0, x1: el.x + vx * r1, y1: el.y + vy * r1, vx: vx, vy: vy }, config); }, point: function(el, config) { var v = orient(el, config.origin); var rx = v.x * el.options.radius; var ry = v.y * el.options.radius; return compute$1({ x0: el.x - rx, y0: el.y - ry, x1: el.x + rx, y1: el.y + ry, vx: v.x, vy: v.y }, config); }, bar: function(el, config) { var v = orient(el, config.origin); var x = el.x; var y = el.y; var sx = 0; var sy = 0; if (el.horizontal) { x = Math.min(el.x, el.base); sx = Math.abs(el.base - el.x); } else { y = Math.min(el.y, el.base); sy = Math.abs(el.base - el.y); } return compute$1({ x0: x, y0: y + sy, x1: x + sx, y1: y, vx: v.x, vy: v.y }, config); }, fallback: function(el, config) { var v = orient(el, config.origin); return compute$1({ x0: el.x, y0: el.y, x1: el.x + (el.width || 0), y1: el.y + (el.height || 0), vx: v.x, vy: v.y }, config); } }; 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, context) { var scale = context.chart.getDatasetMeta(context.datasetIndex).vScale; if (!scale) { return null; } if (scale.xCenter !== undefined && scale.yCenter !== undefined) { return {x: scale.xCenter, y: scale.yCenter}; } var pixel = scale.getBasePixel(); return el.horizontal ? {x: pixel, y: null} : {x: null, y: pixel}; } function getPositioner(el) { if (el instanceof chart_js.ArcElement) { return positioners.arc; } if (el instanceof chart_js.PointElement) { return positioners.point; } if (el instanceof chart_js.BarElement) { return positioners.bar; } return positioners.fallback; } function drawRoundedRect(ctx, x, y, w, h, radius) { var HALF_PI = Math.PI / 2; if (radius) { var r = Math.min(radius, h / 2, w / 2); var left = x + r; var top = y + r; var right = x + w - r; var bottom = y + h - r; ctx.moveTo(x, top); if (left < right && top < bottom) { ctx.arc(left, top, r, -Math.PI, -HALF_PI); ctx.arc(right, top, r, -HALF_PI, 0); ctx.arc(right, bottom, r, 0, HALF_PI); ctx.arc(left, bottom, r, HALF_PI, Math.PI); } else if (left < right) { ctx.moveTo(left, y); ctx.arc(right, top, r, -HALF_PI, HALF_PI); ctx.arc(left, top, r, HALF_PI, Math.PI + HALF_PI); } else if (top < bottom) { ctx.arc(left, top, r, -Math.PI, 0); ctx.arc(left, bottom, r, 0, Math.PI); } else { ctx.arc(left, top, r, -Math.PI, Math.PI); } ctx.closePath(); ctx.moveTo(x, y); } else { ctx.rect(x, y, w, h); } } function drawFrame(ctx, rect, model) { var bgColor = model.backgroundColor; var borderColor = model.borderColor; var borderWidth = model.borderWidth; if (!bgColor && (!borderColor || !borderWidth)) { return; } ctx.beginPath(); drawRoundedRect( 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.merge(Label.prototype, { /** * @private */ _modelize: function(display, lines, config, context) { var me = this; var index = me._index; var font = helpers.toFont(helpers.resolve([config.font, {}], context, index)); var color = helpers.resolve([config.color, chart_js.defaults.color], context, index); return { align: helpers.resolve([config.align, 'center'], context, index), anchor: helpers.resolve([config.anchor, 'center'], context, index), area: context.chart.chartArea, backgroundColor: helpers.resolve([config.backgroundColor, null], context, index), borderColor: helpers.resolve([config.borderColor, null], context, index), borderRadius: helpers.resolve([config.borderRadius, 0], context, index), borderWidth: helpers.resolve([config.borderWidth, 0], context, index), clamp: helpers.resolve([config.clamp, false], context, index), clip: helpers.resolve([config.clip, false], context, index), color: color, display: display, font: font, lines: lines, offset: helpers.resolve([config.offset, 4], context, index), opacity: helpers.resolve([config.opacity, 1], context, index), origin: getScaleOrigin(me._el, context), padding: helpers.toPadding(helpers.resolve([config.padding, 4], context, index)), positioner: getPositioner(me._el), rotation: helpers.resolve([config.rotation, 0], context, index) * (Math.PI / 180), size: utils.textSize(me._ctx, lines, font), textAlign: helpers.resolve([config.textAlign, 'start'], context, index), textShadowBlur: helpers.resolve([config.textShadowBlur, 0], context, index), textShadowColor: helpers.resolve([config.textShadowColor, color], context, index), textStrokeColor: helpers.resolve([config.textStrokeColor, color], context, index), textStrokeWidth: helpers.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.resolve([config.display, true], context, index); if (display) { value = context.dataset.data[index]; label = helpers.valueOrDefault(helpers.callback(config.formatter, [value, context]), value); lines = helpers.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 MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; // eslint-disable-line es/no-number-minsafeinteger var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; // eslint-disable-line es/no-number-maxsafeinteger 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.merge(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(el, model, geometry) { var point = model.positioner(el, 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(labels) { var i, ilen, label, state, geometry, center, proxy; // Initialize labels for overlap detection for (i = 0, ilen = labels.length; i < ilen; ++i) { label = labels[i]; state = label.$layout; if (state._visible) { // Chart.js 3 removed el._model in favor of getProps(), making harder to // abstract reading values in positioners. Also, using string arrays to // read values (i.e. var {a,b,c} = el.getProps(["a","b","c"])) would make // positioners inefficient in the normal case (i.e. not the final values) // and the code a bit ugly, so let's use a Proxy instead. proxy = new Proxy(label._el, {get: (el, p) => el.getProps([p], true)[p]}); geometry = label.geometry(); center = coordinates(proxy, 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: label._index }; } } // 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 ? sb._set - sa._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(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 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, label.model(), geometry); state._box.update(center, geometry, label.rotation()); label.draw(chart, center); } } } }; var formatter = function(value) { if (helpers.isNullOrUndef(value)) { return null; } var label = value; var keys, klen, k; if (helpers.isObject(value)) { if (!helpers.isNullOrUndef(value.label)) { label = value.label; } else if (!helpers.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, labels: undefined, 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 EXPANDO_KEY = '$datalabels'; var DEFAULT_KEY = '$default'; function configure(dataset, options) { var override = dataset.datalabels; var listeners = {}; var configs = []; var labels, keys; if (override === false) { return null; } if (override === true) { override = {}; } options = helpers.merge({}, [options, override]); labels = options.labels || {}; keys = Object.keys(labels); delete options.labels; if (keys.length) { keys.forEach(function(key) { if (labels[key]) { configs.push(helpers.merge({}, [ options, labels[key], {_key: key} ])); } }); } else { // Default label if no "named" label defined. configs.push(options); } // listeners: {: {: }} listeners = configs.reduce(function(target, config) { helpers.each(config.listeners || {}, function(fn, event) { target[event] = target[event] || {}; target[event][config._key || DEFAULT_KEY] = fn; }); delete config.listeners; return target; }, {}); return { labels: configs, listeners: listeners }; } function dispatchEvent(chart, listeners, label, event) { if (!listeners) { return; } var context = label.$context; var groups = label.$groups; var callback; if (!listeners[groups._set]) { return; } callback = listeners[groups._set][groups._key]; if (!callback) { return; } if (helpers.callback(callback, [context, event]) === 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, label, event) { var enter, leave; if (!previous && !label) { return; } if (!previous) { enter = true; } else if (!label) { leave = true; } else if (previous !== label) { leave = enter = true; } if (leave) { dispatchEvent(chart, listeners.leave, previous, event); } if (enter) { dispatchEvent(chart, listeners.enter, label, event); } } function handleMoveEvents(chart, event) { var expando = chart[EXPANDO_KEY]; var listeners = expando._listeners; var previous, label; if (!listeners.enter && !listeners.leave) { return; } if (event.type === 'mousemove') { label = layout.lookup(expando._labels, event); } else if (event.type !== 'mouseout') { return; } previous = expando._hovered; expando._hovered = label; dispatchMoveEvents(chart, listeners, previous, label, event); } function handleClickEvents(chart, event) { var expando = chart[EXPANDO_KEY]; var handlers = expando._listeners.click; var label = handlers && layout.lookup(expando._labels, event); if (label) { dispatchEvent(chart, handlers, label, event); } } var plugin = { id: 'datalabels', defaults: defaults, beforeInit: function(chart) { chart[EXPANDO_KEY] = { _actives: [] }; }, beforeUpdate: function(chart) { var expando = chart[EXPANDO_KEY]; expando._listened = false; expando._listeners = {}; // {: {: {: }}} 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 ctx = chart.ctx; var i, j, ilen, jlen, cfg, key, el, label; ctx.save(); for (i = 0, ilen = elements.length; i < ilen; ++i) { el = elements[i]; el[EXPANDO_KEY] = []; if (visible && el && chart.getDataVisibility(i) && !el.skip) { for (j = 0, jlen = config.labels.length; j < jlen; ++j) { cfg = config.labels[j]; key = cfg._key; label = new Label(cfg, ctx, el, i); label.$groups = { _set: datasetIndex, _key: key || DEFAULT_KEY }; label.$context = { active: false, chart: chart, dataIndex: i, dataset: dataset, datasetIndex: datasetIndex }; label.update(label.$context); el[EXPANDO_KEY].push(label); labels.push(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(event, target, source) { target[event] = target[event] || {}; target[event][args.index] = source[event]; expando._listened = true; } }); }, afterUpdate: function(chart) { chart[EXPANDO_KEY]._labels = layout.prepare(chart[EXPANDO_KEY]._datasets); }, // 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, args) { // 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) { var event = args.event; switch (event.type) { case 'mousemove': case 'mouseout': handleMoveEvents(chart, event); break; case 'click': handleClickEvents(chart, event); break; } } }, afterEvent: function(chart) { var expando = chart[EXPANDO_KEY]; var previous = expando._actives; var actives = expando._actives = chart.getActiveElements(); var updates = utils.arrayDiff(previous, actives); var i, ilen, j, jlen, update, label, labels; for (i = 0, ilen = updates.length; i < ilen; ++i) { update = updates[i]; if (update[1]) { labels = update[0].element[EXPANDO_KEY] || []; for (j = 0, jlen = labels.length; j < jlen; ++j) { label = labels[j]; label.$context.active = (update[1] === 1); label.update(label.$context); } } } if (expando._dirty || updates.length) { layout.update(expando._labels); chart.render(); } delete expando._dirty; } }; return plugin; }));