series-label.src.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787
  1. /**
  2. * @license Highcharts JS v8.1.2 (2020-06-16)
  3. *
  4. * (c) 2009-2019 Torstein Honsi
  5. *
  6. * License: www.highcharts.com/license
  7. */
  8. 'use strict';
  9. (function (factory) {
  10. if (typeof module === 'object' && module.exports) {
  11. factory['default'] = factory;
  12. module.exports = factory;
  13. } else if (typeof define === 'function' && define.amd) {
  14. define('highcharts/modules/series-label', ['highcharts'], function (Highcharts) {
  15. factory(Highcharts);
  16. factory.Highcharts = Highcharts;
  17. return factory;
  18. });
  19. } else {
  20. factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined);
  21. }
  22. }(function (Highcharts) {
  23. var _modules = Highcharts ? Highcharts._modules : {};
  24. function _registerModule(obj, path, args, fn) {
  25. if (!obj.hasOwnProperty(path)) {
  26. obj[path] = fn.apply(null, args);
  27. }
  28. }
  29. _registerModule(_modules, 'modules/series-label.src.js', [_modules['parts/Chart.js'], _modules['parts/Globals.js'], _modules['parts/SVGRenderer.js'], _modules['parts/Utilities.js']], function (Chart, H, SVGRenderer, U) {
  30. /* *
  31. *
  32. * (c) 2009-2020 Torstein Honsi
  33. *
  34. * License: www.highcharts.com/license
  35. *
  36. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  37. *
  38. * */
  39. var addEvent = U.addEvent, animObject = U.animObject, extend = U.extend, fireEvent = U.fireEvent, format = U.format, isNumber = U.isNumber, pick = U.pick, setOptions = U.setOptions, syncTimeout = U.syncTimeout;
  40. /**
  41. * Containing the position of a box that should be avoided by labels.
  42. *
  43. * @interface Highcharts.LabelIntersectBoxObject
  44. */ /**
  45. * @name Highcharts.LabelIntersectBoxObject#bottom
  46. * @type {number}
  47. */ /**
  48. * @name Highcharts.LabelIntersectBoxObject#left
  49. * @type {number}
  50. */ /**
  51. * @name Highcharts.LabelIntersectBoxObject#right
  52. * @type {number}
  53. */ /**
  54. * @name Highcharts.LabelIntersectBoxObject#top
  55. * @type {number}
  56. */
  57. /*
  58. * Highcharts module to place labels next to a series in a natural position.
  59. *
  60. * TODO:
  61. * - add column support (box collision detection, boxesToAvoid logic)
  62. * - avoid data labels, when data labels above, show series label below.
  63. * - add more options (connector, format, formatter)
  64. *
  65. * https://jsfiddle.net/highcharts/L2u9rpwr/
  66. * https://jsfiddle.net/highcharts/y5A37/
  67. * https://jsfiddle.net/highcharts/264Nm/
  68. * https://jsfiddle.net/highcharts/y5A37/
  69. */
  70. ''; // detach doclets above
  71. var labelDistance = 3, Series = H.Series;
  72. setOptions({
  73. /**
  74. * @optionparent plotOptions
  75. *
  76. * @private
  77. */
  78. plotOptions: {
  79. series: {
  80. /**
  81. * Series labels are placed as close to the series as possible in a
  82. * natural way, seeking to avoid other series. The goal of this
  83. * feature is to make the chart more easily readable, like if a
  84. * human designer placed the labels in the optimal position.
  85. *
  86. * The series labels currently work with series types having a
  87. * `graph` or an `area`.
  88. *
  89. * @sample highcharts/series-label/line-chart
  90. * Line chart
  91. * @sample highcharts/demo/streamgraph
  92. * Stream graph
  93. * @sample highcharts/series-label/stock-chart
  94. * Stock chart
  95. *
  96. * @declare Highcharts.SeriesLabelOptionsObject
  97. * @since 6.0.0
  98. * @product highcharts highstock gantt
  99. * @requires modules/series-label
  100. */
  101. label: {
  102. /**
  103. * Enable the series label per series.
  104. */
  105. enabled: true,
  106. /**
  107. * Allow labels to be placed distant to the graph if necessary,
  108. * and draw a connector line to the graph. Setting this option
  109. * to true may decrease the performance significantly, since the
  110. * algorithm with systematically search for open spaces in the
  111. * whole plot area. Visually, it may also result in a more
  112. * cluttered chart, though more of the series will be labeled.
  113. */
  114. connectorAllowed: false,
  115. /**
  116. * If the label is closer than this to a neighbour graph, draw a
  117. * connector.
  118. */
  119. connectorNeighbourDistance: 24,
  120. /**
  121. * A format string for the label, with support for a subset of
  122. * HTML. Variables are enclosed by curly brackets. Available
  123. * variables are `name`, `options.xxx`, `color` and other
  124. * members from the `series` object. Use this option also to set
  125. * a static text for the label.
  126. *
  127. * @type string
  128. * @since 8.1.0
  129. */
  130. format: void 0,
  131. /**
  132. * Callback function to format each of the series' labels. The
  133. * `this` keyword refers to the series object. By default the
  134. * `formatter` is undefined and the `series.name` is rendered.
  135. *
  136. * @type {Highcharts.FormatterCallbackFunction<Series>}
  137. * @since 8.1.0
  138. */
  139. formatter: void 0,
  140. /**
  141. * For area-like series, allow the font size to vary so that
  142. * small areas get a smaller font size. The default applies this
  143. * effect to area-like series but not line-like series.
  144. *
  145. * @type {number|null}
  146. */
  147. minFontSize: null,
  148. /**
  149. * For area-like series, allow the font size to vary so that
  150. * small areas get a smaller font size. The default applies this
  151. * effect to area-like series but not line-like series.
  152. *
  153. * @type {number|null}
  154. */
  155. maxFontSize: null,
  156. /**
  157. * Draw the label on the area of an area series. By default it
  158. * is drawn on the area. Set it to `false` to draw it next to
  159. * the graph instead.
  160. *
  161. * @type {boolean|null}
  162. */
  163. onArea: null,
  164. /**
  165. * Styles for the series label. The color defaults to the series
  166. * color, or a contrast color if `onArea`.
  167. *
  168. * @type {Highcharts.CSSObject}
  169. */
  170. style: {
  171. /** @internal */
  172. fontWeight: 'bold'
  173. },
  174. /**
  175. * An array of boxes to avoid when laying out the labels. Each
  176. * item has a `left`, `right`, `top` and `bottom` property.
  177. *
  178. * @type {Array<Highcharts.LabelIntersectBoxObject>}
  179. */
  180. boxesToAvoid: []
  181. }
  182. }
  183. }
  184. });
  185. /* eslint-disable valid-jsdoc */
  186. /**
  187. * Counter-clockwise, part of the fast line intersection logic.
  188. *
  189. * @private
  190. * @function ccw
  191. */
  192. function ccw(x1, y1, x2, y2, x3, y3) {
  193. var cw = ((y3 - y1) * (x2 - x1)) - ((y2 - y1) * (x3 - x1));
  194. return cw > 0 ? true : !(cw < 0);
  195. }
  196. /**
  197. * Detect if two lines intersect.
  198. *
  199. * @private
  200. * @function intersectLine
  201. */
  202. function intersectLine(x1, y1, x2, y2, x3, y3, x4, y4) {
  203. return ccw(x1, y1, x3, y3, x4, y4) !== ccw(x2, y2, x3, y3, x4, y4) &&
  204. ccw(x1, y1, x2, y2, x3, y3) !== ccw(x1, y1, x2, y2, x4, y4);
  205. }
  206. /**
  207. * Detect if a box intersects with a line.
  208. *
  209. * @private
  210. * @function boxIntersectLine
  211. */
  212. function boxIntersectLine(x, y, w, h, x1, y1, x2, y2) {
  213. return (intersectLine(x, y, x + w, y, x1, y1, x2, y2) || // top of label
  214. intersectLine(x + w, y, x + w, y + h, x1, y1, x2, y2) || // right
  215. intersectLine(x, y + h, x + w, y + h, x1, y1, x2, y2) || // bottom
  216. intersectLine(x, y, x, y + h, x1, y1, x2, y2) // left of label
  217. );
  218. }
  219. /**
  220. * General symbol definition for labels with connector.
  221. *
  222. * @private
  223. * @function Highcharts.SVGRenderer#symbols.connector
  224. */
  225. SVGRenderer.prototype.symbols.connector = function (x, y, w, h, options) {
  226. var anchorX = options && options.anchorX, anchorY = options && options.anchorY, path, yOffset, lateral = w / 2;
  227. if (isNumber(anchorX) && isNumber(anchorY)) {
  228. path = [['M', anchorX, anchorY]];
  229. // Prefer 45 deg connectors
  230. yOffset = y - anchorY;
  231. if (yOffset < 0) {
  232. yOffset = -h - yOffset;
  233. }
  234. if (yOffset < w) {
  235. lateral = anchorX < x + (w / 2) ? yOffset : w - yOffset;
  236. }
  237. // Anchor below label
  238. if (anchorY > y + h) {
  239. path.push(['L', x + lateral, y + h]);
  240. // Anchor above label
  241. }
  242. else if (anchorY < y) {
  243. path.push(['L', x + lateral, y]);
  244. // Anchor left of label
  245. }
  246. else if (anchorX < x) {
  247. path.push(['L', x, y + h / 2]);
  248. // Anchor right of label
  249. }
  250. else if (anchorX > x + w) {
  251. path.push(['L', x + w, y + h / 2]);
  252. }
  253. }
  254. return path || [];
  255. };
  256. /**
  257. * Points to avoid. In addition to actual data points, the label should avoid
  258. * interpolated positions.
  259. *
  260. * @private
  261. * @function Highcharts.Series#getPointsOnGraph
  262. */
  263. Series.prototype.getPointsOnGraph = function () {
  264. if (!this.xAxis && !this.yAxis) {
  265. return;
  266. }
  267. var distance = 16, points = this.points, point, last, interpolated = [], i, deltaX, deltaY, delta, len, n, j, d, graph = this.graph || this.area, node = graph.element, inverted = this.chart.inverted, xAxis = this.xAxis, yAxis = this.yAxis, paneLeft = inverted ? yAxis.pos : xAxis.pos, paneTop = inverted ? xAxis.pos : yAxis.pos, onArea = pick(this.options.label.onArea, !!this.area), translatedThreshold = yAxis.getThreshold(this.options.threshold), grid = {};
  268. /**
  269. * Push the point to the interpolated points, but only if that position in
  270. * the grid has not been occupied. As a performance optimization, we divide
  271. * the plot area into a grid and only add one point per series (#9815).
  272. * @private
  273. */
  274. function pushDiscrete(point) {
  275. var cellSize = 8, key = Math.round(point.plotX / cellSize) + ',' +
  276. Math.round(point.plotY / cellSize);
  277. if (!grid[key]) {
  278. grid[key] = 1;
  279. interpolated.push(point);
  280. }
  281. }
  282. // For splines, get the point at length (possible caveat: peaks are not
  283. // correctly detected)
  284. if (this.getPointSpline &&
  285. node.getPointAtLength &&
  286. !onArea &&
  287. // Not performing well on complex series, node.getPointAtLength is too
  288. // heavy (#9815)
  289. points.length < this.chart.plotSizeX / distance) {
  290. // If it is animating towards a path definition, use that briefly, and
  291. // reset
  292. if (graph.toD) {
  293. d = graph.attr('d');
  294. graph.attr({ d: graph.toD });
  295. }
  296. len = node.getTotalLength();
  297. for (i = 0; i < len; i += distance) {
  298. point = node.getPointAtLength(i);
  299. pushDiscrete({
  300. chartX: paneLeft + point.x,
  301. chartY: paneTop + point.y,
  302. plotX: point.x,
  303. plotY: point.y
  304. });
  305. }
  306. if (d) {
  307. graph.attr({ d: d });
  308. }
  309. // Last point
  310. point = points[points.length - 1];
  311. point.chartX = paneLeft + point.plotX;
  312. point.chartY = paneTop + point.plotY;
  313. pushDiscrete(point);
  314. // Interpolate
  315. }
  316. else {
  317. len = points.length;
  318. for (i = 0; i < len; i += 1) {
  319. point = points[i];
  320. last = points[i - 1];
  321. // Absolute coordinates so we can compare different panes
  322. point.chartX = paneLeft + point.plotX;
  323. point.chartY = paneTop + point.plotY;
  324. if (onArea) {
  325. // Vertically centered inside area
  326. point.chartCenterY = paneTop + (point.plotY +
  327. pick(point.yBottom, translatedThreshold)) / 2;
  328. }
  329. // Add interpolated points
  330. if (i > 0) {
  331. deltaX = Math.abs(point.chartX - last.chartX);
  332. deltaY = Math.abs(point.chartY - last.chartY);
  333. delta = Math.max(deltaX, deltaY);
  334. if (delta > distance) {
  335. n = Math.ceil(delta / distance);
  336. for (j = 1; j < n; j += 1) {
  337. pushDiscrete({
  338. chartX: last.chartX +
  339. (point.chartX - last.chartX) *
  340. (j / n),
  341. chartY: last.chartY +
  342. (point.chartY - last.chartY) *
  343. (j / n),
  344. chartCenterY: last.chartCenterY +
  345. (point.chartCenterY -
  346. last.chartCenterY) * (j / n),
  347. plotX: last.plotX +
  348. (point.plotX - last.plotX) *
  349. (j / n),
  350. plotY: last.plotY +
  351. (point.plotY - last.plotY) *
  352. (j / n)
  353. });
  354. }
  355. }
  356. }
  357. // Add the real point in order to find positive and negative peaks
  358. if (isNumber(point.plotY)) {
  359. pushDiscrete(point);
  360. }
  361. }
  362. }
  363. // Get the bounding box so we can do a quick check first if the bounding
  364. // boxes overlap.
  365. /*
  366. interpolated.bBox = node.getBBox();
  367. interpolated.bBox.x += paneLeft;
  368. interpolated.bBox.y += paneTop;
  369. */
  370. return interpolated;
  371. };
  372. /**
  373. * Overridable function to return series-specific font sizes for the labels. By
  374. * default it returns bigger font sizes for series with the greater sum of y
  375. * values.
  376. *
  377. * @private
  378. * @function Highcharts.Series#labelFontSize
  379. */
  380. Series.prototype.labelFontSize = function (minFontSize, maxFontSize) {
  381. return minFontSize + ((this.sum / this.chart.labelSeriesMaxSum) *
  382. (maxFontSize - minFontSize)) + 'px';
  383. };
  384. /**
  385. * Check whether a proposed label position is clear of other elements.
  386. *
  387. * @private
  388. * @function Highcharts.Series#checkClearPoint
  389. */
  390. Series.prototype.checkClearPoint = function (x, y, bBox, checkDistance) {
  391. var distToOthersSquared = Number.MAX_VALUE, // distance to other graphs
  392. distToPointSquared = Number.MAX_VALUE, dist, connectorPoint, onArea = pick(this.options.label.onArea, !!this.area), findDistanceToOthers = (onArea || this.options.label.connectorAllowed), chart = this.chart, series, points, leastDistance = 16, withinRange, xDist, yDist, i, j;
  393. /**
  394. * @private
  395. */
  396. function intersectRect(r1, r2) {
  397. return !(r2.left > r1.right ||
  398. r2.right < r1.left ||
  399. r2.top > r1.bottom ||
  400. r2.bottom < r1.top);
  401. }
  402. /**
  403. * Get the weight in order to determine the ideal position. Larger distance
  404. * to other series gives more weight. Smaller distance to the actual point
  405. * (connector points only) gives more weight.
  406. * @private
  407. */
  408. function getWeight(distToOthersSquared, distToPointSquared) {
  409. return distToOthersSquared - distToPointSquared;
  410. }
  411. // First check for collision with existing labels
  412. for (i = 0; i < chart.boxesToAvoid.length; i += 1) {
  413. if (intersectRect(chart.boxesToAvoid[i], {
  414. left: x,
  415. right: x + bBox.width,
  416. top: y,
  417. bottom: y + bBox.height
  418. })) {
  419. return false;
  420. }
  421. }
  422. // For each position, check if the lines around the label intersect with any
  423. // of the graphs.
  424. for (i = 0; i < chart.series.length; i += 1) {
  425. series = chart.series[i];
  426. points = series.interpolatedPoints;
  427. if (series.visible && points) {
  428. for (j = 1; j < points.length; j += 1) {
  429. if (
  430. // To avoid processing, only check intersection if the X
  431. // values are close to the box.
  432. points[j].chartX >= x - leastDistance &&
  433. points[j - 1].chartX <= x + bBox.width +
  434. leastDistance
  435. /* @todo condition above is not the same as below
  436. (
  437. (points[j].chartX as any) >=
  438. (x - leastDistance)
  439. ) && (
  440. (points[j - 1].chartX as any) <=
  441. (x + bBox.width + leastDistance)
  442. ) */
  443. ) {
  444. // If any of the box sides intersect with the line, return.
  445. if (boxIntersectLine(x, y, bBox.width, bBox.height, points[j - 1].chartX, points[j - 1].chartY, points[j].chartX, points[j].chartY)) {
  446. return false;
  447. }
  448. // But if it is too far away (a padded box doesn't
  449. // intersect), also return.
  450. if (this === series && !withinRange && checkDistance) {
  451. withinRange = boxIntersectLine(x - leastDistance, y - leastDistance, bBox.width + 2 * leastDistance, bBox.height + 2 * leastDistance, points[j - 1].chartX, points[j - 1].chartY, points[j].chartX, points[j].chartY);
  452. }
  453. }
  454. // Find the squared distance from the center of the label. On
  455. // area series, avoid its own graph.
  456. if ((findDistanceToOthers || withinRange) &&
  457. (this !== series || onArea)) {
  458. xDist = x + bBox.width / 2 - points[j].chartX;
  459. yDist = y + bBox.height / 2 - points[j].chartY;
  460. distToOthersSquared = Math.min(distToOthersSquared, xDist * xDist + yDist * yDist);
  461. }
  462. }
  463. // Do we need a connector?
  464. if (!onArea &&
  465. findDistanceToOthers &&
  466. this === series &&
  467. ((checkDistance && !withinRange) ||
  468. distToOthersSquared < Math.pow(this.options.label.connectorNeighbourDistance, 2))) {
  469. for (j = 1; j < points.length; j += 1) {
  470. dist = Math.min((Math.pow(x + bBox.width / 2 - points[j].chartX, 2) +
  471. Math.pow(y + bBox.height / 2 - points[j].chartY, 2)), (Math.pow(x - points[j].chartX, 2) +
  472. Math.pow(y - points[j].chartY, 2)), (Math.pow(x + bBox.width - points[j].chartX, 2) +
  473. Math.pow(y - points[j].chartY, 2)), (Math.pow(x + bBox.width - points[j].chartX, 2) +
  474. Math.pow(y + bBox.height - points[j].chartY, 2)), (Math.pow(x - points[j].chartX, 2) +
  475. Math.pow(y + bBox.height - points[j].chartY, 2)));
  476. if (dist < distToPointSquared) {
  477. distToPointSquared = dist;
  478. connectorPoint = points[j];
  479. }
  480. }
  481. withinRange = true;
  482. }
  483. }
  484. }
  485. return !checkDistance || withinRange ? {
  486. x: x,
  487. y: y,
  488. weight: getWeight(distToOthersSquared, connectorPoint ? distToPointSquared : 0),
  489. connectorPoint: connectorPoint
  490. } : false;
  491. };
  492. /**
  493. * The main initialize method that runs on chart level after initialization and
  494. * redraw. It runs in a timeout to prevent locking, and loops over all series,
  495. * taking all series and labels into account when placing the labels.
  496. *
  497. * @private
  498. * @function Highcharts.Chart#drawSeriesLabels
  499. */
  500. Chart.prototype.drawSeriesLabels = function () {
  501. // console.time('drawSeriesLabels');
  502. var chart = this, labelSeries = this.labelSeries;
  503. chart.boxesToAvoid = [];
  504. // Build the interpolated points
  505. labelSeries.forEach(function (series) {
  506. series.interpolatedPoints = series.getPointsOnGraph();
  507. (series.options.label.boxesToAvoid || []).forEach(function (box) {
  508. chart.boxesToAvoid.push(box);
  509. });
  510. });
  511. chart.series.forEach(function (series) {
  512. var labelOptions = series.options.label;
  513. if (!labelOptions || (!series.xAxis && !series.yAxis)) {
  514. return;
  515. }
  516. var bBox, x, y, results = [], clearPoint, i, best, inverted = chart.inverted, paneLeft = (inverted ? series.yAxis.pos : series.xAxis.pos), paneTop = (inverted ? series.xAxis.pos : series.yAxis.pos), paneWidth = chart.inverted ? series.yAxis.len : series.xAxis.len, paneHeight = chart.inverted ? series.xAxis.len : series.yAxis.len, points = series.interpolatedPoints, onArea = pick(labelOptions.onArea, !!series.area), label = series.labelBySeries, isNew = !label, minFontSize = labelOptions.minFontSize, maxFontSize = labelOptions.maxFontSize, dataExtremes, areaMin, areaMax, colorClass = 'highcharts-color-' + pick(series.colorIndex, 'none');
  517. // Stay within the area data bounds (#10038)
  518. if (onArea && !inverted) {
  519. dataExtremes = [
  520. series.xAxis.toPixels(series.xData[0]),
  521. series.xAxis.toPixels(series.xData[series.xData.length - 1])
  522. ];
  523. areaMin = Math.min.apply(Math, dataExtremes);
  524. areaMax = Math.max.apply(Math, dataExtremes);
  525. }
  526. /**
  527. * @private
  528. */
  529. function insidePane(x, y, bBox) {
  530. var leftBound = Math.max(paneLeft, pick(areaMin, -Infinity)), rightBound = Math.min(paneLeft + paneWidth, pick(areaMax, Infinity));
  531. return (x > leftBound &&
  532. x <= rightBound - bBox.width &&
  533. y >= paneTop &&
  534. y <= paneTop + paneHeight - bBox.height);
  535. }
  536. /**
  537. * @private
  538. */
  539. function destroyLabel() {
  540. if (label) {
  541. series.labelBySeries = label.destroy();
  542. }
  543. }
  544. if (series.visible && !series.isSeriesBoosting && points) {
  545. if (!label) {
  546. var labelText = series.name;
  547. if (typeof labelOptions.format === 'string') {
  548. labelText = format(labelOptions.format, series, chart);
  549. }
  550. else if (labelOptions.formatter) {
  551. labelText = labelOptions.formatter.call(series);
  552. }
  553. series.labelBySeries = label = chart.renderer
  554. .label(labelText, 0, -9999, 'connector')
  555. .addClass('highcharts-series-label ' +
  556. 'highcharts-series-label-' + series.index + ' ' +
  557. (series.options.className || '') + ' ' +
  558. colorClass);
  559. if (!chart.renderer.styledMode) {
  560. label.css(extend({
  561. color: onArea ?
  562. chart.renderer.getContrast(series.color) :
  563. series.color
  564. }, labelOptions.style || {}));
  565. label.attr({
  566. opacity: chart.renderer.forExport ? 1 : 0,
  567. stroke: series.color,
  568. 'stroke-width': 1
  569. });
  570. }
  571. // Adapt label sizes to the sum of the data
  572. if (minFontSize && maxFontSize) {
  573. label.css({
  574. fontSize: series.labelFontSize(minFontSize, maxFontSize)
  575. });
  576. }
  577. label
  578. .attr({
  579. padding: 0,
  580. zIndex: 3
  581. })
  582. .add();
  583. }
  584. bBox = label.getBBox();
  585. bBox.width = Math.round(bBox.width);
  586. // Ideal positions are centered above or below a point on right side
  587. // of chart
  588. for (i = points.length - 1; i > 0; i -= 1) {
  589. if (onArea) {
  590. // Centered
  591. x = points[i].chartX - bBox.width / 2;
  592. y = points[i].chartCenterY - bBox.height / 2;
  593. if (insidePane(x, y, bBox)) {
  594. best = series.checkClearPoint(x, y, bBox);
  595. }
  596. if (best) {
  597. results.push(best);
  598. }
  599. }
  600. else {
  601. // Right - up
  602. x = points[i].chartX + labelDistance;
  603. y = points[i].chartY - bBox.height - labelDistance;
  604. if (insidePane(x, y, bBox)) {
  605. best = series.checkClearPoint(x, y, bBox, true);
  606. }
  607. if (best) {
  608. results.push(best);
  609. }
  610. // Right - down
  611. x = points[i].chartX + labelDistance;
  612. y = points[i].chartY + labelDistance;
  613. if (insidePane(x, y, bBox)) {
  614. best = series.checkClearPoint(x, y, bBox, true);
  615. }
  616. if (best) {
  617. results.push(best);
  618. }
  619. // Left - down
  620. x = points[i].chartX - bBox.width - labelDistance;
  621. y = points[i].chartY + labelDistance;
  622. if (insidePane(x, y, bBox)) {
  623. best = series.checkClearPoint(x, y, bBox, true);
  624. }
  625. if (best) {
  626. results.push(best);
  627. }
  628. // Left - up
  629. x = points[i].chartX - bBox.width - labelDistance;
  630. y = points[i].chartY - bBox.height - labelDistance;
  631. if (insidePane(x, y, bBox)) {
  632. best = series.checkClearPoint(x, y, bBox, true);
  633. }
  634. if (best) {
  635. results.push(best);
  636. }
  637. }
  638. }
  639. // Brute force, try all positions on the chart in a 16x16 grid
  640. if (labelOptions.connectorAllowed && !results.length && !onArea) {
  641. for (x = paneLeft + paneWidth - bBox.width; x >= paneLeft; x -= 16) {
  642. for (y = paneTop; y < paneTop + paneHeight - bBox.height; y += 16) {
  643. clearPoint = series.checkClearPoint(x, y, bBox, true);
  644. if (clearPoint) {
  645. results.push(clearPoint);
  646. }
  647. }
  648. }
  649. }
  650. if (results.length) {
  651. results.sort(function (a, b) {
  652. return b.weight - a.weight;
  653. });
  654. best = results[0];
  655. chart.boxesToAvoid.push({
  656. left: best.x,
  657. right: best.x + bBox.width,
  658. top: best.y,
  659. bottom: best.y + bBox.height
  660. });
  661. // Move it if needed
  662. var dist = Math.sqrt(Math.pow(Math.abs(best.x - (label.x || 0)), 2) +
  663. Math.pow(Math.abs(best.y - (label.y || 0)), 2));
  664. if (dist && series.labelBySeries) {
  665. // Move fast and fade in - pure animation movement is
  666. // distractive...
  667. var attr = {
  668. opacity: chart.renderer.forExport ? 1 : 0,
  669. x: best.x,
  670. y: best.y
  671. }, anim = {
  672. opacity: 1
  673. };
  674. // ... unless we're just moving a short distance
  675. if (dist <= 10) {
  676. anim = {
  677. x: attr.x,
  678. y: attr.y
  679. };
  680. attr = {};
  681. }
  682. // Default initial animation to a fraction of the series
  683. // animation (#9396)
  684. var animationOptions = void 0;
  685. if (isNew) {
  686. animationOptions = animObject(series.options.animation);
  687. // @todo: Safely remove any cast after merging #13005
  688. animationOptions.duration *= 0.2;
  689. }
  690. series.labelBySeries
  691. .attr(extend(attr, {
  692. anchorX: best.connectorPoint &&
  693. best.connectorPoint.plotX + paneLeft,
  694. anchorY: best.connectorPoint &&
  695. best.connectorPoint.plotY + paneTop
  696. }))
  697. .animate(anim, animationOptions);
  698. // Record closest point to stick to for sync redraw
  699. series.options.kdNow = true;
  700. series.buildKDTree();
  701. var closest = series.searchPoint({
  702. chartX: best.x,
  703. chartY: best.y
  704. }, true);
  705. if (closest) {
  706. label.closest = [
  707. closest,
  708. best.x - (closest.plotX || 0),
  709. best.y - (closest.plotY || 0)
  710. ];
  711. }
  712. }
  713. }
  714. else {
  715. destroyLabel();
  716. }
  717. }
  718. else {
  719. destroyLabel();
  720. }
  721. });
  722. fireEvent(chart, 'afterDrawSeriesLabels');
  723. // console.timeEnd('drawSeriesLabels');
  724. };
  725. /* eslint-disable no-invalid-this */
  726. /**
  727. * Prepare drawing series labels.
  728. *
  729. * @private
  730. * @function drawLabels
  731. */
  732. function drawLabels(e) {
  733. if (this.renderer) {
  734. var chart = this, delay = animObject(chart.renderer.globalAnimation).duration;
  735. chart.labelSeries = [];
  736. chart.labelSeriesMaxSum = 0;
  737. U.clearTimeout(chart.seriesLabelTimer);
  738. // Which series should have labels
  739. chart.series.forEach(function (series) {
  740. var options = series.options.label, label = series.labelBySeries, closest = label && label.closest;
  741. if (options.enabled &&
  742. series.visible &&
  743. (series.graph || series.area) &&
  744. !series.isSeriesBoosting) {
  745. chart.labelSeries.push(series);
  746. if (options.minFontSize && options.maxFontSize) {
  747. series.sum = series.yData.reduce(function (pv, cv) {
  748. return (pv || 0) + (cv || 0);
  749. }, 0);
  750. chart.labelSeriesMaxSum = Math.max(chart.labelSeriesMaxSum, series.sum);
  751. }
  752. // The labels are processing heavy, wait until the animation is
  753. // done
  754. if (e.type === 'load') {
  755. delay = Math.max(delay, animObject(series.options.animation).duration);
  756. }
  757. // Keep the position updated to the axis while redrawing
  758. if (closest) {
  759. if (typeof closest[0].plotX !== 'undefined') {
  760. label.animate({
  761. x: closest[0].plotX + closest[1],
  762. y: closest[0].plotY + closest[2]
  763. });
  764. }
  765. else {
  766. label.attr({ opacity: 0 });
  767. }
  768. }
  769. }
  770. });
  771. chart.seriesLabelTimer = syncTimeout(function () {
  772. if (chart.series && chart.labelSeries) { // #7931, chart destroyed
  773. chart.drawSeriesLabels();
  774. }
  775. }, chart.renderer.forExport || !delay ? 0 : delay);
  776. }
  777. }
  778. // Leave both events, we handle animation differently (#9815)
  779. addEvent(Chart, 'load', drawLabels);
  780. addEvent(Chart, 'redraw', drawLabels);
  781. });
  782. _registerModule(_modules, 'masters/modules/series-label.src.js', [], function () {
  783. });
  784. }));