From cd2df63aaf23657efd9ee5c25a4d2a89f96e8dfd Mon Sep 17 00:00:00 2001 From: "Chart.js" Date: Tue, 13 Apr 2021 06:45:27 +0000 Subject: [PATCH] Release v1.0.0 --- bower.json | 8 + dist/chartjs-plugin-datalabels.js | 1382 +++++++++++++++++++++++++ dist/chartjs-plugin-datalabels.min.js | 7 + 3 files changed, 1397 insertions(+) create mode 100644 bower.json create mode 100644 dist/chartjs-plugin-datalabels.js create mode 100644 dist/chartjs-plugin-datalabels.min.js diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..cca82b5 --- /dev/null +++ b/bower.json @@ -0,0 +1,8 @@ +{ + "name": "chartjs-plugin-datalabels", + "description": "Chart.js plugin to display labels on data elements", + "homepage": "https://chartjs-plugin-datalabels.netlify.app", + "license": "MIT", + "version": "1.0.0", + "main": "dist/chartjs-plugin-datalabels.js" +} diff --git a/dist/chartjs-plugin-datalabels.js b/dist/chartjs-plugin-datalabels.js new file mode 100644 index 0000000..f4a7c21 --- /dev/null +++ b/dist/chartjs-plugin-datalabels.js @@ -0,0 +1,1382 @@ +/*! + * chartjs-plugin-datalabels v1.0.0 + * https://chartjs-plugin-datalabels.netlify.app + * (c) 2017-2021 chartjs-plugin-datalabels 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 = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ChartDataLabels = factory(global.Chart)); +}(this, (function (Chart) { 'use strict'; + +function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } + +var Chart__default = /*#__PURE__*/_interopDefaultLegacy(Chart); + +var helpers$4 = Chart__default['default'].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$4.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$4.isNullOrUndef(font.size) || helpers$4.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__default['default'].defaults.global; + var size = helpers$4.valueOrDefault(value.size, global.defaultFontSize); + var font = { + family: helpers$4.valueOrDefault(value.family, global.defaultFontFamily), + lineHeight: helpers$4.options.toLineHeight(value.lineHeight, size), + size: size, + style: helpers$4.valueOrDefault(value.style, global.defaultFontStyle), + weight: helpers$4.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$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(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$1({ + 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$1({ + 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$1({ + 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$1({ + x0: vm.x, + y0: vm.y, + x1: vm.x, + y1: vm.y, + vx: v.x, + vy: v.y + }, config); + } +}; + +var helpers$3 = Chart__default['default'].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__default['default'].elements.Arc) { + return positioners.arc; + } + if (el instanceof Chart__default['default'].elements.Point) { + return positioners.point; + } + if (el instanceof Chart__default['default'].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$3.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$3.extend(Label.prototype, { + /** + * @private + */ + _modelize: function(display, lines, config, context) { + var me = this; + var index = me._index; + var resolve = helpers$3.options.resolve; + var font = utils.parseFont(resolve([config.font, {}], context, index)); + var color = resolve([config.color, Chart__default['default'].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$3.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$3.options.resolve([config.display, true], context, index); + + if (display) { + value = context.dataset.data[index]; + label = helpers$3.valueOrDefault(helpers$3.callback(config.formatter, [value, context]), value); + lines = helpers$3.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__default['default'].helpers; + +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$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(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 + ? 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._view, label.model(), geometry); + state._box.update(center, geometry, label.rotation()); + label.draw(chart, center); + } + } + } +}; + +var helpers$1 = Chart__default['default'].helpers; + +var formatter = function(value) { + if (helpers$1.isNullOrUndef(value)) { + return null; + } + + var label = value; + var keys, klen, k; + if (helpers$1.isObject(value)) { + if (!helpers$1.isNullOrUndef(value.label)) { + label = value.label; + } else if (!helpers$1.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 helpers = Chart__default['default'].helpers; +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) { + 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]) === 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) { + 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); + } + if (enter) { + dispatchEvent(chart, listeners.enter, label); + } +} + +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); +} + +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); + } +} + +// 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__default['default'].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__default['default'].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 = {}; // {: {: {: }}} + 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 && !el.hidden && !el._model.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, 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; + } + } + }, + + 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, j, jlen, update, label, labels; + + for (i = 0, ilen = updates.length; i < ilen; ++i) { + update = updates[i]; + if (update[1]) { + labels = update[0][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); + invalidate(chart); + } + + delete expando._dirty; + } +}; + +return plugin; + +}))); diff --git a/dist/chartjs-plugin-datalabels.min.js b/dist/chartjs-plugin-datalabels.min.js new file mode 100644 index 0000000..867126c --- /dev/null +++ b/dist/chartjs-plugin-datalabels.min.js @@ -0,0 +1,7 @@ +/*! + * chartjs-plugin-datalabels v1.0.0 + * https://chartjs-plugin-datalabels.netlify.app + * (c) 2017-2021 chartjs-plugin-datalabels contributors + * Released under the MIT license + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("chart.js")):"function"==typeof define&&define.amd?define(["chart.js"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).ChartDataLabels=e(t.Chart)}(this,(function(t){"use strict";function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var r=e(t),n=r.default.helpers,a=function(){if("undefined"!=typeof window){if(window.devicePixelRatio)return window.devicePixelRatio;var t=window.screen;if(t)return(t.deviceXDPI||1)/(t.logicalXDPI||1)}return 1}(),i={toTextLines:function(t){var e,r=[];for(t=[].concat(t);t.length;)"string"==typeof(e=t.pop())?r.unshift.apply(r,e.split("\n")):Array.isArray(e)?t.push.apply(t,e):n.isNullOrUndef(t)||r.unshift(""+e);return r},toFontString:function(t){return!t||n.isNullOrUndef(t.size)||n.isNullOrUndef(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family},textSize:function(t,e,r){var n,a=[].concat(e),i=a.length,o=t.font,l=0;for(t.font=r.string,n=0;nr.right&&(n|=2),er.bottom&&(n|=4),n}function s(t,e){var r,n,a=e.anchor,i=t;return e.clamp&&(i=function(t,e){for(var r,n,a,i=t.x0,o=t.y0,s=t.x1,u=t.y1,d=l(i,o,e),f=l(s,u,e);d|f&&!(d&f);)8&(r=d||f)?(n=i+(s-i)*(e.top-o)/(u-o),a=e.top):4&r?(n=i+(s-i)*(e.bottom-o)/(u-o),a=e.bottom):2&r?(a=o+(u-o)*(e.right-i)/(s-i),n=e.right):1&r&&(a=o+(u-o)*(e.left-i)/(s-i),n=e.left),r===d?d=l(i=n,o=a,e):f=l(s=n,u=a,e);return{x0:i,x1:s,y0:o,y1:u}}(i,e.area)),"start"===a?(r=i.x0,n=i.y0):"end"===a?(r=i.x1,n=i.y1):(r=(i.x0+i.x1)/2,n=(i.y0+i.y1)/2),function(t,e,r,n,a){switch(a){case"center":r=n=0;break;case"bottom":r=0,n=1;break;case"right":r=1,n=0;break;case"left":r=-1,n=0;break;case"top":r=0,n=-1;break;case"start":r=-r,n=-n;break;case"end":break;default:a*=Math.PI/180,r=Math.cos(a),n=Math.sin(a)}return{x:t,y:e,vx:r,vy:n}}(r,n,t.vx,t.vy,e.align)}var u=function(t,e){var r=(t.startAngle+t.endAngle)/2,n=Math.cos(r),a=Math.sin(r),i=t.innerRadius,o=t.outerRadius;return s({x0:t.x+n*i,y0:t.y+a*i,x1:t.x+n*o,y1:t.y+a*o,vx:n,vy:a},e)},d=function(t,e){var r=o(t,e.origin),n=r.x*t.radius,a=r.y*t.radius;return s({x0:t.x-n,y0:t.y-a,x1:t.x+n,y1:t.y+a,vx:r.x,vy:r.y},e)},f=function(t,e){var r=o(t,e.origin),n=t.x,a=t.y,i=0,l=0;return t.horizontal?(n=Math.min(t.x,t.base),i=Math.abs(t.base-t.x)):(a=Math.min(t.y,t.base),l=Math.abs(t.base-t.y)),s({x0:n,y0:a+l,x1:n+i,y1:a,vx:r.x,vy:r.y},e)},c=function(t,e){var r=o(t,e.origin);return s({x0:t.x,y0:t.y,x1:t.x,y1:t.y,vx:r.x,vy:r.y},e)},h=r.default.helpers,x=i.rasterize;function y(t){var e=t._model.horizontal,r=t._scale||e&&t._xScale||t._yScale;if(!r)return null;if(void 0!==r.xCenter&&void 0!==r.yCenter)return{x:r.xCenter,y:r.yCenter};var n=r.getBasePixel();return e?{x:n,y:null}:{x:null,y:n}}function v(t,e,r){var n=t.shadowBlur,a=r.stroked,i=x(r.x),o=x(r.y),l=x(r.w);a&&t.strokeText(e,i,o,l),r.filled&&(n&&a&&(t.shadowBlur=0),t.fillText(e,i,o,l),n&&a&&(t.shadowBlur=n))}var b=function(t,e,r,n){var a=this;a._config=t,a._index=n,a._model=null,a._rects=null,a._ctx=e,a._el=r};h.extend(b.prototype,{_modelize:function(t,e,n,a){var o,l=this,s=l._index,x=h.options.resolve,v=i.parseFont(x([n.font,{}],a,s)),b=x([n.color,r.default.defaults.global.defaultFontColor],a,s);return{align:x([n.align,"center"],a,s),anchor:x([n.anchor,"center"],a,s),area:a.chart.chartArea,backgroundColor:x([n.backgroundColor,null],a,s),borderColor:x([n.borderColor,null],a,s),borderRadius:x([n.borderRadius,0],a,s),borderWidth:x([n.borderWidth,0],a,s),clamp:x([n.clamp,!1],a,s),clip:x([n.clip,!1],a,s),color:b,display:t,font:v,lines:e,offset:x([n.offset,0],a,s),opacity:x([n.opacity,1],a,s),origin:y(l._el),padding:h.options.toPadding(x([n.padding,0],a,s)),positioner:(o=l._el,o instanceof r.default.elements.Arc?u:o instanceof r.default.elements.Point?d:o instanceof r.default.elements.Rectangle?f:c),rotation:x([n.rotation,0],a,s)*(Math.PI/180),size:i.textSize(l._ctx,e,v),textAlign:x([n.textAlign,"start"],a,s),textShadowBlur:x([n.textShadowBlur,0],a,s),textShadowColor:x([n.textShadowColor,b],a,s),textStrokeColor:x([n.textStrokeColor,b],a,s),textStrokeWidth:x([n.textStrokeWidth,0],a,s)}},update:function(t){var e,r,n,a=this,o=null,l=null,s=a._index,u=a._config,d=h.options.resolve([u.display,!0],t,s);d&&(e=t.dataset.data[s],r=h.valueOrDefault(h.callback(u.formatter,[e,t]),e),(n=h.isNullOrUndef(r)?[]:i.toTextLines(r)).length&&(l=function(t){var e=t.borderWidth||0,r=t.padding,n=t.size.height,a=t.size.width,i=-a/2,o=-n/2;return{frame:{x:i-r.left-e,y:o-r.top-e,w:a+r.width+2*e,h:n+r.height+2*e},text:{x:i,y:o,w:a,h:n}}}(o=a._modelize(d,n,u,t)))),a._model=o,a._rects=l},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(t,e){var r,n=t.ctx,a=this._model,o=this._rects;this.visible()&&(n.save(),a.clip&&(r=a.area,n.beginPath(),n.rect(r.left,r.top,r.right-r.left,r.bottom-r.top),n.clip()),n.globalAlpha=i.bound(0,a.opacity,1),n.translate(x(e.x),x(e.y)),n.rotate(a.rotation),function(t,e,r){var n=r.backgroundColor,a=r.borderColor,i=r.borderWidth;(n||a&&i)&&(t.beginPath(),h.canvas.roundedRect(t,x(e.x)+i/2,x(e.y)+i/2,x(e.w)-i,x(e.h)-i,r.borderRadius),t.closePath(),n&&(t.fillStyle=n,t.fill()),a&&i&&(t.strokeStyle=a,t.lineWidth=i,t.lineJoin="miter",t.stroke()))}(n,o.frame,a),function(t,e,r,n){var a,i=n.textAlign,o=n.color,l=!!o,s=n.font,u=e.length,d=n.textStrokeColor,f=n.textStrokeWidth,c=d&&f;if(u&&(l||c))for(r=function(t,e,r){var n=r.lineHeight,a=t.w,i=t.x;return"center"===e?i+=a/2:"end"!==e&&"right"!==e||(i+=a),{h:n,w:a,x:i,y:t.y+n/2}}(r,i,s),t.font=s.string,t.textAlign=i,t.textBaseline="middle",t.shadowBlur=n.textShadowBlur,t.shadowColor=n.textShadowColor,l&&(t.fillStyle=o),c&&(t.lineJoin="round",t.lineWidth=f,t.strokeStyle=d),a=0,u=e.length;ar.x+r.w+2||t.y>r.y+r.h+2)},intersects:function(t){var e,r,n,a=this._points(),i=t._points(),o=[k(a[0],a[1]),k(a[0],a[3])];for(this._rotation!==t._rotation&&o.push(k(i[0],i[1]),k(i[0],i[3])),e=0;e=0;--r)for(a=t[r].$layout,n=r-1;n>=0&&a._visible;--n)(i=t[n].$layout)._visible&&a._box.intersects(i._box)&&e(a,i)})(t,(function(t,e){var r=t._hidable,n=e._hidable;r&&n||n?e._visible=!1:r&&(t._visible=!1)}))}(t)},lookup:function(t,e){var r,n;for(r=t.length-1;r>=0;--r)if((n=t[r].$layout)&&n._visible&&n._box.contains(e))return t[r];return null},draw:function(t,e){var r,n,a,i,o,l;for(r=0,n=e.length;r