series-label.src.js 39 KB

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