series-label.src.js 31 KB

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