series-label.src.js 29 KB

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