SeriesLabel.js 31 KB

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