series-label.src.js 35 KB

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