series-label.src.js 29 KB

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