OverlappingDataLabels.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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 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. });
  59. }
  60. });
  61. };
  62. push(series.nodes || []);
  63. push(series.points);
  64. }
  65. });
  66. this.hideOverlappingLabels(labels);
  67. });
  68. /**
  69. * Hide overlapping labels. Labels are moved and faded in and out on zoom to
  70. * provide a smooth visual imression.
  71. *
  72. * @private
  73. * @function Highcharts.Chart#hideOverlappingLabels
  74. * @param {Array<Highcharts.SVGElement>} labels
  75. * Rendered data labels
  76. * @requires modules/overlapping-datalabels
  77. */
  78. Chart.prototype.hideOverlappingLabels = function (labels) {
  79. var chart = this, len = labels.length, ren = chart.renderer, label, i, j, label1, label2, box1, box2, isLabelAffected = false, isIntersectRect = function (box1, box2) {
  80. return !(box2.x >= box1.x + box1.width ||
  81. box2.x + box2.width <= box1.x ||
  82. box2.y >= box1.y + box1.height ||
  83. box2.y + box2.height <= box1.y);
  84. },
  85. // Get the box with its position inside the chart, as opposed to getBBox
  86. // that only reports the position relative to the parent.
  87. getAbsoluteBox = function (label) {
  88. var pos, parent, bBox,
  89. // Substract the padding if no background or border (#4333)
  90. padding = label.box ? 0 : (label.padding || 0), lineHeightCorrection = 0, xOffset = 0, boxWidth, alignValue;
  91. if (label &&
  92. (!label.alignAttr || label.placed)) {
  93. pos = label.alignAttr || {
  94. x: label.attr('x'),
  95. y: label.attr('y')
  96. };
  97. parent = label.parentGroup;
  98. // Get width and height if pure text nodes (stack labels)
  99. if (!label.width) {
  100. bBox = label.getBBox();
  101. label.width = bBox.width;
  102. label.height = bBox.height;
  103. // Labels positions are computed from top left corner, so
  104. // we need to substract the text height from text nodes too.
  105. lineHeightCorrection = ren
  106. .fontMetrics(null, label.element).h;
  107. }
  108. boxWidth = label.width - 2 * padding;
  109. alignValue = {
  110. left: '0',
  111. center: '0.5',
  112. right: '1'
  113. }[label.alignValue];
  114. if (alignValue) {
  115. xOffset = +alignValue * boxWidth;
  116. }
  117. else if (isNumber(label.x) && Math.round(label.x) !== label.translateX) {
  118. xOffset = label.x - label.translateX;
  119. }
  120. return {
  121. x: pos.x + (parent.translateX || 0) + padding -
  122. (xOffset || 0),
  123. y: pos.y + (parent.translateY || 0) + padding -
  124. lineHeightCorrection,
  125. width: label.width - 2 * padding,
  126. height: label.height - 2 * padding
  127. };
  128. }
  129. };
  130. for (i = 0; i < len; i++) {
  131. label = labels[i];
  132. if (label) {
  133. // Mark with initial opacity
  134. label.oldOpacity = label.opacity;
  135. label.newOpacity = 1;
  136. label.absoluteBox = getAbsoluteBox(label);
  137. }
  138. }
  139. // Prevent a situation in a gradually rising slope, that each label will
  140. // hide the previous one because the previous one always has lower rank.
  141. labels.sort(function (a, b) {
  142. return (b.labelrank || 0) - (a.labelrank || 0);
  143. });
  144. // Detect overlapping labels
  145. for (i = 0; i < len; i++) {
  146. label1 = labels[i];
  147. box1 = label1 && label1.absoluteBox;
  148. for (j = i + 1; j < len; ++j) {
  149. label2 = labels[j];
  150. box2 = label2 && label2.absoluteBox;
  151. if (box1 &&
  152. box2 &&
  153. label1 !== label2 && // #6465, polar chart with connectEnds
  154. label1.newOpacity !== 0 &&
  155. label2.newOpacity !== 0) {
  156. if (isIntersectRect(box1, box2)) {
  157. (label1.labelrank < label2.labelrank ? label1 : label2)
  158. .newOpacity = 0;
  159. }
  160. }
  161. }
  162. }
  163. // Hide or show
  164. labels.forEach(function (label) {
  165. var complete, newOpacity;
  166. if (label) {
  167. newOpacity = label.newOpacity;
  168. if (label.oldOpacity !== newOpacity) {
  169. // Make sure the label is completely hidden to avoid catching
  170. // clicks (#4362)
  171. if (label.alignAttr && label.placed) { // data labels
  172. label[newOpacity ? 'removeClass' : 'addClass']('highcharts-data-label-hidden');
  173. complete = function () {
  174. if (!chart.styledMode) {
  175. label.css({ pointerEvents: newOpacity ? 'auto' : 'none' });
  176. }
  177. label.visibility = newOpacity ? 'inherit' : 'hidden';
  178. };
  179. isLabelAffected = true;
  180. // Animate or set the opacity
  181. label.alignAttr.opacity = newOpacity;
  182. label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete);
  183. fireEvent(chart, 'afterHideOverlappingLabel');
  184. }
  185. else { // other labels, tick labels
  186. label.attr({
  187. opacity: newOpacity
  188. });
  189. }
  190. }
  191. label.isOld = true;
  192. }
  193. });
  194. if (isLabelAffected) {
  195. fireEvent(chart, 'afterHideAllOverlappingLabels');
  196. }
  197. };