Built files from Bizgaze WebServer
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

chartjs-plugin-datalabels.js 32KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358
  1. /*!
  2. * chartjs-plugin-datalabels v2.0.0
  3. * https://chartjs-plugin-datalabels.netlify.app
  4. * (c) 2017-2021 chartjs-plugin-datalabels contributors
  5. * Released under the MIT license
  6. */
  7. (function (global, factory) {
  8. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('chart.js/helpers'), require('chart.js')) :
  9. typeof define === 'function' && define.amd ? define(['chart.js/helpers', 'chart.js'], factory) :
  10. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ChartDataLabels = factory(global.Chart.helpers, global.Chart));
  11. }(this, (function (helpers, chart_js) { 'use strict';
  12. var devicePixelRatio = (function() {
  13. if (typeof window !== 'undefined') {
  14. if (window.devicePixelRatio) {
  15. return window.devicePixelRatio;
  16. }
  17. // devicePixelRatio is undefined on IE10
  18. // https://stackoverflow.com/a/20204180/8837887
  19. // https://github.com/chartjs/chartjs-plugin-datalabels/issues/85
  20. var screen = window.screen;
  21. if (screen) {
  22. return (screen.deviceXDPI || 1) / (screen.logicalXDPI || 1);
  23. }
  24. }
  25. return 1;
  26. }());
  27. var utils = {
  28. // @todo move this in Chart.helpers.toTextLines
  29. toTextLines: function(inputs) {
  30. var lines = [];
  31. var input;
  32. inputs = [].concat(inputs);
  33. while (inputs.length) {
  34. input = inputs.pop();
  35. if (typeof input === 'string') {
  36. lines.unshift.apply(lines, input.split('\n'));
  37. } else if (Array.isArray(input)) {
  38. inputs.push.apply(inputs, input);
  39. } else if (!helpers.isNullOrUndef(inputs)) {
  40. lines.unshift('' + input);
  41. }
  42. }
  43. return lines;
  44. },
  45. // @todo move this in Chart.helpers.canvas.textSize
  46. // @todo cache calls of measureText if font doesn't change?!
  47. textSize: function(ctx, lines, font) {
  48. var items = [].concat(lines);
  49. var ilen = items.length;
  50. var prev = ctx.font;
  51. var width = 0;
  52. var i;
  53. ctx.font = font.string;
  54. for (i = 0; i < ilen; ++i) {
  55. width = Math.max(ctx.measureText(items[i]).width, width);
  56. }
  57. ctx.font = prev;
  58. return {
  59. height: ilen * font.lineHeight,
  60. width: width
  61. };
  62. },
  63. /**
  64. * Returns value bounded by min and max. This is equivalent to max(min, min(value, max)).
  65. * @todo move this method in Chart.helpers.bound
  66. * https://doc.qt.io/qt-5/qtglobal.html#qBound
  67. */
  68. bound: function(min, value, max) {
  69. return Math.max(min, Math.min(value, max));
  70. },
  71. /**
  72. * Returns an array of pair [value, state] where state is:
  73. * * -1: value is only in a0 (removed)
  74. * * 1: value is only in a1 (added)
  75. */
  76. arrayDiff: function(a0, a1) {
  77. var prev = a0.slice();
  78. var updates = [];
  79. var i, j, ilen, v;
  80. for (i = 0, ilen = a1.length; i < ilen; ++i) {
  81. v = a1[i];
  82. j = prev.indexOf(v);
  83. if (j === -1) {
  84. updates.push([v, 1]);
  85. } else {
  86. prev.splice(j, 1);
  87. }
  88. }
  89. for (i = 0, ilen = prev.length; i < ilen; ++i) {
  90. updates.push([prev[i], -1]);
  91. }
  92. return updates;
  93. },
  94. /**
  95. * https://github.com/chartjs/chartjs-plugin-datalabels/issues/70
  96. */
  97. rasterize: function(v) {
  98. return Math.round(v * devicePixelRatio) / devicePixelRatio;
  99. }
  100. };
  101. function orient(point, origin) {
  102. var x0 = origin.x;
  103. var y0 = origin.y;
  104. if (x0 === null) {
  105. return {x: 0, y: -1};
  106. }
  107. if (y0 === null) {
  108. return {x: 1, y: 0};
  109. }
  110. var dx = point.x - x0;
  111. var dy = point.y - y0;
  112. var ln = Math.sqrt(dx * dx + dy * dy);
  113. return {
  114. x: ln ? dx / ln : 0,
  115. y: ln ? dy / ln : -1
  116. };
  117. }
  118. function aligned(x, y, vx, vy, align) {
  119. switch (align) {
  120. case 'center':
  121. vx = vy = 0;
  122. break;
  123. case 'bottom':
  124. vx = 0;
  125. vy = 1;
  126. break;
  127. case 'right':
  128. vx = 1;
  129. vy = 0;
  130. break;
  131. case 'left':
  132. vx = -1;
  133. vy = 0;
  134. break;
  135. case 'top':
  136. vx = 0;
  137. vy = -1;
  138. break;
  139. case 'start':
  140. vx = -vx;
  141. vy = -vy;
  142. break;
  143. case 'end':
  144. // keep natural orientation
  145. break;
  146. default:
  147. // clockwise rotation (in degree)
  148. align *= (Math.PI / 180);
  149. vx = Math.cos(align);
  150. vy = Math.sin(align);
  151. break;
  152. }
  153. return {
  154. x: x,
  155. y: y,
  156. vx: vx,
  157. vy: vy
  158. };
  159. }
  160. // Line clipping (Cohen–Sutherland algorithm)
  161. // https://en.wikipedia.org/wiki/Cohen–Sutherland_algorithm
  162. var R_INSIDE = 0;
  163. var R_LEFT = 1;
  164. var R_RIGHT = 2;
  165. var R_BOTTOM = 4;
  166. var R_TOP = 8;
  167. function region(x, y, rect) {
  168. var res = R_INSIDE;
  169. if (x < rect.left) {
  170. res |= R_LEFT;
  171. } else if (x > rect.right) {
  172. res |= R_RIGHT;
  173. }
  174. if (y < rect.top) {
  175. res |= R_TOP;
  176. } else if (y > rect.bottom) {
  177. res |= R_BOTTOM;
  178. }
  179. return res;
  180. }
  181. function clipped(segment, area) {
  182. var x0 = segment.x0;
  183. var y0 = segment.y0;
  184. var x1 = segment.x1;
  185. var y1 = segment.y1;
  186. var r0 = region(x0, y0, area);
  187. var r1 = region(x1, y1, area);
  188. var r, x, y;
  189. // eslint-disable-next-line no-constant-condition
  190. while (true) {
  191. if (!(r0 | r1) || (r0 & r1)) {
  192. // both points inside or on the same side: no clipping
  193. break;
  194. }
  195. // at least one point is outside
  196. r = r0 || r1;
  197. if (r & R_TOP) {
  198. x = x0 + (x1 - x0) * (area.top - y0) / (y1 - y0);
  199. y = area.top;
  200. } else if (r & R_BOTTOM) {
  201. x = x0 + (x1 - x0) * (area.bottom - y0) / (y1 - y0);
  202. y = area.bottom;
  203. } else if (r & R_RIGHT) {
  204. y = y0 + (y1 - y0) * (area.right - x0) / (x1 - x0);
  205. x = area.right;
  206. } else if (r & R_LEFT) {
  207. y = y0 + (y1 - y0) * (area.left - x0) / (x1 - x0);
  208. x = area.left;
  209. }
  210. if (r === r0) {
  211. x0 = x;
  212. y0 = y;
  213. r0 = region(x0, y0, area);
  214. } else {
  215. x1 = x;
  216. y1 = y;
  217. r1 = region(x1, y1, area);
  218. }
  219. }
  220. return {
  221. x0: x0,
  222. x1: x1,
  223. y0: y0,
  224. y1: y1
  225. };
  226. }
  227. function compute$1(range, config) {
  228. var anchor = config.anchor;
  229. var segment = range;
  230. var x, y;
  231. if (config.clamp) {
  232. segment = clipped(segment, config.area);
  233. }
  234. if (anchor === 'start') {
  235. x = segment.x0;
  236. y = segment.y0;
  237. } else if (anchor === 'end') {
  238. x = segment.x1;
  239. y = segment.y1;
  240. } else {
  241. x = (segment.x0 + segment.x1) / 2;
  242. y = (segment.y0 + segment.y1) / 2;
  243. }
  244. return aligned(x, y, range.vx, range.vy, config.align);
  245. }
  246. var positioners = {
  247. arc: function(el, config) {
  248. var angle = (el.startAngle + el.endAngle) / 2;
  249. var vx = Math.cos(angle);
  250. var vy = Math.sin(angle);
  251. var r0 = el.innerRadius;
  252. var r1 = el.outerRadius;
  253. return compute$1({
  254. x0: el.x + vx * r0,
  255. y0: el.y + vy * r0,
  256. x1: el.x + vx * r1,
  257. y1: el.y + vy * r1,
  258. vx: vx,
  259. vy: vy
  260. }, config);
  261. },
  262. point: function(el, config) {
  263. var v = orient(el, config.origin);
  264. var rx = v.x * el.options.radius;
  265. var ry = v.y * el.options.radius;
  266. return compute$1({
  267. x0: el.x - rx,
  268. y0: el.y - ry,
  269. x1: el.x + rx,
  270. y1: el.y + ry,
  271. vx: v.x,
  272. vy: v.y
  273. }, config);
  274. },
  275. bar: function(el, config) {
  276. var v = orient(el, config.origin);
  277. var x = el.x;
  278. var y = el.y;
  279. var sx = 0;
  280. var sy = 0;
  281. if (el.horizontal) {
  282. x = Math.min(el.x, el.base);
  283. sx = Math.abs(el.base - el.x);
  284. } else {
  285. y = Math.min(el.y, el.base);
  286. sy = Math.abs(el.base - el.y);
  287. }
  288. return compute$1({
  289. x0: x,
  290. y0: y + sy,
  291. x1: x + sx,
  292. y1: y,
  293. vx: v.x,
  294. vy: v.y
  295. }, config);
  296. },
  297. fallback: function(el, config) {
  298. var v = orient(el, config.origin);
  299. return compute$1({
  300. x0: el.x,
  301. y0: el.y,
  302. x1: el.x,
  303. y1: el.y,
  304. vx: v.x,
  305. vy: v.y
  306. }, config);
  307. }
  308. };
  309. var rasterize = utils.rasterize;
  310. function boundingRects(model) {
  311. var borderWidth = model.borderWidth || 0;
  312. var padding = model.padding;
  313. var th = model.size.height;
  314. var tw = model.size.width;
  315. var tx = -tw / 2;
  316. var ty = -th / 2;
  317. return {
  318. frame: {
  319. x: tx - padding.left - borderWidth,
  320. y: ty - padding.top - borderWidth,
  321. w: tw + padding.width + borderWidth * 2,
  322. h: th + padding.height + borderWidth * 2
  323. },
  324. text: {
  325. x: tx,
  326. y: ty,
  327. w: tw,
  328. h: th
  329. }
  330. };
  331. }
  332. function getScaleOrigin(el, context) {
  333. var scale = context.chart.getDatasetMeta(context.datasetIndex).vScale;
  334. if (!scale) {
  335. return null;
  336. }
  337. if (scale.xCenter !== undefined && scale.yCenter !== undefined) {
  338. return {x: scale.xCenter, y: scale.yCenter};
  339. }
  340. var pixel = scale.getBasePixel();
  341. return el.horizontal ?
  342. {x: pixel, y: null} :
  343. {x: null, y: pixel};
  344. }
  345. function getPositioner(el) {
  346. if (el instanceof chart_js.ArcElement) {
  347. return positioners.arc;
  348. }
  349. if (el instanceof chart_js.PointElement) {
  350. return positioners.point;
  351. }
  352. if (el instanceof chart_js.BarElement) {
  353. return positioners.bar;
  354. }
  355. return positioners.fallback;
  356. }
  357. function drawRoundedRect(ctx, x, y, w, h, radius) {
  358. var HALF_PI = Math.PI / 2;
  359. if (radius) {
  360. var r = Math.min(radius, h / 2, w / 2);
  361. var left = x + r;
  362. var top = y + r;
  363. var right = x + w - r;
  364. var bottom = y + h - r;
  365. ctx.moveTo(x, top);
  366. if (left < right && top < bottom) {
  367. ctx.arc(left, top, r, -Math.PI, -HALF_PI);
  368. ctx.arc(right, top, r, -HALF_PI, 0);
  369. ctx.arc(right, bottom, r, 0, HALF_PI);
  370. ctx.arc(left, bottom, r, HALF_PI, Math.PI);
  371. } else if (left < right) {
  372. ctx.moveTo(left, y);
  373. ctx.arc(right, top, r, -HALF_PI, HALF_PI);
  374. ctx.arc(left, top, r, HALF_PI, Math.PI + HALF_PI);
  375. } else if (top < bottom) {
  376. ctx.arc(left, top, r, -Math.PI, 0);
  377. ctx.arc(left, bottom, r, 0, Math.PI);
  378. } else {
  379. ctx.arc(left, top, r, -Math.PI, Math.PI);
  380. }
  381. ctx.closePath();
  382. ctx.moveTo(x, y);
  383. } else {
  384. ctx.rect(x, y, w, h);
  385. }
  386. }
  387. function drawFrame(ctx, rect, model) {
  388. var bgColor = model.backgroundColor;
  389. var borderColor = model.borderColor;
  390. var borderWidth = model.borderWidth;
  391. if (!bgColor && (!borderColor || !borderWidth)) {
  392. return;
  393. }
  394. ctx.beginPath();
  395. drawRoundedRect(
  396. ctx,
  397. rasterize(rect.x) + borderWidth / 2,
  398. rasterize(rect.y) + borderWidth / 2,
  399. rasterize(rect.w) - borderWidth,
  400. rasterize(rect.h) - borderWidth,
  401. model.borderRadius);
  402. ctx.closePath();
  403. if (bgColor) {
  404. ctx.fillStyle = bgColor;
  405. ctx.fill();
  406. }
  407. if (borderColor && borderWidth) {
  408. ctx.strokeStyle = borderColor;
  409. ctx.lineWidth = borderWidth;
  410. ctx.lineJoin = 'miter';
  411. ctx.stroke();
  412. }
  413. }
  414. function textGeometry(rect, align, font) {
  415. var h = font.lineHeight;
  416. var w = rect.w;
  417. var x = rect.x;
  418. var y = rect.y + h / 2;
  419. if (align === 'center') {
  420. x += w / 2;
  421. } else if (align === 'end' || align === 'right') {
  422. x += w;
  423. }
  424. return {
  425. h: h,
  426. w: w,
  427. x: x,
  428. y: y
  429. };
  430. }
  431. function drawTextLine(ctx, text, cfg) {
  432. var shadow = ctx.shadowBlur;
  433. var stroked = cfg.stroked;
  434. var x = rasterize(cfg.x);
  435. var y = rasterize(cfg.y);
  436. var w = rasterize(cfg.w);
  437. if (stroked) {
  438. ctx.strokeText(text, x, y, w);
  439. }
  440. if (cfg.filled) {
  441. if (shadow && stroked) {
  442. // Prevent drawing shadow on both the text stroke and fill, so
  443. // if the text is stroked, remove the shadow for the text fill.
  444. ctx.shadowBlur = 0;
  445. }
  446. ctx.fillText(text, x, y, w);
  447. if (shadow && stroked) {
  448. ctx.shadowBlur = shadow;
  449. }
  450. }
  451. }
  452. function drawText(ctx, lines, rect, model) {
  453. var align = model.textAlign;
  454. var color = model.color;
  455. var filled = !!color;
  456. var font = model.font;
  457. var ilen = lines.length;
  458. var strokeColor = model.textStrokeColor;
  459. var strokeWidth = model.textStrokeWidth;
  460. var stroked = strokeColor && strokeWidth;
  461. var i;
  462. if (!ilen || (!filled && !stroked)) {
  463. return;
  464. }
  465. // Adjust coordinates based on text alignment and line height
  466. rect = textGeometry(rect, align, font);
  467. ctx.font = font.string;
  468. ctx.textAlign = align;
  469. ctx.textBaseline = 'middle';
  470. ctx.shadowBlur = model.textShadowBlur;
  471. ctx.shadowColor = model.textShadowColor;
  472. if (filled) {
  473. ctx.fillStyle = color;
  474. }
  475. if (stroked) {
  476. ctx.lineJoin = 'round';
  477. ctx.lineWidth = strokeWidth;
  478. ctx.strokeStyle = strokeColor;
  479. }
  480. for (i = 0, ilen = lines.length; i < ilen; ++i) {
  481. drawTextLine(ctx, lines[i], {
  482. stroked: stroked,
  483. filled: filled,
  484. w: rect.w,
  485. x: rect.x,
  486. y: rect.y + rect.h * i
  487. });
  488. }
  489. }
  490. var Label = function(config, ctx, el, index) {
  491. var me = this;
  492. me._config = config;
  493. me._index = index;
  494. me._model = null;
  495. me._rects = null;
  496. me._ctx = ctx;
  497. me._el = el;
  498. };
  499. helpers.merge(Label.prototype, {
  500. /**
  501. * @private
  502. */
  503. _modelize: function(display, lines, config, context) {
  504. var me = this;
  505. var index = me._index;
  506. var font = helpers.toFont(helpers.resolve([config.font, {}], context, index));
  507. var color = helpers.resolve([config.color, chart_js.defaults.color], context, index);
  508. return {
  509. align: helpers.resolve([config.align, 'center'], context, index),
  510. anchor: helpers.resolve([config.anchor, 'center'], context, index),
  511. area: context.chart.chartArea,
  512. backgroundColor: helpers.resolve([config.backgroundColor, null], context, index),
  513. borderColor: helpers.resolve([config.borderColor, null], context, index),
  514. borderRadius: helpers.resolve([config.borderRadius, 0], context, index),
  515. borderWidth: helpers.resolve([config.borderWidth, 0], context, index),
  516. clamp: helpers.resolve([config.clamp, false], context, index),
  517. clip: helpers.resolve([config.clip, false], context, index),
  518. color: color,
  519. display: display,
  520. font: font,
  521. lines: lines,
  522. offset: helpers.resolve([config.offset, 0], context, index),
  523. opacity: helpers.resolve([config.opacity, 1], context, index),
  524. origin: getScaleOrigin(me._el, context),
  525. padding: helpers.toPadding(helpers.resolve([config.padding, 0], context, index)),
  526. positioner: getPositioner(me._el),
  527. rotation: helpers.resolve([config.rotation, 0], context, index) * (Math.PI / 180),
  528. size: utils.textSize(me._ctx, lines, font),
  529. textAlign: helpers.resolve([config.textAlign, 'start'], context, index),
  530. textShadowBlur: helpers.resolve([config.textShadowBlur, 0], context, index),
  531. textShadowColor: helpers.resolve([config.textShadowColor, color], context, index),
  532. textStrokeColor: helpers.resolve([config.textStrokeColor, color], context, index),
  533. textStrokeWidth: helpers.resolve([config.textStrokeWidth, 0], context, index)
  534. };
  535. },
  536. update: function(context) {
  537. var me = this;
  538. var model = null;
  539. var rects = null;
  540. var index = me._index;
  541. var config = me._config;
  542. var value, label, lines;
  543. // We first resolve the display option (separately) to avoid computing
  544. // other options in case the label is hidden (i.e. display: false).
  545. var display = helpers.resolve([config.display, true], context, index);
  546. if (display) {
  547. value = context.dataset.data[index];
  548. label = helpers.valueOrDefault(helpers.callback(config.formatter, [value, context]), value);
  549. lines = helpers.isNullOrUndef(label) ? [] : utils.toTextLines(label);
  550. if (lines.length) {
  551. model = me._modelize(display, lines, config, context);
  552. rects = boundingRects(model);
  553. }
  554. }
  555. me._model = model;
  556. me._rects = rects;
  557. },
  558. geometry: function() {
  559. return this._rects ? this._rects.frame : {};
  560. },
  561. rotation: function() {
  562. return this._model ? this._model.rotation : 0;
  563. },
  564. visible: function() {
  565. return this._model && this._model.opacity;
  566. },
  567. model: function() {
  568. return this._model;
  569. },
  570. draw: function(chart, center) {
  571. var me = this;
  572. var ctx = chart.ctx;
  573. var model = me._model;
  574. var rects = me._rects;
  575. var area;
  576. if (!this.visible()) {
  577. return;
  578. }
  579. ctx.save();
  580. if (model.clip) {
  581. area = model.area;
  582. ctx.beginPath();
  583. ctx.rect(
  584. area.left,
  585. area.top,
  586. area.right - area.left,
  587. area.bottom - area.top);
  588. ctx.clip();
  589. }
  590. ctx.globalAlpha = utils.bound(0, model.opacity, 1);
  591. ctx.translate(rasterize(center.x), rasterize(center.y));
  592. ctx.rotate(model.rotation);
  593. drawFrame(ctx, rects.frame, model);
  594. drawText(ctx, model.lines, rects.text, model);
  595. ctx.restore();
  596. }
  597. });
  598. var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; // eslint-disable-line es/no-number-minsafeinteger
  599. var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; // eslint-disable-line es/no-number-maxsafeinteger
  600. function rotated(point, center, angle) {
  601. var cos = Math.cos(angle);
  602. var sin = Math.sin(angle);
  603. var cx = center.x;
  604. var cy = center.y;
  605. return {
  606. x: cx + cos * (point.x - cx) - sin * (point.y - cy),
  607. y: cy + sin * (point.x - cx) + cos * (point.y - cy)
  608. };
  609. }
  610. function projected(points, axis) {
  611. var min = MAX_INTEGER;
  612. var max = MIN_INTEGER;
  613. var origin = axis.origin;
  614. var i, pt, vx, vy, dp;
  615. for (i = 0; i < points.length; ++i) {
  616. pt = points[i];
  617. vx = pt.x - origin.x;
  618. vy = pt.y - origin.y;
  619. dp = axis.vx * vx + axis.vy * vy;
  620. min = Math.min(min, dp);
  621. max = Math.max(max, dp);
  622. }
  623. return {
  624. min: min,
  625. max: max
  626. };
  627. }
  628. function toAxis(p0, p1) {
  629. var vx = p1.x - p0.x;
  630. var vy = p1.y - p0.y;
  631. var ln = Math.sqrt(vx * vx + vy * vy);
  632. return {
  633. vx: (p1.x - p0.x) / ln,
  634. vy: (p1.y - p0.y) / ln,
  635. origin: p0,
  636. ln: ln
  637. };
  638. }
  639. var HitBox = function() {
  640. this._rotation = 0;
  641. this._rect = {
  642. x: 0,
  643. y: 0,
  644. w: 0,
  645. h: 0
  646. };
  647. };
  648. helpers.merge(HitBox.prototype, {
  649. center: function() {
  650. var r = this._rect;
  651. return {
  652. x: r.x + r.w / 2,
  653. y: r.y + r.h / 2
  654. };
  655. },
  656. update: function(center, rect, rotation) {
  657. this._rotation = rotation;
  658. this._rect = {
  659. x: rect.x + center.x,
  660. y: rect.y + center.y,
  661. w: rect.w,
  662. h: rect.h
  663. };
  664. },
  665. contains: function(point) {
  666. var me = this;
  667. var margin = 1;
  668. var rect = me._rect;
  669. point = rotated(point, me.center(), -me._rotation);
  670. return !(point.x < rect.x - margin
  671. || point.y < rect.y - margin
  672. || point.x > rect.x + rect.w + margin * 2
  673. || point.y > rect.y + rect.h + margin * 2);
  674. },
  675. // Separating Axis Theorem
  676. // https://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem--gamedev-169
  677. intersects: function(other) {
  678. var r0 = this._points();
  679. var r1 = other._points();
  680. var axes = [
  681. toAxis(r0[0], r0[1]),
  682. toAxis(r0[0], r0[3])
  683. ];
  684. var i, pr0, pr1;
  685. if (this._rotation !== other._rotation) {
  686. // Only separate with r1 axis if the rotation is different,
  687. // else it's enough to separate r0 and r1 with r0 axis only!
  688. axes.push(
  689. toAxis(r1[0], r1[1]),
  690. toAxis(r1[0], r1[3])
  691. );
  692. }
  693. for (i = 0; i < axes.length; ++i) {
  694. pr0 = projected(r0, axes[i]);
  695. pr1 = projected(r1, axes[i]);
  696. if (pr0.max < pr1.min || pr1.max < pr0.min) {
  697. return false;
  698. }
  699. }
  700. return true;
  701. },
  702. /**
  703. * @private
  704. */
  705. _points: function() {
  706. var me = this;
  707. var rect = me._rect;
  708. var angle = me._rotation;
  709. var center = me.center();
  710. return [
  711. rotated({x: rect.x, y: rect.y}, center, angle),
  712. rotated({x: rect.x + rect.w, y: rect.y}, center, angle),
  713. rotated({x: rect.x + rect.w, y: rect.y + rect.h}, center, angle),
  714. rotated({x: rect.x, y: rect.y + rect.h}, center, angle)
  715. ];
  716. }
  717. });
  718. function coordinates(el, model, geometry) {
  719. var point = model.positioner(el, model);
  720. var vx = point.vx;
  721. var vy = point.vy;
  722. if (!vx && !vy) {
  723. // if aligned center, we don't want to offset the center point
  724. return {x: point.x, y: point.y};
  725. }
  726. var w = geometry.w;
  727. var h = geometry.h;
  728. // take in account the label rotation
  729. var rotation = model.rotation;
  730. var dx = Math.abs(w / 2 * Math.cos(rotation)) + Math.abs(h / 2 * Math.sin(rotation));
  731. var dy = Math.abs(w / 2 * Math.sin(rotation)) + Math.abs(h / 2 * Math.cos(rotation));
  732. // scale the unit vector (vx, vy) to get at least dx or dy equal to
  733. // w or h respectively (else we would calculate the distance to the
  734. // ellipse inscribed in the bounding rect)
  735. var vs = 1 / Math.max(Math.abs(vx), Math.abs(vy));
  736. dx *= vx * vs;
  737. dy *= vy * vs;
  738. // finally, include the explicit offset
  739. dx += model.offset * vx;
  740. dy += model.offset * vy;
  741. return {
  742. x: point.x + dx,
  743. y: point.y + dy
  744. };
  745. }
  746. function collide(labels, collider) {
  747. var i, j, s0, s1;
  748. // IMPORTANT Iterate in the reverse order since items at the end of the
  749. // list have an higher weight/priority and thus should be less impacted
  750. // by the overlapping strategy.
  751. for (i = labels.length - 1; i >= 0; --i) {
  752. s0 = labels[i].$layout;
  753. for (j = i - 1; j >= 0 && s0._visible; --j) {
  754. s1 = labels[j].$layout;
  755. if (s1._visible && s0._box.intersects(s1._box)) {
  756. collider(s0, s1);
  757. }
  758. }
  759. }
  760. return labels;
  761. }
  762. function compute(labels) {
  763. var i, ilen, label, state, geometry, center, proxy;
  764. // Initialize labels for overlap detection
  765. for (i = 0, ilen = labels.length; i < ilen; ++i) {
  766. label = labels[i];
  767. state = label.$layout;
  768. if (state._visible) {
  769. // Chart.js 3 removed el._model in favor of getProps(), making harder to
  770. // abstract reading values in positioners. Also, using string arrays to
  771. // read values (i.e. var {a,b,c} = el.getProps(["a","b","c"])) would make
  772. // positioners inefficient in the normal case (i.e. not the final values)
  773. // and the code a bit ugly, so let's use a Proxy instead.
  774. proxy = new Proxy(label._el, {get: (el, p) => el.getProps([p], true)[p]});
  775. geometry = label.geometry();
  776. center = coordinates(proxy, label.model(), geometry);
  777. state._box.update(center, geometry, label.rotation());
  778. }
  779. }
  780. // Auto hide overlapping labels
  781. return collide(labels, function(s0, s1) {
  782. var h0 = s0._hidable;
  783. var h1 = s1._hidable;
  784. if ((h0 && h1) || h1) {
  785. s1._visible = false;
  786. } else if (h0) {
  787. s0._visible = false;
  788. }
  789. });
  790. }
  791. var layout = {
  792. prepare: function(datasets) {
  793. var labels = [];
  794. var i, j, ilen, jlen, label;
  795. for (i = 0, ilen = datasets.length; i < ilen; ++i) {
  796. for (j = 0, jlen = datasets[i].length; j < jlen; ++j) {
  797. label = datasets[i][j];
  798. labels.push(label);
  799. label.$layout = {
  800. _box: new HitBox(),
  801. _hidable: false,
  802. _visible: true,
  803. _set: i,
  804. _idx: j
  805. };
  806. }
  807. }
  808. // TODO New `z` option: labels with a higher z-index are drawn
  809. // of top of the ones with a lower index. Lowest z-index labels
  810. // are also discarded first when hiding overlapping labels.
  811. labels.sort(function(a, b) {
  812. var sa = a.$layout;
  813. var sb = b.$layout;
  814. return sa._idx === sb._idx
  815. ? sb._set - sa._set
  816. : sb._idx - sa._idx;
  817. });
  818. this.update(labels);
  819. return labels;
  820. },
  821. update: function(labels) {
  822. var dirty = false;
  823. var i, ilen, label, model, state;
  824. for (i = 0, ilen = labels.length; i < ilen; ++i) {
  825. label = labels[i];
  826. model = label.model();
  827. state = label.$layout;
  828. state._hidable = model && model.display === 'auto';
  829. state._visible = label.visible();
  830. dirty |= state._hidable;
  831. }
  832. if (dirty) {
  833. compute(labels);
  834. }
  835. },
  836. lookup: function(labels, point) {
  837. var i, state;
  838. // IMPORTANT Iterate in the reverse order since items at the end of
  839. // the list have an higher z-index, thus should be picked first.
  840. for (i = labels.length - 1; i >= 0; --i) {
  841. state = labels[i].$layout;
  842. if (state && state._visible && state._box.contains(point)) {
  843. return labels[i];
  844. }
  845. }
  846. return null;
  847. },
  848. draw: function(chart, labels) {
  849. var i, ilen, label, state, geometry, center;
  850. for (i = 0, ilen = labels.length; i < ilen; ++i) {
  851. label = labels[i];
  852. state = label.$layout;
  853. if (state._visible) {
  854. geometry = label.geometry();
  855. center = coordinates(label._el, label.model(), geometry);
  856. state._box.update(center, geometry, label.rotation());
  857. label.draw(chart, center);
  858. }
  859. }
  860. }
  861. };
  862. var formatter = function(value) {
  863. if (helpers.isNullOrUndef(value)) {
  864. return null;
  865. }
  866. var label = value;
  867. var keys, klen, k;
  868. if (helpers.isObject(value)) {
  869. if (!helpers.isNullOrUndef(value.label)) {
  870. label = value.label;
  871. } else if (!helpers.isNullOrUndef(value.r)) {
  872. label = value.r;
  873. } else {
  874. label = '';
  875. keys = Object.keys(value);
  876. for (k = 0, klen = keys.length; k < klen; ++k) {
  877. label += (k !== 0 ? ', ' : '') + keys[k] + ': ' + value[keys[k]];
  878. }
  879. }
  880. }
  881. return '' + label;
  882. };
  883. /**
  884. * IMPORTANT: make sure to also update tests and TypeScript definition
  885. * files (`/test/specs/defaults.spec.js` and `/types/options.d.ts`)
  886. */
  887. var defaults = {
  888. align: 'center',
  889. anchor: 'center',
  890. backgroundColor: null,
  891. borderColor: null,
  892. borderRadius: 0,
  893. borderWidth: 0,
  894. clamp: false,
  895. clip: false,
  896. color: undefined,
  897. display: true,
  898. font: {
  899. family: undefined,
  900. lineHeight: 1.2,
  901. size: undefined,
  902. style: undefined,
  903. weight: null
  904. },
  905. formatter: formatter,
  906. labels: undefined,
  907. listeners: {},
  908. offset: 4,
  909. opacity: 1,
  910. padding: {
  911. top: 4,
  912. right: 4,
  913. bottom: 4,
  914. left: 4
  915. },
  916. rotation: 0,
  917. textAlign: 'start',
  918. textStrokeColor: undefined,
  919. textStrokeWidth: 0,
  920. textShadowBlur: 0,
  921. textShadowColor: undefined
  922. };
  923. /**
  924. * @see https://github.com/chartjs/Chart.js/issues/4176
  925. */
  926. var EXPANDO_KEY = '$datalabels';
  927. var DEFAULT_KEY = '$default';
  928. function configure(dataset, options) {
  929. var override = dataset.datalabels;
  930. var listeners = {};
  931. var configs = [];
  932. var labels, keys;
  933. if (override === false) {
  934. return null;
  935. }
  936. if (override === true) {
  937. override = {};
  938. }
  939. options = helpers.merge({}, [options, override]);
  940. labels = options.labels || {};
  941. keys = Object.keys(labels);
  942. delete options.labels;
  943. if (keys.length) {
  944. keys.forEach(function(key) {
  945. if (labels[key]) {
  946. configs.push(helpers.merge({}, [
  947. options,
  948. labels[key],
  949. {_key: key}
  950. ]));
  951. }
  952. });
  953. } else {
  954. // Default label if no "named" label defined.
  955. configs.push(options);
  956. }
  957. // listeners: {<event-type>: {<label-key>: <fn>}}
  958. listeners = configs.reduce(function(target, config) {
  959. helpers.each(config.listeners || {}, function(fn, event) {
  960. target[event] = target[event] || {};
  961. target[event][config._key || DEFAULT_KEY] = fn;
  962. });
  963. delete config.listeners;
  964. return target;
  965. }, {});
  966. return {
  967. labels: configs,
  968. listeners: listeners
  969. };
  970. }
  971. function dispatchEvent(chart, listeners, label) {
  972. if (!listeners) {
  973. return;
  974. }
  975. var context = label.$context;
  976. var groups = label.$groups;
  977. var callback;
  978. if (!listeners[groups._set]) {
  979. return;
  980. }
  981. callback = listeners[groups._set][groups._key];
  982. if (!callback) {
  983. return;
  984. }
  985. if (helpers.callback(callback, [context]) === true) {
  986. // Users are allowed to tweak the given context by injecting values that can be
  987. // used in scriptable options to display labels differently based on the current
  988. // event (e.g. highlight an hovered label). That's why we update the label with
  989. // the output context and schedule a new chart render by setting it dirty.
  990. chart[EXPANDO_KEY]._dirty = true;
  991. label.update(context);
  992. }
  993. }
  994. function dispatchMoveEvents(chart, listeners, previous, label) {
  995. var enter, leave;
  996. if (!previous && !label) {
  997. return;
  998. }
  999. if (!previous) {
  1000. enter = true;
  1001. } else if (!label) {
  1002. leave = true;
  1003. } else if (previous !== label) {
  1004. leave = enter = true;
  1005. }
  1006. if (leave) {
  1007. dispatchEvent(chart, listeners.leave, previous);
  1008. }
  1009. if (enter) {
  1010. dispatchEvent(chart, listeners.enter, label);
  1011. }
  1012. }
  1013. function handleMoveEvents(chart, event) {
  1014. var expando = chart[EXPANDO_KEY];
  1015. var listeners = expando._listeners;
  1016. var previous, label;
  1017. if (!listeners.enter && !listeners.leave) {
  1018. return;
  1019. }
  1020. if (event.type === 'mousemove') {
  1021. label = layout.lookup(expando._labels, event);
  1022. } else if (event.type !== 'mouseout') {
  1023. return;
  1024. }
  1025. previous = expando._hovered;
  1026. expando._hovered = label;
  1027. dispatchMoveEvents(chart, listeners, previous, label);
  1028. }
  1029. function handleClickEvents(chart, event) {
  1030. var expando = chart[EXPANDO_KEY];
  1031. var handlers = expando._listeners.click;
  1032. var label = handlers && layout.lookup(expando._labels, event);
  1033. if (label) {
  1034. dispatchEvent(chart, handlers, label);
  1035. }
  1036. }
  1037. var plugin = {
  1038. id: 'datalabels',
  1039. defaults: defaults,
  1040. beforeInit: function(chart) {
  1041. chart[EXPANDO_KEY] = {
  1042. _actives: []
  1043. };
  1044. },
  1045. beforeUpdate: function(chart) {
  1046. var expando = chart[EXPANDO_KEY];
  1047. expando._listened = false;
  1048. expando._listeners = {}; // {<event-type>: {<dataset-index>: {<label-key>: <fn>}}}
  1049. expando._datasets = []; // per dataset labels: [Label[]]
  1050. expando._labels = []; // layouted labels: Label[]
  1051. },
  1052. afterDatasetUpdate: function(chart, args, options) {
  1053. var datasetIndex = args.index;
  1054. var expando = chart[EXPANDO_KEY];
  1055. var labels = expando._datasets[datasetIndex] = [];
  1056. var visible = chart.isDatasetVisible(datasetIndex);
  1057. var dataset = chart.data.datasets[datasetIndex];
  1058. var config = configure(dataset, options);
  1059. var elements = args.meta.data || [];
  1060. var ctx = chart.ctx;
  1061. var i, j, ilen, jlen, cfg, key, el, label;
  1062. ctx.save();
  1063. for (i = 0, ilen = elements.length; i < ilen; ++i) {
  1064. el = elements[i];
  1065. el[EXPANDO_KEY] = [];
  1066. if (visible && el && chart.getDataVisibility(i) && !el.skip) {
  1067. for (j = 0, jlen = config.labels.length; j < jlen; ++j) {
  1068. cfg = config.labels[j];
  1069. key = cfg._key;
  1070. label = new Label(cfg, ctx, el, i);
  1071. label.$groups = {
  1072. _set: datasetIndex,
  1073. _key: key || DEFAULT_KEY
  1074. };
  1075. label.$context = {
  1076. active: false,
  1077. chart: chart,
  1078. dataIndex: i,
  1079. dataset: dataset,
  1080. datasetIndex: datasetIndex
  1081. };
  1082. label.update(label.$context);
  1083. el[EXPANDO_KEY].push(label);
  1084. labels.push(label);
  1085. }
  1086. }
  1087. }
  1088. ctx.restore();
  1089. // Store listeners at the chart level and per event type to optimize
  1090. // cases where no listeners are registered for a specific event.
  1091. helpers.merge(expando._listeners, config.listeners, {
  1092. merger: function(event, target, source) {
  1093. target[event] = target[event] || {};
  1094. target[event][args.index] = source[event];
  1095. expando._listened = true;
  1096. }
  1097. });
  1098. },
  1099. afterUpdate: function(chart, options) {
  1100. chart[EXPANDO_KEY]._labels = layout.prepare(
  1101. chart[EXPANDO_KEY]._datasets,
  1102. options);
  1103. },
  1104. // Draw labels on top of all dataset elements
  1105. // https://github.com/chartjs/chartjs-plugin-datalabels/issues/29
  1106. // https://github.com/chartjs/chartjs-plugin-datalabels/issues/32
  1107. afterDatasetsDraw: function(chart) {
  1108. layout.draw(chart, chart[EXPANDO_KEY]._labels);
  1109. },
  1110. beforeEvent: function(chart, args) {
  1111. // If there is no listener registered for this chart, `listened` will be false,
  1112. // meaning we can immediately ignore the incoming event and avoid useless extra
  1113. // computation for users who don't implement label interactions.
  1114. if (chart[EXPANDO_KEY]._listened) {
  1115. var event = args.event;
  1116. switch (event.type) {
  1117. case 'mousemove':
  1118. case 'mouseout':
  1119. handleMoveEvents(chart, event);
  1120. break;
  1121. case 'click':
  1122. handleClickEvents(chart, event);
  1123. break;
  1124. }
  1125. }
  1126. },
  1127. afterEvent: function(chart) {
  1128. var expando = chart[EXPANDO_KEY];
  1129. var previous = expando._actives;
  1130. var actives = expando._actives = chart.getActiveElements();
  1131. var updates = utils.arrayDiff(previous, actives);
  1132. var i, ilen, j, jlen, update, label, labels;
  1133. for (i = 0, ilen = updates.length; i < ilen; ++i) {
  1134. update = updates[i];
  1135. if (update[1]) {
  1136. labels = update[0].element[EXPANDO_KEY] || [];
  1137. for (j = 0, jlen = labels.length; j < jlen; ++j) {
  1138. label = labels[j];
  1139. label.$context.active = (update[1] === 1);
  1140. label.update(label.$context);
  1141. }
  1142. }
  1143. }
  1144. if (expando._dirty || updates.length) {
  1145. layout.update(expando._labels);
  1146. chart.render();
  1147. }
  1148. delete expando._dirty;
  1149. }
  1150. };
  1151. return plugin;
  1152. })));