series-label.src.js 33 KB

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