SeriesLabel.js 31 KB

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