chartjs-plugin-datalabels.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  1. /*!
  2. * @license
  3. * chartjs-plugin-datalabels
  4. * http://chartjs.org/
  5. * Version: 0.3.0
  6. *
  7. * Copyright 2018 Chart.js Contributors
  8. * Released under the MIT license
  9. * https://github.com/chartjs/chartjs-plugin-datalabels/blob/master/LICENSE.md
  10. */
  11. (function (global, factory) {
  12. typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('chart.js')) :
  13. typeof define === 'function' && define.amd ? define(['chart.js'], factory) :
  14. (factory(global.Chart));
  15. }(this, (function (Chart) { 'use strict';
  16. Chart = Chart && Chart.hasOwnProperty('default') ? Chart['default'] : Chart;
  17. 'use strict';
  18. var helpers$2 = Chart.helpers;
  19. var HitBox = function() {
  20. this._rect = null;
  21. this._rotation = 0;
  22. };
  23. helpers$2.extend(HitBox.prototype, {
  24. update: function(center, rect, rotation) {
  25. var margin = 1;
  26. var cx = center.x;
  27. var cy = center.y;
  28. var x = cx + rect.x;
  29. var y = cy + rect.y;
  30. this._rotation = rotation;
  31. this._rect = {
  32. x0: x - margin,
  33. y0: y - margin,
  34. x1: x + rect.w + margin * 2,
  35. y1: y + rect.h + margin * 2,
  36. cx: cx,
  37. cy: cy,
  38. };
  39. },
  40. contains: function(x, y) {
  41. var me = this;
  42. var rect = me._rect;
  43. var cx, cy, r, rx, ry;
  44. if (!rect) {
  45. return false;
  46. }
  47. cx = rect.cx;
  48. cy = rect.cy;
  49. r = me._rotation;
  50. rx = cx + (x - cx) * Math.cos(r) + (y - cy) * Math.sin(r);
  51. ry = cy - (x - cx) * Math.sin(r) + (y - cy) * Math.cos(r);
  52. return !(rx < rect.x0
  53. || ry < rect.y0
  54. || rx > rect.x1
  55. || ry > rect.y1);
  56. }
  57. });
  58. 'use strict';
  59. var helpers$3 = Chart.helpers;
  60. var utils = {
  61. // @todo move this in Chart.helpers.toTextLines
  62. toTextLines: function(inputs) {
  63. var lines = [];
  64. var input;
  65. inputs = [].concat(inputs);
  66. while (inputs.length) {
  67. input = inputs.pop();
  68. if (typeof input === 'string') {
  69. lines.unshift.apply(lines, input.split('\n'));
  70. } else if (Array.isArray(input)) {
  71. inputs.push.apply(inputs, input);
  72. } else if (!helpers$3.isNullOrUndef(inputs)) {
  73. lines.unshift('' + input);
  74. }
  75. }
  76. return lines;
  77. },
  78. // @todo move this method in Chart.helpers.canvas.toFont (deprecates helpers.fontString)
  79. // @see https://developer.mozilla.org/en-US/docs/Web/CSS/font
  80. toFontString: function(font) {
  81. if (!font || helpers$3.isNullOrUndef(font.size) || helpers$3.isNullOrUndef(font.family)) {
  82. return null;
  83. }
  84. return (font.style ? font.style + ' ' : '')
  85. + (font.weight ? font.weight + ' ' : '')
  86. + font.size + 'px '
  87. + font.family;
  88. },
  89. // @todo move this in Chart.helpers.canvas.textSize
  90. // @todo cache calls of measureText if font doesn't change?!
  91. textSize: function(ctx, lines, font) {
  92. var items = [].concat(lines);
  93. var ilen = items.length;
  94. var prev = ctx.font;
  95. var width = 0;
  96. var i;
  97. ctx.font = font.string;
  98. for (i = 0; i < ilen; ++i) {
  99. width = Math.max(ctx.measureText(items[i]).width, width);
  100. }
  101. ctx.font = prev;
  102. return {
  103. height: ilen * font.lineHeight,
  104. width: width
  105. };
  106. },
  107. // @todo move this method in Chart.helpers.options.toFont
  108. parseFont: function(value) {
  109. var global = Chart.defaults.global;
  110. var size = helpers$3.valueOrDefault(value.size, global.defaultFontSize);
  111. var font = {
  112. family: helpers$3.valueOrDefault(value.family, global.defaultFontFamily),
  113. lineHeight: helpers$3.options.toLineHeight(value.lineHeight, size),
  114. size: size,
  115. style: helpers$3.valueOrDefault(value.style, global.defaultFontStyle),
  116. weight: helpers$3.valueOrDefault(value.weight, null),
  117. string: ''
  118. };
  119. font.string = utils.toFontString(font);
  120. return font;
  121. },
  122. /**
  123. * Returns value bounded by min and max. This is equivalent to max(min, min(value, max)).
  124. * @todo move this method in Chart.helpers.bound
  125. * https://doc.qt.io/qt-5/qtglobal.html#qBound
  126. */
  127. bound: function(min, value, max) {
  128. return Math.max(min, Math.min(value, max));
  129. },
  130. /**
  131. * Returns an array of pair [value, state] where state is:
  132. * * -1: value is only in a0 (removed)
  133. * * 1: value is only in a1 (added)
  134. */
  135. arrayDiff: function(a0, a1) {
  136. var prev = a0.slice();
  137. var updates = [];
  138. var i, j, ilen, v;
  139. for (i = 0, ilen = a1.length; i < ilen; ++i) {
  140. v = a1[i];
  141. j = prev.indexOf(v);
  142. if (j === -1) {
  143. updates.push([v, 1]);
  144. } else {
  145. prev.splice(j, 1);
  146. }
  147. }
  148. for (i = 0, ilen = prev.length; i < ilen; ++i) {
  149. updates.push([prev[i], -1]);
  150. }
  151. return updates;
  152. }
  153. };
  154. 'use strict';
  155. function orient(point, origin) {
  156. var x0 = origin.x;
  157. var y0 = origin.y;
  158. if (x0 === null) {
  159. return {x: 0, y: -1};
  160. }
  161. if (y0 === null) {
  162. return {x: 1, y: 0};
  163. }
  164. var dx = point.x - x0;
  165. var dy = point.y - y0;
  166. var ln = Math.sqrt(dx * dx + dy * dy);
  167. return {
  168. x: ln ? dx / ln : 0,
  169. y: ln ? dy / ln : -1
  170. };
  171. }
  172. function aligned(x, y, vx, vy, align) {
  173. switch (align) {
  174. case 'center':
  175. vx = vy = 0;
  176. break;
  177. case 'bottom':
  178. vx = 0;
  179. vy = 1;
  180. break;
  181. case 'right':
  182. vx = 1;
  183. vy = 0;
  184. break;
  185. case 'left':
  186. vx = -1;
  187. vy = 0;
  188. break;
  189. case 'top':
  190. vx = 0;
  191. vy = -1;
  192. break;
  193. case 'start':
  194. vx = -vx;
  195. vy = -vy;
  196. break;
  197. case 'end':
  198. // keep the natural orientation
  199. break;
  200. default:
  201. // clockwise rotation (in degree)
  202. align *= (Math.PI / 180);
  203. vx = Math.cos(align);
  204. vy = Math.sin(align);
  205. break;
  206. }
  207. return {
  208. x: x,
  209. y: y,
  210. vx: vx,
  211. vy: vy
  212. };
  213. }
  214. var positioners = {
  215. arc: function(vm, anchor, align) {
  216. var angle = (vm.startAngle + vm.endAngle) / 2;
  217. var vx = Math.cos(angle);
  218. var vy = Math.sin(angle);
  219. var r0 = vm.innerRadius;
  220. var r1 = vm.outerRadius;
  221. var d;
  222. if (anchor === 'start') {
  223. d = r0;
  224. } else if (anchor === 'end') {
  225. d = r1;
  226. } else {
  227. d = (r0 + r1) / 2;
  228. }
  229. return aligned(
  230. vm.x + vx * d,
  231. vm.y + vy * d,
  232. vx,
  233. vy,
  234. align);
  235. },
  236. point: function(vm, anchor, align, origin) {
  237. var v = orient(vm, origin);
  238. var r = vm.radius;
  239. var d = 0;
  240. if (anchor === 'start') {
  241. d = -r;
  242. } else if (anchor === 'end') {
  243. d = r;
  244. }
  245. return aligned(
  246. vm.x + v.x * d,
  247. vm.y + v.y * d,
  248. v.x,
  249. v.y,
  250. align);
  251. },
  252. rect: function(vm, anchor, align, origin) {
  253. var horizontal = vm.horizontal;
  254. var size = Math.abs(vm.base - (horizontal ? vm.x : vm.y));
  255. var x = horizontal ? Math.min(vm.x, vm.base) : vm.x;
  256. var y = horizontal ? vm.y : Math.min(vm.y, vm.base);
  257. var v = orient(vm, origin);
  258. if (anchor === 'center') {
  259. if (horizontal) {
  260. x += size / 2;
  261. } else {
  262. y += size / 2;
  263. }
  264. } else if (anchor === 'start' && !horizontal) {
  265. y += size;
  266. } else if (anchor === 'end' && horizontal) {
  267. x += size;
  268. }
  269. return aligned(
  270. x,
  271. y,
  272. v.x,
  273. v.y,
  274. align);
  275. },
  276. fallback: function(vm, anchor, align, origin) {
  277. var v = orient(vm, origin);
  278. return aligned(
  279. vm.x,
  280. vm.y,
  281. v.x,
  282. v.y,
  283. align);
  284. }
  285. };
  286. 'use strict';
  287. var helpers$1 = Chart.helpers;
  288. function boundingRects(size, padding) {
  289. var th = size.height;
  290. var tw = size.width;
  291. var tx = -tw / 2;
  292. var ty = -th / 2;
  293. return {
  294. frame: {
  295. x: tx - padding.left,
  296. y: ty - padding.top,
  297. w: tw + padding.width,
  298. h: th + padding.height,
  299. },
  300. text: {
  301. x: tx,
  302. y: ty,
  303. w: tw,
  304. h: th
  305. }
  306. };
  307. }
  308. function getScaleOrigin(el) {
  309. var horizontal = el._model.horizontal;
  310. var scale = el._scale || (horizontal && el._xScale) || el._yScale;
  311. if (!scale) {
  312. return null;
  313. }
  314. if (scale.xCenter !== undefined && scale.yCenter !== undefined) {
  315. return {x: scale.xCenter, y: scale.yCenter};
  316. }
  317. var pixel = scale.getBasePixel();
  318. return horizontal ?
  319. {x: pixel, y: null} :
  320. {x: null, y: pixel};
  321. }
  322. function getPositioner(el) {
  323. if (el instanceof Chart.elements.Arc) {
  324. return positioners.arc;
  325. }
  326. if (el instanceof Chart.elements.Point) {
  327. return positioners.point;
  328. }
  329. if (el instanceof Chart.elements.Rectangle) {
  330. return positioners.rect;
  331. }
  332. return positioners.fallback;
  333. }
  334. function coordinates(el, model, rect) {
  335. var point = model.positioner(el._view, model.anchor, model.align, model.origin);
  336. var vx = point.vx;
  337. var vy = point.vy;
  338. if (!vx && !vy) {
  339. // if aligned center, we don't want to offset the center point
  340. return {x: point.x, y: point.y};
  341. }
  342. // include borders to the bounding rect
  343. var borderWidth = model.borderWidth || 0;
  344. var w = (rect.w + borderWidth * 2);
  345. var h = (rect.h + borderWidth * 2);
  346. // take in account the label rotation
  347. var rotation = model.rotation;
  348. var dx = Math.abs(w / 2 * Math.cos(rotation)) + Math.abs(h / 2 * Math.sin(rotation));
  349. var dy = Math.abs(w / 2 * Math.sin(rotation)) + Math.abs(h / 2 * Math.cos(rotation));
  350. // scale the unit vector (vx, vy) to get at least dx or dy equal to w or h respectively
  351. // (else we would calculate the distance to the ellipse inscribed in the bounding rect)
  352. var vs = 1 / Math.max(Math.abs(vx), Math.abs(vy));
  353. dx *= vx * vs;
  354. dy *= vy * vs;
  355. // finally, include the explicit offset
  356. dx += model.offset * vx;
  357. dy += model.offset * vy;
  358. return {
  359. x: point.x + dx,
  360. y: point.y + dy
  361. };
  362. }
  363. function drawFrame(ctx, rect, model) {
  364. var bgColor = model.backgroundColor;
  365. var borderColor = model.borderColor;
  366. var borderWidth = model.borderWidth;
  367. if (!bgColor && (!borderColor || !borderWidth)) {
  368. return;
  369. }
  370. ctx.beginPath();
  371. helpers$1.canvas.roundedRect(
  372. ctx,
  373. Math.round(rect.x) - borderWidth / 2,
  374. Math.round(rect.y) - borderWidth / 2,
  375. Math.round(rect.w) + borderWidth,
  376. Math.round(rect.h) + borderWidth,
  377. model.borderRadius);
  378. ctx.closePath();
  379. if (bgColor) {
  380. ctx.fillStyle = bgColor;
  381. ctx.fill();
  382. }
  383. if (borderColor && borderWidth) {
  384. ctx.strokeStyle = borderColor;
  385. ctx.lineWidth = borderWidth;
  386. ctx.lineJoin = 'miter';
  387. ctx.stroke();
  388. }
  389. }
  390. function drawText(ctx, lines, rect, model) {
  391. var align = model.textAlign;
  392. var font = model.font;
  393. var lh = font.lineHeight;
  394. var color = model.color;
  395. var ilen = lines.length;
  396. var x, y, i;
  397. if (!ilen || !color) {
  398. return;
  399. }
  400. x = rect.x;
  401. y = rect.y + lh / 2;
  402. if (align === 'center') {
  403. x += rect.w / 2;
  404. } else if (align === 'end' || align === 'right') {
  405. x += rect.w;
  406. }
  407. ctx.font = model.font.string;
  408. ctx.fillStyle = color;
  409. ctx.textAlign = align;
  410. ctx.textBaseline = 'middle';
  411. for (i = 0; i < ilen; ++i) {
  412. ctx.fillText(
  413. lines[i],
  414. Math.round(x),
  415. Math.round(y),
  416. Math.round(rect.w));
  417. y += lh;
  418. }
  419. }
  420. var Label = function(config, ctx, el, index) {
  421. var me = this;
  422. me._hitbox = new HitBox();
  423. me._config = config;
  424. me._index = index;
  425. me._model = null;
  426. me._ctx = ctx;
  427. me._el = el;
  428. };
  429. helpers$1.extend(Label.prototype, {
  430. /**
  431. * @private
  432. */
  433. _modelize: function(lines, config, context) {
  434. var me = this;
  435. var index = me._index;
  436. var resolve = helpers$1.options.resolve;
  437. var font = utils.parseFont(resolve([config.font, {}], context, index));
  438. return {
  439. align: resolve([config.align, 'center'], context, index),
  440. anchor: resolve([config.anchor, 'center'], context, index),
  441. backgroundColor: resolve([config.backgroundColor, null], context, index),
  442. borderColor: resolve([config.borderColor, null], context, index),
  443. borderRadius: resolve([config.borderRadius, 0], context, index),
  444. borderWidth: resolve([config.borderWidth, 0], context, index),
  445. color: resolve([config.color, Chart.defaults.global.defaultFontColor], context, index),
  446. font: font,
  447. lines: lines,
  448. offset: resolve([config.offset, 0], context, index),
  449. opacity: resolve([config.opacity, 1], context, index),
  450. origin: getScaleOrigin(me._el),
  451. padding: helpers$1.options.toPadding(resolve([config.padding, 0], context, index)),
  452. positioner: getPositioner(me._el),
  453. rotation: resolve([config.rotation, 0], context, index) * (Math.PI / 180),
  454. size: utils.textSize(me._ctx, lines, font),
  455. textAlign: resolve([config.textAlign, 'start'], context, index)
  456. };
  457. },
  458. update: function(context) {
  459. var me = this;
  460. var model = null;
  461. var index = me._index;
  462. var config = me._config;
  463. var value, label, lines;
  464. if (helpers$1.options.resolve([config.display, true], context, index)) {
  465. value = context.dataset.data[index];
  466. label = helpers$1.valueOrDefault(helpers$1.callback(config.formatter, [value, context]), value);
  467. lines = helpers$1.isNullOrUndef(label) ? [] : utils.toTextLines(label);
  468. model = lines.length ? me._modelize(lines, config, context) : null;
  469. }
  470. me._model = model;
  471. },
  472. draw: function(ctx) {
  473. var me = this;
  474. var model = me._model;
  475. var rects, center;
  476. if (!model || !model.opacity) {
  477. return;
  478. }
  479. rects = boundingRects(model.size, model.padding);
  480. center = coordinates(me._el, model, rects.frame);
  481. me._hitbox.update(center, rects.frame, model.rotation);
  482. ctx.save();
  483. ctx.globalAlpha = utils.bound(0, model.opacity, 1);
  484. ctx.translate(Math.round(center.x), Math.round(center.y));
  485. ctx.rotate(model.rotation);
  486. drawFrame(ctx, rects.frame, model);
  487. drawText(ctx, model.lines, rects.text, model);
  488. ctx.restore();
  489. },
  490. contains: function(x, y) {
  491. return this._hitbox.contains(x, y);
  492. }
  493. });
  494. /**
  495. * @module Options
  496. */
  497. 'use strict';
  498. var helpers$4 = Chart.helpers;
  499. var defaults = {
  500. /**
  501. * The label box alignment relative to `anchor` that can be expressed either by a number
  502. * representing the clockwise angle (in degree) or by one of the following string presets:
  503. * - 'start': before the anchor point, following the same direction
  504. * - 'end': after the anchor point, following the same direction
  505. * - 'center': centered on the anchor point
  506. * - 'right': 0°
  507. * - 'bottom': 90°
  508. * - 'left': 180°
  509. * - 'top': 270°
  510. * @member {String|Number|Array|Function}
  511. * @default 'center'
  512. */
  513. align: 'center',
  514. /**
  515. * The label box alignment relative to the element ('start'|'center'|'end')
  516. * @member {String|Array|Function}
  517. * @default 'center'
  518. */
  519. anchor: 'center',
  520. /**
  521. * The color used to draw the background of the surrounding frame.
  522. * @member {String|Array|Function|null}
  523. * @default null (no background)
  524. */
  525. backgroundColor: null,
  526. /**
  527. * The color used to draw the border of the surrounding frame.
  528. * @member {String|Array|Function|null}
  529. * @default null (no border)
  530. */
  531. borderColor: null,
  532. /**
  533. * The border radius used to add rounded corners to the surrounding frame.
  534. * @member {Number|Array|Function}
  535. * @default 0 (not rounded)
  536. */
  537. borderRadius: 0,
  538. /**
  539. * The border width of the surrounding frame.
  540. * @member {Number|Array|Function}
  541. * @default 0 (no border)
  542. */
  543. borderWidth: 0,
  544. /**
  545. * The color used to draw the label text.
  546. * @member {String|Array|Function}
  547. * @default undefined (use Chart.defaults.global.defaultFontColor)
  548. */
  549. color: undefined,
  550. /**
  551. * Whether to display labels global (boolean) or per data (function)
  552. * @member {Boolean|Array|Function}
  553. * @default true
  554. */
  555. display: true,
  556. /**
  557. * The font options used to draw the label text.
  558. * @member {Object|Array|Function}
  559. * @prop {String} font.family - defaults to Chart.defaults.global.defaultFontFamily
  560. * @prop {Number} font.lineHeight - defaults to 1.2
  561. * @prop {Number} font.size - defaults to Chart.defaults.global.defaultFontSize
  562. * @prop {String} font.style - defaults to Chart.defaults.global.defaultFontStyle
  563. * @prop {Number} font.weight - defaults to 'normal'
  564. * @default Chart.defaults.global.defaultFont.*
  565. */
  566. font: {
  567. family: undefined,
  568. lineHeight: 1.2,
  569. size: undefined,
  570. style: undefined,
  571. weight: null
  572. },
  573. /**
  574. * The distance (in pixels) to pull the label away from the anchor point, the direction
  575. * being determined by the `align` value (only applicable if `align` is `start` or `end`).
  576. * @member {Number|Array|Function}
  577. * @default 4
  578. */
  579. offset: 4,
  580. /**
  581. * The label global opacity, including the text, background, borders, etc., specified as
  582. * a number between 0.0 (fully transparent) and 1.0 (fully opaque).
  583. * https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalAlpha
  584. * @member {Number|Array|Function}
  585. * @default 1
  586. */
  587. opacity: 1,
  588. /**
  589. * The padding (in pixels) to apply between the text and the surrounding frame.
  590. * @member {Number|Object|Array|Function}
  591. * @prop {Number} padding.top - Space above the text.
  592. * @prop {Number} padding.right - Space on the right of the text.
  593. * @prop {Number} padding.bottom - Space below the text.
  594. * @prop {Number} padding.left - Space on the left of the text.
  595. * @default 4 (all values)
  596. */
  597. padding: {
  598. top: 4,
  599. right: 4,
  600. bottom: 4,
  601. left: 4
  602. },
  603. /**
  604. * Clockwise rotation of the label relative to its center.
  605. * @member {Number|Array|Function}
  606. * @default 0
  607. */
  608. rotation: 0,
  609. /**
  610. * Text alignment for multi-lines labels ('left'|'right'|'start'|'center'|'end').
  611. * @member {String|Array|Function}
  612. * @default 'start'
  613. */
  614. textAlign: 'start',
  615. /**
  616. * Allows to customize the label text by transforming input data.
  617. * @member {Function|null}
  618. * @prop {*} value - The data value
  619. * @prop {Object} context - The function unique argument:
  620. * @prop {Chart} context.chart - The current chart
  621. * @prop {Number} context.dataIndex - Index of the current data
  622. * @prop {Object} context.dataset - The current dataset
  623. * @prop {Number} context.datasetIndex - Index of the current dataset
  624. * @default data[index]
  625. */
  626. formatter: function(value) {
  627. if (helpers$4.isNullOrUndef(value)) {
  628. return null;
  629. }
  630. var label = value;
  631. var keys, klen, k;
  632. if (helpers$4.isObject(value)) {
  633. if (!helpers$4.isNullOrUndef(value.label)) {
  634. label = value.label;
  635. } else if (!helpers$4.isNullOrUndef(value.r)) {
  636. label = value.r;
  637. } else {
  638. label = '';
  639. keys = Object.keys(value);
  640. for (k = 0, klen = keys.length; k < klen; ++k) {
  641. label += (k !== 0 ? ', ' : '') + keys[k] + ': ' + value[keys[k]];
  642. }
  643. }
  644. }
  645. return '' + label;
  646. },
  647. /**
  648. * Event listeners, where the property is the type of the event to listen and the value
  649. * a callback with a unique `context` argument containing the same information as for
  650. * scriptable options. If a callback explicitly returns `true`, the label is updated
  651. * with the current context and the chart re-rendered. This allows to implement visual
  652. * interactions with labels such as highlight, selection, etc.
  653. *
  654. * Event currently supported are:
  655. * - 'click': a mouse click is detected within a label
  656. * - 'enter': the mouse enters a label
  657. * -' leave': the mouse leaves a label
  658. *
  659. * @member {Object}
  660. * @default {}
  661. */
  662. listeners: {}
  663. };
  664. /**
  665. * @see https://github.com/chartjs/Chart.js/issues/4176
  666. */
  667. 'use strict';
  668. var helpers = Chart.helpers;
  669. var EXPANDO_KEY = '$datalabels';
  670. Chart.defaults.global.plugins.datalabels = defaults;
  671. function configure(dataset, options) {
  672. var override = dataset.datalabels;
  673. var config = {};
  674. if (override === false) {
  675. return null;
  676. }
  677. if (override === true) {
  678. override = {};
  679. }
  680. return helpers.merge(config, [options, override]);
  681. }
  682. function drawLabels(chart, datasetIndex) {
  683. var meta = chart.getDatasetMeta(datasetIndex);
  684. var elements = meta.data || [];
  685. var ilen = elements.length;
  686. var i, el, label;
  687. for (i = 0; i < ilen; ++i) {
  688. el = elements[i];
  689. label = el[EXPANDO_KEY];
  690. if (label) {
  691. label.draw(chart.ctx);
  692. }
  693. }
  694. }
  695. function labelAtXY(chart, x, y) {
  696. var items = chart[EXPANDO_KEY].labels;
  697. var i, j, labels, label;
  698. // Until we support z-index, let's hit test in the drawing reverse order
  699. for (i = items.length - 1; i >= 0; --i) {
  700. labels = items[i] || [];
  701. for (j = labels.length - 1; j >= 0; --j) {
  702. label = labels[j];
  703. if (label.contains(x, y)) {
  704. return {dataset: i, label: label};
  705. }
  706. }
  707. }
  708. return null;
  709. }
  710. function dispatchEvent(chart, listeners, target) {
  711. var callback = listeners && listeners[target.dataset];
  712. if (!callback) {
  713. return;
  714. }
  715. var label = target.label;
  716. var context = label.$context;
  717. if (helpers.callback(callback, [context]) === true) {
  718. // Users are allowed to tweak the given context by injecting values that can be
  719. // used in scriptable options to display labels differently based on the current
  720. // event (e.g. highlight an hovered label). That's why we update the label with
  721. // the output context and schedule a new chart render by setting it dirty.
  722. chart[EXPANDO_KEY].dirty = true;
  723. label.update(context);
  724. }
  725. }
  726. function dispatchMoveEvents(chart, listeners, previous, target) {
  727. var enter, leave;
  728. if (!previous && !target) {
  729. return;
  730. }
  731. if (!previous) {
  732. enter = true;
  733. } else if (!target) {
  734. leave = true;
  735. } else if (previous.label !== target.label) {
  736. leave = enter = true;
  737. }
  738. if (leave) {
  739. dispatchEvent(chart, listeners.leave, previous);
  740. }
  741. if (enter) {
  742. dispatchEvent(chart, listeners.enter, target);
  743. }
  744. }
  745. function handleMoveEvents(chart, event) {
  746. var expando = chart[EXPANDO_KEY];
  747. var listeners = expando.listeners;
  748. var previous, target;
  749. if (!listeners.enter && !listeners.leave) {
  750. return;
  751. }
  752. if (event.type === 'mousemove') {
  753. target = labelAtXY(chart, event.x, event.y);
  754. } else if (event.type !== 'mouseout') {
  755. return;
  756. }
  757. previous = expando.hovered;
  758. expando.hovered = target;
  759. dispatchMoveEvents(chart, listeners, previous, target);
  760. }
  761. function handleClickEvents(chart, event) {
  762. var handlers = chart[EXPANDO_KEY].listeners.click;
  763. var target = handlers && labelAtXY(chart, event.x, event.y);
  764. if (target) {
  765. dispatchEvent(chart, handlers, target);
  766. }
  767. }
  768. Chart.defaults.global.plugins.datalabels = defaults;
  769. Chart.plugins.register({
  770. id: 'datalabels',
  771. beforeInit: function(chart) {
  772. chart[EXPANDO_KEY] = {
  773. actives: []
  774. };
  775. },
  776. beforeUpdate: function(chart) {
  777. var expando = chart[EXPANDO_KEY];
  778. expando.listened = false;
  779. expando.listeners = {}; // {event-type: {dataset-index: function}}
  780. expando.labels = []; // [dataset-index: [labels]]
  781. },
  782. afterDatasetUpdate: function(chart, args, options) {
  783. var datasetIndex = args.index;
  784. var expando = chart[EXPANDO_KEY];
  785. var labels = expando.labels[datasetIndex] = [];
  786. var dataset = chart.data.datasets[datasetIndex];
  787. var config = configure(dataset, options);
  788. var elements = args.meta.data || [];
  789. var ilen = elements.length;
  790. var ctx = chart.ctx;
  791. var i, el, label;
  792. ctx.save();
  793. for (i = 0; i < ilen; ++i) {
  794. el = elements[i];
  795. if (el && !el.hidden && !el._model.skip) {
  796. labels.push(label = new Label(config, ctx, el, i));
  797. label.update(label.$context = {
  798. active: false,
  799. chart: chart,
  800. dataIndex: i,
  801. dataset: dataset,
  802. datasetIndex: datasetIndex
  803. });
  804. } else {
  805. label = null;
  806. }
  807. el[EXPANDO_KEY] = label;
  808. }
  809. ctx.restore();
  810. // Store listeners at the chart level and per event type to optimize
  811. // cases where no listeners are registered for a specific event
  812. helpers.merge(expando.listeners, config.listeners || {}, {
  813. merger: function(key, target, source) {
  814. target[key] = target[key] || {};
  815. target[key][args.index] = source[key];
  816. expando.listened = true;
  817. }
  818. });
  819. },
  820. // Draw labels on top of all dataset elements
  821. // https://github.com/chartjs/chartjs-plugin-datalabels/issues/29
  822. // https://github.com/chartjs/chartjs-plugin-datalabels/issues/32
  823. afterDatasetsDraw: function(chart) {
  824. for (var i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) {
  825. drawLabels(chart, i);
  826. }
  827. },
  828. beforeEvent: function(chart, event) {
  829. // If there is no listener registered for this chart, `listened` will be false,
  830. // meaning we can immediately ignore the incoming event and avoid useless extra
  831. // computation for users who don't implement label interactions.
  832. if (chart[EXPANDO_KEY].listened) {
  833. switch (event.type) {
  834. case 'mousemove':
  835. case 'mouseout':
  836. handleMoveEvents(chart, event);
  837. break;
  838. case 'click':
  839. handleClickEvents(chart, event);
  840. break;
  841. default:
  842. }
  843. }
  844. },
  845. afterEvent: function(chart) {
  846. var expando = chart[EXPANDO_KEY];
  847. var previous = expando.actives;
  848. var actives = expando.actives = chart.lastActive || []; // public API?!
  849. var updates = utils.arrayDiff(previous, actives);
  850. var i, ilen, update, label;
  851. for (i = 0, ilen = updates.length; i < ilen; ++i) {
  852. update = updates[i];
  853. if (update[1]) {
  854. label = update[0][EXPANDO_KEY];
  855. label.$context.active = (update[1] === 1);
  856. label.update(label.$context);
  857. }
  858. }
  859. if ((expando.dirty || updates.length) && !chart.animating) {
  860. chart.render();
  861. }
  862. delete expando.dirty;
  863. }
  864. });
  865. })));