DataLabels.js 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431
  1. /**
  2. * (c) 2010-2019 Torstein Honsi
  3. *
  4. * License: www.highcharts.com/license
  5. */
  6. 'use strict';
  7. import H from './Globals.js';
  8. import './Utilities.js';
  9. import './Series.js';
  10. var addEvent = H.addEvent,
  11. arrayMax = H.arrayMax,
  12. defined = H.defined,
  13. extend = H.extend,
  14. format = H.format,
  15. merge = H.merge,
  16. noop = H.noop,
  17. pick = H.pick,
  18. relativeLength = H.relativeLength,
  19. Series = H.Series,
  20. seriesTypes = H.seriesTypes,
  21. stableSort = H.stableSort,
  22. isArray = H.isArray,
  23. splat = H.splat;
  24. /**
  25. * General distribution algorithm for distributing labels of differing size
  26. * along a confined length in two dimensions. The algorithm takes an array of
  27. * objects containing a size, a target and a rank. It will place the labels as
  28. * close as possible to their targets, skipping the lowest ranked labels if
  29. * necessary.
  30. *
  31. * @private
  32. * @function Highcharts.distribute
  33. *
  34. * @param {Array<object>} boxes
  35. *
  36. * @param {number} len
  37. *
  38. * @param {number} maxDistance
  39. */
  40. H.distribute = function (boxes, len, maxDistance) {
  41. var i,
  42. overlapping = true,
  43. origBoxes = boxes, // Original array will be altered with added .pos
  44. restBoxes = [], // The outranked overshoot
  45. box,
  46. target,
  47. total = 0,
  48. reducedLen = origBoxes.reducedLen || len;
  49. function sortByTarget(a, b) {
  50. return a.target - b.target;
  51. }
  52. // If the total size exceeds the len, remove those boxes with the lowest
  53. // rank
  54. i = boxes.length;
  55. while (i--) {
  56. total += boxes[i].size;
  57. }
  58. // Sort by rank, then slice away overshoot
  59. if (total > reducedLen) {
  60. stableSort(boxes, function (a, b) {
  61. return (b.rank || 0) - (a.rank || 0);
  62. });
  63. i = 0;
  64. total = 0;
  65. while (total <= reducedLen) {
  66. total += boxes[i].size;
  67. i++;
  68. }
  69. restBoxes = boxes.splice(i - 1, boxes.length);
  70. }
  71. // Order by target
  72. stableSort(boxes, sortByTarget);
  73. // So far we have been mutating the original array. Now
  74. // create a copy with target arrays
  75. boxes = boxes.map(function (box) {
  76. return {
  77. size: box.size,
  78. targets: [box.target],
  79. align: pick(box.align, 0.5)
  80. };
  81. });
  82. while (overlapping) {
  83. // Initial positions: target centered in box
  84. i = boxes.length;
  85. while (i--) {
  86. box = boxes[i];
  87. // Composite box, average of targets
  88. target = (
  89. Math.min.apply(0, box.targets) +
  90. Math.max.apply(0, box.targets)
  91. ) / 2;
  92. box.pos = Math.min(
  93. Math.max(0, target - box.size * box.align),
  94. len - box.size
  95. );
  96. }
  97. // Detect overlap and join boxes
  98. i = boxes.length;
  99. overlapping = false;
  100. while (i--) {
  101. // Overlap
  102. if (i > 0 && boxes[i - 1].pos + boxes[i - 1].size > boxes[i].pos) {
  103. // Add this size to the previous box
  104. boxes[i - 1].size += boxes[i].size;
  105. boxes[i - 1].targets = boxes[i - 1]
  106. .targets
  107. .concat(boxes[i].targets);
  108. boxes[i - 1].align = 0.5;
  109. // Overlapping right, push left
  110. if (boxes[i - 1].pos + boxes[i - 1].size > len) {
  111. boxes[i - 1].pos = len - boxes[i - 1].size;
  112. }
  113. boxes.splice(i, 1); // Remove this item
  114. overlapping = true;
  115. }
  116. }
  117. }
  118. // Add the rest (hidden boxes)
  119. origBoxes.push.apply(origBoxes, restBoxes);
  120. // Now the composite boxes are placed, we need to put the original boxes
  121. // within them
  122. i = 0;
  123. boxes.some(function (box) {
  124. var posInCompositeBox = 0;
  125. if (box.targets.some(function () {
  126. origBoxes[i].pos = box.pos + posInCompositeBox;
  127. // If the distance between the position and the target exceeds
  128. // maxDistance, abort the loop and decrease the length in increments
  129. // of 10% to recursively reduce the number of visible boxes by
  130. // rank. Once all boxes are within the maxDistance, we're good.
  131. if (
  132. Math.abs(origBoxes[i].pos - origBoxes[i].target) >
  133. maxDistance
  134. ) {
  135. // Reset the positions that are already set
  136. origBoxes.slice(0, i + 1).forEach(function (box) {
  137. delete box.pos;
  138. });
  139. // Try with a smaller length
  140. origBoxes.reducedLen =
  141. (origBoxes.reducedLen || len) - (len * 0.1);
  142. // Recurse
  143. if (origBoxes.reducedLen > len * 0.1) {
  144. H.distribute(origBoxes, len, maxDistance);
  145. }
  146. // Exceeded maxDistance => abort
  147. return true;
  148. }
  149. posInCompositeBox += origBoxes[i].size;
  150. i++;
  151. })) {
  152. // Exceeded maxDistance => abort
  153. return true;
  154. }
  155. });
  156. // Add the rest (hidden) boxes and sort by target
  157. stableSort(origBoxes, sortByTarget);
  158. };
  159. /**
  160. * Draw the data labels
  161. *
  162. * @private
  163. * @function Highcharts.Series#drawDataLabels
  164. *
  165. * @fires Highcharts.Series#event:afterDrawDataLabels
  166. */
  167. Series.prototype.drawDataLabels = function () {
  168. var series = this,
  169. chart = series.chart,
  170. seriesOptions = series.options,
  171. seriesDlOptions = seriesOptions.dataLabels,
  172. points = series.points,
  173. pointOptions,
  174. hasRendered = series.hasRendered || 0,
  175. dataLabelsGroup,
  176. defer = pick(seriesDlOptions.defer, !!seriesOptions.animation),
  177. renderer = chart.renderer;
  178. /*
  179. * Handle the dataLabels.filter option.
  180. */
  181. function applyFilter(point, options) {
  182. var filter = options.filter,
  183. op,
  184. prop,
  185. val;
  186. if (filter) {
  187. op = filter.operator;
  188. prop = point[filter.property];
  189. val = filter.value;
  190. if (
  191. (op === '>' && prop > val) ||
  192. (op === '<' && prop < val) ||
  193. (op === '>=' && prop >= val) ||
  194. (op === '<=' && prop <= val) ||
  195. (op === '==' && prop == val) || // eslint-disable-line eqeqeq
  196. (op === '===' && prop === val)
  197. ) {
  198. return true;
  199. }
  200. return false;
  201. }
  202. return true;
  203. }
  204. /*
  205. * Merge two objects that can be arrays. If one of them is an array, the
  206. * other is merged into each element. If both are arrays, each element is
  207. * merged by index. If neither are arrays, we use normal merge.
  208. */
  209. function mergeArrays(one, two) {
  210. var res = [],
  211. i;
  212. if (isArray(one) && !isArray(two)) {
  213. res = one.map(function (el) {
  214. return merge(el, two);
  215. });
  216. } else if (isArray(two) && !isArray(one)) {
  217. res = two.map(function (el) {
  218. return merge(one, el);
  219. });
  220. } else if (!isArray(one) && !isArray(two)) {
  221. res = merge(one, two);
  222. } else {
  223. i = Math.max(one.length, two.length);
  224. while (i--) {
  225. res[i] = merge(one[i], two[i]);
  226. }
  227. }
  228. return res;
  229. }
  230. // Merge in plotOptions.dataLabels for series
  231. seriesDlOptions = mergeArrays(
  232. mergeArrays(
  233. chart.options.plotOptions &&
  234. chart.options.plotOptions.series &&
  235. chart.options.plotOptions.series.dataLabels,
  236. chart.options.plotOptions &&
  237. chart.options.plotOptions[series.type] &&
  238. chart.options.plotOptions[series.type].dataLabels
  239. ),
  240. seriesDlOptions
  241. );
  242. H.fireEvent(this, 'drawDataLabels');
  243. if (
  244. isArray(seriesDlOptions) ||
  245. seriesDlOptions.enabled ||
  246. series._hasPointLabels
  247. ) {
  248. // Create a separate group for the data labels to avoid rotation
  249. dataLabelsGroup = series.plotGroup(
  250. 'dataLabelsGroup',
  251. 'data-labels',
  252. defer && !hasRendered ? 'hidden' : 'visible', // #5133
  253. seriesDlOptions.zIndex || 6
  254. );
  255. if (defer) {
  256. dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300
  257. if (!hasRendered) {
  258. addEvent(series, 'afterAnimate', function () {
  259. if (series.visible) { // #2597, #3023, #3024
  260. dataLabelsGroup.show(true);
  261. }
  262. dataLabelsGroup[
  263. seriesOptions.animation ? 'animate' : 'attr'
  264. ]({ opacity: 1 }, { duration: 200 });
  265. });
  266. }
  267. }
  268. // Make the labels for each point
  269. points.forEach(function (point) {
  270. // Merge in series options for the point.
  271. // @note dataLabelAttribs (like pointAttribs) would eradicate
  272. // the need for dlOptions, and simplify the section below.
  273. pointOptions = splat(
  274. mergeArrays(
  275. seriesDlOptions,
  276. point.dlOptions || // dlOptions is used in treemaps
  277. (point.options && point.options.dataLabels)
  278. )
  279. );
  280. // Handle each individual data label for this point
  281. pointOptions.forEach(function (labelOptions, i) {
  282. // Options for one datalabel
  283. var labelEnabled = labelOptions.enabled &&
  284. !point.isNull && // #2282, #4641, #7112
  285. applyFilter(point, labelOptions),
  286. labelConfig,
  287. formatString,
  288. labelText,
  289. style,
  290. rotation,
  291. attr,
  292. dataLabel = point.dataLabels ? point.dataLabels[i] :
  293. point.dataLabel,
  294. connector = point.connectors ? point.connectors[i] :
  295. point.connector,
  296. isNew = !dataLabel;
  297. if (labelEnabled) {
  298. // Create individual options structure that can be extended
  299. // without affecting others
  300. labelConfig = point.getLabelConfig();
  301. formatString = (
  302. labelOptions[point.formatPrefix + 'Format'] ||
  303. labelOptions.format
  304. );
  305. labelText = defined(formatString) ?
  306. format(formatString, labelConfig, chart.time) :
  307. (
  308. labelOptions[point.formatPrefix + 'Formatter'] ||
  309. labelOptions.formatter
  310. ).call(labelConfig, labelOptions);
  311. style = labelOptions.style;
  312. rotation = labelOptions.rotation;
  313. if (!chart.styledMode) {
  314. // Determine the color
  315. style.color = pick(
  316. labelOptions.color,
  317. style.color,
  318. series.color,
  319. '#000000'
  320. );
  321. // Get automated contrast color
  322. if (style.color === 'contrast') {
  323. point.contrastColor = renderer.getContrast(
  324. point.color || series.color
  325. );
  326. style.color = labelOptions.inside ||
  327. pick(
  328. labelOptions.distance,
  329. point.labelDistance
  330. ) < 0 ||
  331. !!seriesOptions.stacking ?
  332. point.contrastColor :
  333. '#000000';
  334. }
  335. if (seriesOptions.cursor) {
  336. style.cursor = seriesOptions.cursor;
  337. }
  338. }
  339. attr = {
  340. r: labelOptions.borderRadius || 0,
  341. rotation: rotation,
  342. padding: labelOptions.padding,
  343. zIndex: 1
  344. };
  345. if (!chart.styledMode) {
  346. attr.fill = labelOptions.backgroundColor;
  347. attr.stroke = labelOptions.borderColor;
  348. attr['stroke-width'] = labelOptions.borderWidth;
  349. }
  350. // Remove unused attributes (#947)
  351. H.objectEach(attr, function (val, name) {
  352. if (val === undefined) {
  353. delete attr[name];
  354. }
  355. });
  356. }
  357. // If the point is outside the plot area, destroy it. #678, #820
  358. if (dataLabel && (!labelEnabled || !defined(labelText))) {
  359. point.dataLabel =
  360. point.dataLabel && point.dataLabel.destroy();
  361. if (point.dataLabels) {
  362. // Remove point.dataLabels if this was the last one
  363. if (point.dataLabels.length === 1) {
  364. delete point.dataLabels;
  365. } else {
  366. delete point.dataLabels[i];
  367. }
  368. }
  369. if (!i) {
  370. delete point.dataLabel;
  371. }
  372. if (connector) {
  373. point.connector = point.connector.destroy();
  374. if (point.connectors) {
  375. // Remove point.connectors if this was the last one
  376. if (point.connectors.length === 1) {
  377. delete point.connectors;
  378. } else {
  379. delete point.connectors[i];
  380. }
  381. }
  382. }
  383. // Individual labels are disabled if the are explicitly disabled
  384. // in the point options, or if they fall outside the plot area.
  385. } else if (labelEnabled && defined(labelText)) {
  386. if (!dataLabel) {
  387. // Create new label element
  388. point.dataLabels = point.dataLabels || [];
  389. dataLabel = point.dataLabels[i] = rotation ?
  390. // Labels don't rotate, use text element
  391. renderer.text(labelText, 0, -9999)
  392. .addClass('highcharts-data-label') :
  393. // We can use label
  394. renderer.label(
  395. labelText,
  396. 0,
  397. -9999,
  398. labelOptions.shape,
  399. null,
  400. null,
  401. labelOptions.useHTML,
  402. null,
  403. 'data-label'
  404. );
  405. // Store for backwards compatibility
  406. if (!i) {
  407. point.dataLabel = dataLabel;
  408. }
  409. dataLabel.addClass(
  410. ' highcharts-data-label-color-' + point.colorIndex +
  411. ' ' + (labelOptions.className || '') +
  412. ( // #3398
  413. labelOptions.useHTML ?
  414. ' highcharts-tracker' :
  415. ''
  416. )
  417. );
  418. } else {
  419. // Use old element and just update text
  420. attr.text = labelText;
  421. }
  422. // Store data label options for later access
  423. dataLabel.options = labelOptions;
  424. dataLabel.attr(attr);
  425. if (!chart.styledMode) {
  426. // Styles must be applied before add in order to read
  427. // text bounding box
  428. dataLabel.css(style).shadow(labelOptions.shadow);
  429. }
  430. if (!dataLabel.added) {
  431. dataLabel.add(dataLabelsGroup);
  432. }
  433. // Now the data label is created and placed at 0,0, so we
  434. // need to align it
  435. series.alignDataLabel(
  436. point, dataLabel, labelOptions, null, isNew
  437. );
  438. }
  439. });
  440. });
  441. }
  442. H.fireEvent(this, 'afterDrawDataLabels');
  443. };
  444. /**
  445. * Align each individual data label.
  446. *
  447. * @private
  448. * @function Highcharts.Series#alignDataLabel
  449. *
  450. * @param {Highcharts.Point} point
  451. *
  452. * @param {Highcharts.SVGElement} dataLabel
  453. *
  454. * @param {Highcharts.PlotSeriesDataLabelsOptions} options
  455. *
  456. * @param {Highcharts.BBoxObject} alignTo
  457. *
  458. * @param {boolean} isNew
  459. */
  460. Series.prototype.alignDataLabel = function (
  461. point,
  462. dataLabel,
  463. options,
  464. alignTo,
  465. isNew
  466. ) {
  467. var chart = this.chart,
  468. inverted = this.isCartesian && chart.inverted,
  469. plotX = pick(point.dlBox && point.dlBox.centerX, point.plotX, -9999),
  470. plotY = pick(point.plotY, -9999),
  471. bBox = dataLabel.getBBox(),
  472. baseline,
  473. rotation = options.rotation,
  474. normRotation,
  475. negRotation,
  476. align = options.align,
  477. rotCorr, // rotation correction
  478. // Math.round for rounding errors (#2683), alignTo to allow column
  479. // labels (#2700)
  480. visible =
  481. this.visible &&
  482. (
  483. point.series.forceDL ||
  484. chart.isInsidePlot(plotX, Math.round(plotY), inverted) ||
  485. (
  486. alignTo && chart.isInsidePlot(
  487. plotX,
  488. inverted ?
  489. alignTo.x + 1 :
  490. alignTo.y + alignTo.height - 1,
  491. inverted
  492. )
  493. )
  494. ),
  495. alignAttr, // the final position;
  496. justify = pick(options.overflow, 'justify') === 'justify';
  497. if (visible) {
  498. baseline = chart.renderer.fontMetrics(
  499. chart.styledMode ? undefined : options.style.fontSize,
  500. dataLabel
  501. ).b;
  502. // The alignment box is a singular point
  503. alignTo = extend({
  504. x: inverted ? this.yAxis.len - plotY : plotX,
  505. y: Math.round(inverted ? this.xAxis.len - plotX : plotY),
  506. width: 0,
  507. height: 0
  508. }, alignTo);
  509. // Add the text size for alignment calculation
  510. extend(options, {
  511. width: bBox.width,
  512. height: bBox.height
  513. });
  514. // Allow a hook for changing alignment in the last moment, then do the
  515. // alignment
  516. if (rotation) {
  517. justify = false; // Not supported for rotated text
  518. rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723
  519. alignAttr = {
  520. x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
  521. y: (
  522. alignTo.y +
  523. options.y +
  524. { top: 0, middle: 0.5, bottom: 1 }[options.verticalAlign] *
  525. alignTo.height
  526. )
  527. };
  528. dataLabel[isNew ? 'attr' : 'animate'](alignAttr)
  529. .attr({ // #3003
  530. align: align
  531. });
  532. // Compensate for the rotated label sticking out on the sides
  533. normRotation = (rotation + 720) % 360;
  534. negRotation = normRotation > 180 && normRotation < 360;
  535. if (align === 'left') {
  536. alignAttr.y -= negRotation ? bBox.height : 0;
  537. } else if (align === 'center') {
  538. alignAttr.x -= bBox.width / 2;
  539. alignAttr.y -= bBox.height / 2;
  540. } else if (align === 'right') {
  541. alignAttr.x -= bBox.width;
  542. alignAttr.y -= negRotation ? 0 : bBox.height;
  543. }
  544. dataLabel.placed = true;
  545. dataLabel.alignAttr = alignAttr;
  546. } else {
  547. dataLabel.align(options, null, alignTo);
  548. alignAttr = dataLabel.alignAttr;
  549. }
  550. // Handle justify or crop
  551. if (justify && alignTo.height >= 0) { // #8830
  552. point.isLabelJustified = this.justifyDataLabel(
  553. dataLabel,
  554. options,
  555. alignAttr,
  556. bBox,
  557. alignTo,
  558. isNew
  559. );
  560. // Now check that the data label is within the plot area
  561. } else if (pick(options.crop, true)) {
  562. visible =
  563. chart.isInsidePlot(
  564. alignAttr.x,
  565. alignAttr.y
  566. ) &&
  567. chart.isInsidePlot(
  568. alignAttr.x + bBox.width,
  569. alignAttr.y + bBox.height
  570. );
  571. }
  572. // When we're using a shape, make it possible with a connector or an
  573. // arrow pointing to thie point
  574. if (options.shape && !rotation) {
  575. dataLabel[isNew ? 'attr' : 'animate']({
  576. anchorX: inverted ? chart.plotWidth - point.plotY : point.plotX,
  577. anchorY: inverted ? chart.plotHeight - point.plotX : point.plotY
  578. });
  579. }
  580. }
  581. // Show or hide based on the final aligned position
  582. if (!visible) {
  583. dataLabel.attr({ y: -9999 });
  584. dataLabel.placed = false; // don't animate back in
  585. }
  586. };
  587. /**
  588. * If data labels fall partly outside the plot area, align them back in, in a
  589. * way that doesn't hide the point.
  590. *
  591. * @private
  592. * @function Highcharts.Series#justifyDataLabel
  593. *
  594. * @param {Highcharts.SVGElement} dataLabel
  595. *
  596. * @param {Highcharts.PlotSeriesDataLabelsOptions} options
  597. *
  598. * @param {*} alignAttr
  599. *
  600. * @param {Highcharts.BBoxObject} bBox
  601. *
  602. * @param {boolean} isNew
  603. *
  604. * @return {boolean}
  605. */
  606. Series.prototype.justifyDataLabel = function (
  607. dataLabel,
  608. options,
  609. alignAttr,
  610. bBox,
  611. alignTo,
  612. isNew
  613. ) {
  614. var chart = this.chart,
  615. align = options.align,
  616. verticalAlign = options.verticalAlign,
  617. off,
  618. justified,
  619. padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
  620. // Off left
  621. off = alignAttr.x + padding;
  622. if (off < 0) {
  623. if (align === 'right') {
  624. options.align = 'left';
  625. } else {
  626. options.x = -off;
  627. }
  628. justified = true;
  629. }
  630. // Off right
  631. off = alignAttr.x + bBox.width - padding;
  632. if (off > chart.plotWidth) {
  633. if (align === 'left') {
  634. options.align = 'right';
  635. } else {
  636. options.x = chart.plotWidth - off;
  637. }
  638. justified = true;
  639. }
  640. // Off top
  641. off = alignAttr.y + padding;
  642. if (off < 0) {
  643. if (verticalAlign === 'bottom') {
  644. options.verticalAlign = 'top';
  645. } else {
  646. options.y = -off;
  647. }
  648. justified = true;
  649. }
  650. // Off bottom
  651. off = alignAttr.y + bBox.height - padding;
  652. if (off > chart.plotHeight) {
  653. if (verticalAlign === 'top') {
  654. options.verticalAlign = 'bottom';
  655. } else {
  656. options.y = chart.plotHeight - off;
  657. }
  658. justified = true;
  659. }
  660. if (justified) {
  661. dataLabel.placed = !isNew;
  662. dataLabel.align(options, null, alignTo);
  663. }
  664. return justified;
  665. };
  666. if (seriesTypes.pie) {
  667. seriesTypes.pie.prototype.dataLabelPositioners = {
  668. // Based on the value computed in Highcharts' distribute algorithm.
  669. radialDistributionY: function (point) {
  670. return point.top + point.distributeBox.pos;
  671. },
  672. // get the x - use the natural x position for labels near the
  673. // top and bottom, to prevent the top and botton slice
  674. // connectors from touching each other on either side
  675. // Based on the value computed in Highcharts' distribute algorithm.
  676. radialDistributionX: function (series, point, y, naturalY) {
  677. return series.getX(
  678. y < point.top + 2 || y > point.bottom - 2 ?
  679. naturalY :
  680. y,
  681. point.half,
  682. point
  683. );
  684. },
  685. // dataLabels.distance determines the x position of the label
  686. justify: function (point, radius, seriesCenter) {
  687. return seriesCenter[0] + (point.half ? -1 : 1) *
  688. (radius + point.labelDistance);
  689. },
  690. // Left edges of the left-half labels touch the left edge of the plot
  691. // area. Right edges of the right-half labels touch the right edge of
  692. // the plot area.
  693. alignToPlotEdges: function (
  694. dataLabel,
  695. half,
  696. plotWidth,
  697. plotLeft
  698. ) {
  699. var dataLabelWidth = dataLabel.getBBox().width;
  700. return half ? dataLabelWidth + plotLeft :
  701. plotWidth - dataLabelWidth - plotLeft;
  702. },
  703. // Connectors of each side end in the same x position. Labels are
  704. // aligned to them. Left edge of the widest left-half label touches the
  705. // left edge of the plot area. Right edge of the widest right-half label
  706. // touches the right edge of the plot area.
  707. alignToConnectors: function (
  708. points,
  709. half,
  710. plotWidth,
  711. plotLeft
  712. ) {
  713. var maxDataLabelWidth = 0,
  714. dataLabelWidth;
  715. // find widest data label
  716. points.forEach(function (point) {
  717. dataLabelWidth = point.dataLabel.getBBox().width;
  718. if (dataLabelWidth > maxDataLabelWidth) {
  719. maxDataLabelWidth = dataLabelWidth;
  720. }
  721. });
  722. return half ? maxDataLabelWidth + plotLeft :
  723. plotWidth - maxDataLabelWidth - plotLeft;
  724. }
  725. };
  726. /**
  727. * Override the base drawDataLabels method by pie specific functionality
  728. *
  729. * @private
  730. * @function Highcharts.seriesTypes.pie#drawDataLabels
  731. */
  732. seriesTypes.pie.prototype.drawDataLabels = function () {
  733. var series = this,
  734. data = series.data,
  735. point,
  736. chart = series.chart,
  737. options = series.options.dataLabels,
  738. connectorPadding = options.connectorPadding,
  739. connectorWidth = pick(options.connectorWidth, 1),
  740. plotWidth = chart.plotWidth,
  741. plotHeight = chart.plotHeight,
  742. plotLeft = chart.plotLeft,
  743. maxWidth = Math.round(chart.chartWidth / 3),
  744. connector,
  745. seriesCenter = series.center,
  746. radius = seriesCenter[2] / 2,
  747. centerY = seriesCenter[1],
  748. dataLabel,
  749. dataLabelWidth,
  750. // labelPos,
  751. labelPosition,
  752. labelHeight,
  753. // divide the points into right and left halves for anti collision
  754. halves = [
  755. [], // right
  756. [] // left
  757. ],
  758. x,
  759. y,
  760. visibility,
  761. j,
  762. overflow = [0, 0, 0, 0], // top, right, bottom, left
  763. dataLabelPositioners = series.dataLabelPositioners;
  764. // get out if not enabled
  765. if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
  766. return;
  767. }
  768. // Reset all labels that have been shortened
  769. data.forEach(function (point) {
  770. if (point.dataLabel && point.visible && point.dataLabel.shortened) {
  771. point.dataLabel
  772. .attr({
  773. width: 'auto'
  774. }).css({
  775. width: 'auto',
  776. textOverflow: 'clip'
  777. });
  778. point.dataLabel.shortened = false;
  779. }
  780. });
  781. // run parent method
  782. Series.prototype.drawDataLabels.apply(series);
  783. data.forEach(function (point) {
  784. if (point.dataLabel) {
  785. if (point.visible) { // #407, #2510
  786. // Arrange points for detection collision
  787. halves[point.half].push(point);
  788. // Reset positions (#4905)
  789. point.dataLabel._pos = null;
  790. // Avoid long labels squeezing the pie size too far down
  791. if (
  792. !defined(options.style.width) &&
  793. !defined(
  794. point.options.dataLabels &&
  795. point.options.dataLabels.style &&
  796. point.options.dataLabels.style.width
  797. )
  798. ) {
  799. if (point.dataLabel.getBBox().width > maxWidth) {
  800. point.dataLabel.css({
  801. // Use a fraction of the maxWidth to avoid
  802. // wrapping close to the end of the string.
  803. width: maxWidth * 0.7
  804. });
  805. point.dataLabel.shortened = true;
  806. }
  807. }
  808. } else {
  809. point.dataLabel = point.dataLabel.destroy();
  810. // Workaround to make pies destroy multiple datalabels
  811. // correctly. This logic needs rewriting to support multiple
  812. // datalabels fully.
  813. if (point.dataLabels && point.dataLabels.length === 1) {
  814. delete point.dataLabels;
  815. }
  816. }
  817. }
  818. });
  819. /* Loop over the points in each half, starting from the top and bottom
  820. * of the pie to detect overlapping labels.
  821. */
  822. halves.forEach(function (points, i) {
  823. var top,
  824. bottom,
  825. length = points.length,
  826. positions = [],
  827. naturalY,
  828. sideOverflow,
  829. size,
  830. distributionLength;
  831. if (!length) {
  832. return;
  833. }
  834. // Sort by angle
  835. series.sortByAngle(points, i - 0.5);
  836. // Only do anti-collision when we have dataLabels outside the pie
  837. // and have connectors. (#856)
  838. if (series.maxLabelDistance > 0) {
  839. top = Math.max(
  840. 0,
  841. centerY - radius - series.maxLabelDistance
  842. );
  843. bottom = Math.min(
  844. centerY + radius + series.maxLabelDistance,
  845. chart.plotHeight
  846. );
  847. points.forEach(function (point) {
  848. // check if specific points' label is outside the pie
  849. if (point.labelDistance > 0 && point.dataLabel) {
  850. // point.top depends on point.labelDistance value
  851. // Used for calculation of y value in getX method
  852. point.top = Math.max(
  853. 0,
  854. centerY - radius - point.labelDistance
  855. );
  856. point.bottom = Math.min(
  857. centerY + radius + point.labelDistance,
  858. chart.plotHeight
  859. );
  860. size = point.dataLabel.getBBox().height || 21;
  861. // point.positionsIndex is needed for getting index of
  862. // parameter related to specific point inside positions
  863. // array - not every point is in positions array.
  864. point.distributeBox = {
  865. target: point.labelPosition.natural.y -
  866. point.top + size / 2,
  867. size: size,
  868. rank: point.y
  869. };
  870. positions.push(point.distributeBox);
  871. }
  872. });
  873. distributionLength = bottom + size - top;
  874. H.distribute(
  875. positions,
  876. distributionLength,
  877. distributionLength / 5
  878. );
  879. }
  880. // Now the used slots are sorted, fill them up sequentially
  881. for (j = 0; j < length; j++) {
  882. point = points[j];
  883. // labelPos = point.labelPos;
  884. labelPosition = point.labelPosition;
  885. dataLabel = point.dataLabel;
  886. visibility = point.visible === false ? 'hidden' : 'inherit';
  887. naturalY = labelPosition.natural.y;
  888. y = naturalY;
  889. if (positions && defined(point.distributeBox)) {
  890. if (point.distributeBox.pos === undefined) {
  891. visibility = 'hidden';
  892. } else {
  893. labelHeight = point.distributeBox.size;
  894. // Find label's y position
  895. y = dataLabelPositioners.radialDistributionY(point);
  896. }
  897. }
  898. // It is needed to delete point.positionIndex for
  899. // dynamically added points etc.
  900. delete point.positionIndex;
  901. // Find label's x position
  902. // justify is undocumented in the API - preserve support for it
  903. if (options.justify) {
  904. x = dataLabelPositioners.justify(point, radius,
  905. seriesCenter);
  906. } else {
  907. switch (options.alignTo) {
  908. case 'connectors':
  909. x = dataLabelPositioners.alignToConnectors(points,
  910. i, plotWidth, plotLeft);
  911. break;
  912. case 'plotEdges':
  913. x = dataLabelPositioners.alignToPlotEdges(dataLabel,
  914. i, plotWidth, plotLeft);
  915. break;
  916. default:
  917. x = dataLabelPositioners.radialDistributionX(series,
  918. point, y, naturalY);
  919. }
  920. }
  921. // Record the placement and visibility
  922. dataLabel._attr = {
  923. visibility: visibility,
  924. align: labelPosition.alignment
  925. };
  926. dataLabel._pos = {
  927. x: (
  928. x +
  929. options.x +
  930. ({
  931. left: connectorPadding,
  932. right: -connectorPadding
  933. }[labelPosition.alignment] || 0)
  934. ),
  935. // 10 is for the baseline (label vs text)
  936. y: y + options.y - 10
  937. };
  938. // labelPos.x = x;
  939. // labelPos.y = y;
  940. labelPosition.final.x = x;
  941. labelPosition.final.y = y;
  942. // Detect overflowing data labels
  943. if (pick(options.crop, true)) {
  944. dataLabelWidth = dataLabel.getBBox().width;
  945. sideOverflow = null;
  946. // Overflow left
  947. if (
  948. x - dataLabelWidth < connectorPadding &&
  949. i === 1 // left half
  950. ) {
  951. sideOverflow = Math.round(
  952. dataLabelWidth - x + connectorPadding
  953. );
  954. overflow[3] = Math.max(sideOverflow, overflow[3]);
  955. // Overflow right
  956. } else if (
  957. x + dataLabelWidth > plotWidth - connectorPadding &&
  958. i === 0 // right half
  959. ) {
  960. sideOverflow = Math.round(
  961. x + dataLabelWidth - plotWidth + connectorPadding
  962. );
  963. overflow[1] = Math.max(sideOverflow, overflow[1]);
  964. }
  965. // Overflow top
  966. if (y - labelHeight / 2 < 0) {
  967. overflow[0] = Math.max(
  968. Math.round(-y + labelHeight / 2),
  969. overflow[0]
  970. );
  971. // Overflow left
  972. } else if (y + labelHeight / 2 > plotHeight) {
  973. overflow[2] = Math.max(
  974. Math.round(y + labelHeight / 2 - plotHeight),
  975. overflow[2]
  976. );
  977. }
  978. dataLabel.sideOverflow = sideOverflow;
  979. }
  980. } // for each point
  981. }); // for each half
  982. // Do not apply the final placement and draw the connectors until we
  983. // have verified that labels are not spilling over.
  984. if (
  985. arrayMax(overflow) === 0 ||
  986. this.verifyDataLabelOverflow(overflow)
  987. ) {
  988. // Place the labels in the final position
  989. this.placeDataLabels();
  990. // Draw the connectors
  991. if (connectorWidth) {
  992. this.points.forEach(function (point) {
  993. var isNew;
  994. connector = point.connector;
  995. dataLabel = point.dataLabel;
  996. if (
  997. dataLabel &&
  998. dataLabel._pos &&
  999. point.visible &&
  1000. point.labelDistance > 0
  1001. ) {
  1002. visibility = dataLabel._attr.visibility;
  1003. isNew = !connector;
  1004. if (isNew) {
  1005. point.connector = connector = chart.renderer.path()
  1006. .addClass(
  1007. 'highcharts-data-label-connector ' +
  1008. ' highcharts-color-' + point.colorIndex +
  1009. (
  1010. point.className ?
  1011. ' ' + point.className :
  1012. ''
  1013. )
  1014. )
  1015. .add(series.dataLabelsGroup);
  1016. if (!chart.styledMode) {
  1017. connector.attr({
  1018. 'stroke-width': connectorWidth,
  1019. 'stroke': (
  1020. options.connectorColor ||
  1021. point.color ||
  1022. '#666666'
  1023. )
  1024. });
  1025. }
  1026. }
  1027. connector[isNew ? 'attr' : 'animate']({
  1028. d: point.getConnectorPath()
  1029. });
  1030. connector.attr('visibility', visibility);
  1031. } else if (connector) {
  1032. point.connector = connector.destroy();
  1033. }
  1034. });
  1035. }
  1036. }
  1037. };
  1038. /**
  1039. * Extendable method for getting the path of the connector between the data
  1040. * label and the pie slice.
  1041. *
  1042. * @private
  1043. * @function Highcharts.seriesTypes.pie#connectorPath
  1044. *
  1045. * @param {*} labelPos
  1046. *
  1047. * @return {Highcharts.PathObject}
  1048. */
  1049. // TODO: depracated - remove it
  1050. /*
  1051. seriesTypes.pie.prototype.connectorPath = function (labelPos) {
  1052. var x = labelPos.x,
  1053. y = labelPos.y;
  1054. return pick(this.options.dataLabels.softConnector, true) ? [
  1055. 'M',
  1056. // end of the string at the label
  1057. x + (labelPos[6] === 'left' ? 5 : -5), y,
  1058. 'C',
  1059. x, y, // first break, next to the label
  1060. 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
  1061. labelPos[2], labelPos[3], // second break
  1062. 'L',
  1063. labelPos[4], labelPos[5] // base
  1064. ] : [
  1065. 'M',
  1066. // end of the string at the label
  1067. x + (labelPos[6] === 'left' ? 5 : -5), y,
  1068. 'L',
  1069. labelPos[2], labelPos[3], // second break
  1070. 'L',
  1071. labelPos[4], labelPos[5] // base
  1072. ];
  1073. };
  1074. */
  1075. /**
  1076. * Perform the final placement of the data labels after we have verified
  1077. * that they fall within the plot area.
  1078. *
  1079. * @private
  1080. * @function Highcharts.seriesTypes.pie#placeDataLabels
  1081. */
  1082. seriesTypes.pie.prototype.placeDataLabels = function () {
  1083. this.points.forEach(function (point) {
  1084. var dataLabel = point.dataLabel,
  1085. _pos;
  1086. if (dataLabel && point.visible) {
  1087. _pos = dataLabel._pos;
  1088. if (_pos) {
  1089. // Shorten data labels with ellipsis if they still overflow
  1090. // after the pie has reached minSize (#223).
  1091. if (dataLabel.sideOverflow) {
  1092. dataLabel._attr.width =
  1093. dataLabel.getBBox().width - dataLabel.sideOverflow;
  1094. dataLabel.css({
  1095. width: dataLabel._attr.width + 'px',
  1096. textOverflow: (
  1097. (this.options.dataLabels.style || {})
  1098. .textOverflow ||
  1099. 'ellipsis'
  1100. )
  1101. });
  1102. dataLabel.shortened = true;
  1103. }
  1104. dataLabel.attr(dataLabel._attr);
  1105. dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
  1106. dataLabel.moved = true;
  1107. } else if (dataLabel) {
  1108. dataLabel.attr({ y: -9999 });
  1109. }
  1110. }
  1111. }, this);
  1112. };
  1113. seriesTypes.pie.prototype.alignDataLabel = noop;
  1114. /**
  1115. * Verify whether the data labels are allowed to draw, or we should run more
  1116. * translation and data label positioning to keep them inside the plot area.
  1117. * Returns true when data labels are ready to draw.
  1118. *
  1119. * @private
  1120. * @function Highcharts.seriesTypes.pie#verifyDataLabelOverflow
  1121. *
  1122. * @param {boolean} overflow
  1123. *
  1124. * @return {boolean}
  1125. */
  1126. seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) {
  1127. var center = this.center,
  1128. options = this.options,
  1129. centerOption = options.center,
  1130. minSize = options.minSize || 80,
  1131. newSize = minSize,
  1132. // If a size is set, return true and don't try to shrink the pie
  1133. // to fit the labels.
  1134. ret = options.size !== null;
  1135. if (!ret) {
  1136. // Handle horizontal size and center
  1137. if (centerOption[0] !== null) { // Fixed center
  1138. newSize = Math.max(center[2] -
  1139. Math.max(overflow[1], overflow[3]), minSize);
  1140. } else { // Auto center
  1141. newSize = Math.max(
  1142. // horizontal overflow
  1143. center[2] - overflow[1] - overflow[3],
  1144. minSize
  1145. );
  1146. // horizontal center
  1147. center[0] += (overflow[3] - overflow[1]) / 2;
  1148. }
  1149. // Handle vertical size and center
  1150. if (centerOption[1] !== null) { // Fixed center
  1151. newSize = Math.max(Math.min(newSize, center[2] -
  1152. Math.max(overflow[0], overflow[2])), minSize);
  1153. } else { // Auto center
  1154. newSize = Math.max(
  1155. Math.min(
  1156. newSize,
  1157. // vertical overflow
  1158. center[2] - overflow[0] - overflow[2]
  1159. ),
  1160. minSize
  1161. );
  1162. // vertical center
  1163. center[1] += (overflow[0] - overflow[2]) / 2;
  1164. }
  1165. // If the size must be decreased, we need to run translate and
  1166. // drawDataLabels again
  1167. if (newSize < center[2]) {
  1168. center[2] = newSize;
  1169. center[3] = Math.min( // #3632
  1170. relativeLength(options.innerSize || 0, newSize),
  1171. newSize
  1172. );
  1173. this.translate(center);
  1174. if (this.drawDataLabels) {
  1175. this.drawDataLabels();
  1176. }
  1177. // Else, return true to indicate that the pie and its labels is
  1178. // within the plot area
  1179. } else {
  1180. ret = true;
  1181. }
  1182. }
  1183. return ret;
  1184. };
  1185. }
  1186. if (seriesTypes.column) {
  1187. /**
  1188. * Override the basic data label alignment by adjusting for the position of
  1189. * the column.
  1190. *
  1191. * @private
  1192. * @function Highcharts.seriesTypes.column#alignDataLabel
  1193. *
  1194. * @param {Highcharts.Point} point
  1195. *
  1196. * @param {Highcharts.SVGElement} dataLabel
  1197. *
  1198. * @param {Highcharts.PlotSeriesDataLabelsOptions} options
  1199. *
  1200. * @param {Highcharts.BBoxObject} alignTo
  1201. *
  1202. * @param {boolean} isNew
  1203. */
  1204. seriesTypes.column.prototype.alignDataLabel = function (
  1205. point,
  1206. dataLabel,
  1207. options,
  1208. alignTo,
  1209. isNew
  1210. ) {
  1211. var inverted = this.chart.inverted,
  1212. series = point.series,
  1213. // data label box for alignment
  1214. dlBox = point.dlBox || point.shapeArgs,
  1215. below = pick(
  1216. point.below, // range series
  1217. point.plotY > pick(this.translatedThreshold, series.yAxis.len)
  1218. ),
  1219. // draw it inside the box?
  1220. inside = pick(options.inside, !!this.options.stacking),
  1221. overshoot;
  1222. // Align to the column itself, or the top of it
  1223. if (dlBox) { // Area range uses this method but not alignTo
  1224. alignTo = merge(dlBox);
  1225. if (alignTo.y < 0) {
  1226. alignTo.height += alignTo.y;
  1227. alignTo.y = 0;
  1228. }
  1229. overshoot = alignTo.y + alignTo.height - series.yAxis.len;
  1230. if (overshoot > 0) {
  1231. alignTo.height -= overshoot;
  1232. }
  1233. if (inverted) {
  1234. alignTo = {
  1235. x: series.yAxis.len - alignTo.y - alignTo.height,
  1236. y: series.xAxis.len - alignTo.x - alignTo.width,
  1237. width: alignTo.height,
  1238. height: alignTo.width
  1239. };
  1240. }
  1241. // Compute the alignment box
  1242. if (!inside) {
  1243. if (inverted) {
  1244. alignTo.x += below ? 0 : alignTo.width;
  1245. alignTo.width = 0;
  1246. } else {
  1247. alignTo.y += below ? alignTo.height : 0;
  1248. alignTo.height = 0;
  1249. }
  1250. }
  1251. }
  1252. // When alignment is undefined (typically columns and bars), display the
  1253. // individual point below or above the point depending on the threshold
  1254. options.align = pick(
  1255. options.align,
  1256. !inverted || inside ? 'center' : below ? 'right' : 'left'
  1257. );
  1258. options.verticalAlign = pick(
  1259. options.verticalAlign,
  1260. inverted || inside ? 'middle' : below ? 'top' : 'bottom'
  1261. );
  1262. // Call the parent method
  1263. Series.prototype.alignDataLabel.call(
  1264. this,
  1265. point,
  1266. dataLabel,
  1267. options,
  1268. alignTo,
  1269. isNew
  1270. );
  1271. // If label was justified and we have contrast, set it:
  1272. if (point.isLabelJustified && point.contrastColor) {
  1273. dataLabel.css({
  1274. color: point.contrastColor
  1275. });
  1276. }
  1277. };
  1278. }