OverlappingDataLabels.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /* *
  2. *
  3. * Highcharts module to hide overlapping data labels.
  4. * This module is included in Highcharts.
  5. *
  6. * (c) 2009-2021 Torstein Honsi
  7. *
  8. * License: www.highcharts.com/license
  9. *
  10. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  11. *
  12. * */
  13. 'use strict';
  14. import Chart from '../Core/Chart/Chart.js';
  15. import U from '../Core/Utilities.js';
  16. var addEvent = U.addEvent, fireEvent = U.fireEvent, isArray = U.isArray, isNumber = U.isNumber, objectEach = U.objectEach, pick = U.pick;
  17. /**
  18. * Internal type
  19. * @private
  20. */
  21. /* eslint-disable no-invalid-this */
  22. // Collect potensial overlapping data labels. Stack labels probably don't need
  23. // to be considered because they are usually accompanied by data labels that lie
  24. // inside the columns.
  25. addEvent(Chart, 'render', function collectAndHide() {
  26. var chart = this, labels = [];
  27. // Consider external label collectors
  28. (this.labelCollectors || []).forEach(function (collector) {
  29. labels = labels.concat(collector());
  30. });
  31. (this.yAxis || []).forEach(function (yAxis) {
  32. if (yAxis.stacking &&
  33. yAxis.options.stackLabels &&
  34. !yAxis.options.stackLabels.allowOverlap) {
  35. objectEach(yAxis.stacking.stacks, function (stack) {
  36. objectEach(stack, function (stackItem) {
  37. labels.push(stackItem.label);
  38. });
  39. });
  40. }
  41. });
  42. (this.series || []).forEach(function (series) {
  43. var dlOptions = series.options.dataLabels;
  44. if (series.visible &&
  45. !(dlOptions.enabled === false && !series._hasPointLabels)) { // #3866
  46. var push = function (points) {
  47. return points.forEach(function (point) {
  48. if (point.visible) {
  49. var dataLabels = (isArray(point.dataLabels) ?
  50. point.dataLabels :
  51. (point.dataLabel ? [point.dataLabel] : []));
  52. dataLabels.forEach(function (label) {
  53. var options = label.options;
  54. label.labelrank = pick(options.labelrank, point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118
  55. if (!options.allowOverlap) {
  56. labels.push(label);
  57. }
  58. else { // #13449
  59. label.oldOpacity = label.opacity;
  60. label.newOpacity = 1;
  61. hideOrShow(label, chart);
  62. }
  63. });
  64. }
  65. });
  66. };
  67. push(series.nodes || []);
  68. push(series.points);
  69. }
  70. });
  71. this.hideOverlappingLabels(labels);
  72. });
  73. /**
  74. * Hide overlapping labels. Labels are moved and faded in and out on zoom to
  75. * provide a smooth visual imression.
  76. *
  77. * @private
  78. * @function Highcharts.Chart#hideOverlappingLabels
  79. * @param {Array<Highcharts.SVGElement>} labels
  80. * Rendered data labels
  81. * @requires modules/overlapping-datalabels
  82. */
  83. Chart.prototype.hideOverlappingLabels = function (labels) {
  84. var chart = this, len = labels.length, ren = chart.renderer, label, i, j, label1, label2, box1, box2, isLabelAffected = false, isIntersectRect = function (box1, box2) {
  85. return !(box2.x >= box1.x + box1.width ||
  86. box2.x + box2.width <= box1.x ||
  87. box2.y >= box1.y + box1.height ||
  88. box2.y + box2.height <= box1.y);
  89. },
  90. // Get the box with its position inside the chart, as opposed to getBBox
  91. // that only reports the position relative to the parent.
  92. getAbsoluteBox = function (label) {
  93. var pos, parent, bBox,
  94. // Substract the padding if no background or border (#4333)
  95. padding = label.box ? 0 : (label.padding || 0), lineHeightCorrection = 0, xOffset = 0, boxWidth, alignValue;
  96. if (label &&
  97. (!label.alignAttr || label.placed)) {
  98. pos = label.alignAttr || {
  99. x: label.attr('x'),
  100. y: label.attr('y')
  101. };
  102. parent = label.parentGroup;
  103. // Get width and height if pure text nodes (stack labels)
  104. if (!label.width) {
  105. bBox = label.getBBox();
  106. label.width = bBox.width;
  107. label.height = bBox.height;
  108. // Labels positions are computed from top left corner, so
  109. // we need to substract the text height from text nodes too.
  110. lineHeightCorrection = ren
  111. .fontMetrics(null, label.element).h;
  112. }
  113. boxWidth = label.width - 2 * padding;
  114. alignValue = {
  115. left: '0',
  116. center: '0.5',
  117. right: '1'
  118. }[label.alignValue];
  119. if (alignValue) {
  120. xOffset = +alignValue * boxWidth;
  121. }
  122. else if (isNumber(label.x) && Math.round(label.x) !== label.translateX) {
  123. xOffset = label.x - label.translateX;
  124. }
  125. return {
  126. x: pos.x + (parent.translateX || 0) + padding -
  127. (xOffset || 0),
  128. y: pos.y + (parent.translateY || 0) + padding -
  129. lineHeightCorrection,
  130. width: label.width - 2 * padding,
  131. height: label.height - 2 * padding
  132. };
  133. }
  134. };
  135. for (i = 0; i < len; i++) {
  136. label = labels[i];
  137. if (label) {
  138. // Mark with initial opacity
  139. label.oldOpacity = label.opacity;
  140. label.newOpacity = 1;
  141. label.absoluteBox = getAbsoluteBox(label);
  142. }
  143. }
  144. // Prevent a situation in a gradually rising slope, that each label will
  145. // hide the previous one because the previous one always has lower rank.
  146. labels.sort(function (a, b) {
  147. return (b.labelrank || 0) - (a.labelrank || 0);
  148. });
  149. // Detect overlapping labels
  150. for (i = 0; i < len; i++) {
  151. label1 = labels[i];
  152. box1 = label1 && label1.absoluteBox;
  153. for (j = i + 1; j < len; ++j) {
  154. label2 = labels[j];
  155. box2 = label2 && label2.absoluteBox;
  156. if (box1 &&
  157. box2 &&
  158. label1 !== label2 && // #6465, polar chart with connectEnds
  159. label1.newOpacity !== 0 &&
  160. label2.newOpacity !== 0) {
  161. if (isIntersectRect(box1, box2)) {
  162. (label1.labelrank < label2.labelrank ? label1 : label2)
  163. .newOpacity = 0;
  164. }
  165. }
  166. }
  167. }
  168. // Hide or show
  169. labels.forEach(function (label) {
  170. if (hideOrShow(label, chart)) {
  171. isLabelAffected = true;
  172. }
  173. });
  174. if (isLabelAffected) {
  175. fireEvent(chart, 'afterHideAllOverlappingLabels');
  176. }
  177. };
  178. /**
  179. * Hide or show labels based on opacity.
  180. *
  181. * @private
  182. * @function hideOrShow
  183. * @param {Highcharts.SVGElement} label
  184. * The label.
  185. * @param {Highcharts.Chart} chart
  186. * The chart that contains the label.
  187. * @return {boolean}
  188. */
  189. function hideOrShow(label, chart) {
  190. var complete, newOpacity, isLabelAffected = false;
  191. if (label) {
  192. newOpacity = label.newOpacity;
  193. if (label.oldOpacity !== newOpacity) {
  194. // Make sure the label is completely hidden to avoid catching
  195. // clicks (#4362)
  196. if (label.alignAttr && label.placed) { // data labels
  197. label[newOpacity ? 'removeClass' : 'addClass']('highcharts-data-label-hidden');
  198. complete = function () {
  199. if (!chart.styledMode) {
  200. label.css({ pointerEvents: newOpacity ? 'auto' : 'none' });
  201. }
  202. label.visibility = newOpacity ? 'inherit' : 'hidden';
  203. };
  204. isLabelAffected = true;
  205. // Animate or set the opacity
  206. label.alignAttr.opacity = newOpacity;
  207. label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete);
  208. fireEvent(chart, 'afterHideOverlappingLabel');
  209. }
  210. else { // other labels, tick labels
  211. label.attr({
  212. opacity: newOpacity
  213. });
  214. }
  215. }
  216. label.isOld = true;
  217. }
  218. return isLabelAffected;
  219. }