overlapping-datalabels.src.js 7.4 KB

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