OverlappingDataLabels.js 7.5 KB

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