SeriesLabel.js 31 KB

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