OverlappingDataLabels.js 8.4 KB

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