heatmap_tooltip.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import * as d3 from 'd3';
  2. import $ from 'jquery';
  3. import _ from 'lodash';
  4. import kbn from 'app/core/utils/kbn';
  5. import { getValueBucketBound } from './heatmap_data_converter';
  6. let TOOLTIP_PADDING_X = 30;
  7. let TOOLTIP_PADDING_Y = 5;
  8. let HISTOGRAM_WIDTH = 160;
  9. let HISTOGRAM_HEIGHT = 40;
  10. export class HeatmapTooltip {
  11. tooltip: any;
  12. scope: any;
  13. dashboard: any;
  14. panelCtrl: any;
  15. panel: any;
  16. heatmapPanel: any;
  17. mouseOverBucket: boolean;
  18. originalFillColor: any;
  19. constructor(elem, scope) {
  20. this.scope = scope;
  21. this.dashboard = scope.ctrl.dashboard;
  22. this.panelCtrl = scope.ctrl;
  23. this.panel = scope.ctrl.panel;
  24. this.heatmapPanel = elem;
  25. this.mouseOverBucket = false;
  26. this.originalFillColor = null;
  27. elem.on('mouseover', this.onMouseOver.bind(this));
  28. elem.on('mouseleave', this.onMouseLeave.bind(this));
  29. }
  30. onMouseOver(e) {
  31. if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data.buckets)) {
  32. return;
  33. }
  34. if (!this.tooltip) {
  35. this.add();
  36. this.move(e);
  37. }
  38. }
  39. onMouseLeave() {
  40. this.destroy();
  41. }
  42. onMouseMove(e) {
  43. if (!this.panel.tooltip.show) {
  44. return;
  45. }
  46. this.move(e);
  47. }
  48. add() {
  49. this.tooltip = d3
  50. .select('body')
  51. .append('div')
  52. .attr('class', 'heatmap-tooltip graph-tooltip grafana-tooltip');
  53. }
  54. destroy() {
  55. if (this.tooltip) {
  56. this.tooltip.remove();
  57. }
  58. this.tooltip = null;
  59. }
  60. show(pos, data) {
  61. if (!this.panel.tooltip.show || !data) {
  62. return;
  63. }
  64. // shared tooltip mode
  65. if (pos.panelRelY) {
  66. return;
  67. }
  68. let { xBucketIndex, yBucketIndex } = this.getBucketIndexes(pos, data);
  69. if (!data.buckets[xBucketIndex] || !this.tooltip) {
  70. this.destroy();
  71. return;
  72. }
  73. let boundBottom, boundTop, valuesNumber;
  74. let xData = data.buckets[xBucketIndex];
  75. // Search in special 'zero' bucket also
  76. let yData = _.find(xData.buckets, (bucket, bucketIndex) => {
  77. return bucket.bounds.bottom === yBucketIndex || bucketIndex === yBucketIndex.toString();
  78. });
  79. let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
  80. let time = this.dashboard.formatDate(xData.x, tooltipTimeFormat);
  81. // Decimals override. Code from panel/graph/graph.ts
  82. let valueFormatter;
  83. if (_.isNumber(this.panel.tooltipDecimals)) {
  84. valueFormatter = this.valueFormatter(this.panel.tooltipDecimals, null);
  85. } else {
  86. // auto decimals
  87. // legend and tooltip gets one more decimal precision
  88. // than graph legend ticks
  89. let decimals = (this.panelCtrl.decimals || -1) + 1;
  90. valueFormatter = this.valueFormatter(decimals, this.panelCtrl.scaledDecimals + 2);
  91. }
  92. let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
  93. <div class="heatmap-histogram"></div>`;
  94. if (yData) {
  95. if (yData.bounds) {
  96. // Display 0 if bucket is a special 'zero' bucket
  97. let bottom = yData.y ? yData.bounds.bottom : 0;
  98. boundBottom = valueFormatter(bottom);
  99. boundTop = valueFormatter(yData.bounds.top);
  100. valuesNumber = yData.count;
  101. tooltipHtml += `<div>
  102. bucket: <b>${boundBottom} - ${boundTop}</b> <br>
  103. count: <b>${valuesNumber}</b> <br>
  104. </div>`;
  105. } else {
  106. // currently no bounds for pre bucketed data
  107. tooltipHtml += `<div>count: <b>${yData.count}</b><br></div>`;
  108. }
  109. } else {
  110. if (!this.panel.tooltip.showHistogram) {
  111. this.destroy();
  112. return;
  113. }
  114. boundBottom = yBucketIndex;
  115. boundTop = '';
  116. valuesNumber = 0;
  117. }
  118. this.tooltip.html(tooltipHtml);
  119. if (this.panel.tooltip.showHistogram) {
  120. this.addHistogram(xData);
  121. }
  122. this.move(pos);
  123. }
  124. getBucketIndexes(pos, data) {
  125. const xBucketIndex = this.getXBucketIndex(pos.offsetX, data);
  126. const yBucketIndex = this.getYBucketIndex(pos.offsetY, data);
  127. return { xBucketIndex, yBucketIndex };
  128. }
  129. getXBucketIndex(offsetX, data) {
  130. let x = this.scope.xScale.invert(offsetX - this.scope.yAxisWidth).valueOf();
  131. // First try to find X bucket by checking x pos is in the
  132. // [bucket.x, bucket.x + xBucketSize] interval
  133. let xBucket = _.find(data.buckets, bucket => {
  134. return x > bucket.x && x - bucket.x <= data.xBucketSize;
  135. });
  136. return xBucket ? xBucket.x : getValueBucketBound(x, data.xBucketSize, 1);
  137. }
  138. getYBucketIndex(offsetY, data) {
  139. let y = this.scope.yScale.invert(offsetY - this.scope.chartTop);
  140. let yBucketIndex = getValueBucketBound(y, data.yBucketSize, this.panel.yAxis.logBase);
  141. return yBucketIndex;
  142. }
  143. getSharedTooltipPos(pos) {
  144. // get pageX from position on x axis and pageY from relative position in original panel
  145. pos.pageX = this.heatmapPanel.offset().left + this.scope.xScale(pos.x);
  146. pos.pageY = this.heatmapPanel.offset().top + this.scope.chartHeight * pos.panelRelY;
  147. return pos;
  148. }
  149. addHistogram(data) {
  150. let xBucket = this.scope.ctrl.data.buckets[data.x];
  151. let yBucketSize = this.scope.ctrl.data.yBucketSize;
  152. let { min, max, ticks } = this.scope.ctrl.data.yAxis;
  153. let histogramData = _.map(xBucket.buckets, bucket => {
  154. let count = bucket.count !== undefined ? bucket.count : bucket.values.length;
  155. return [bucket.bounds.bottom, count];
  156. });
  157. histogramData = _.filter(histogramData, d => {
  158. return d[0] >= min && d[0] <= max;
  159. });
  160. let scale = this.scope.yScale.copy();
  161. let histXScale = scale.domain([min, max]).range([0, HISTOGRAM_WIDTH]);
  162. let barWidth;
  163. if (this.panel.yAxis.logBase === 1) {
  164. barWidth = Math.floor(HISTOGRAM_WIDTH / (max - min) * yBucketSize * 0.9);
  165. } else {
  166. let barNumberFactor = yBucketSize ? yBucketSize : 1;
  167. barWidth = Math.floor(HISTOGRAM_WIDTH / ticks / barNumberFactor * 0.9);
  168. }
  169. barWidth = Math.max(barWidth, 1);
  170. // Normalize histogram Y axis
  171. let histogramDomain = _.reduce(_.map(histogramData, d => d[1]), (sum, val) => sum + val, 0);
  172. let histYScale = d3
  173. .scaleLinear()
  174. .domain([0, histogramDomain])
  175. .range([0, HISTOGRAM_HEIGHT]);
  176. let histogram = this.tooltip
  177. .select('.heatmap-histogram')
  178. .append('svg')
  179. .attr('width', HISTOGRAM_WIDTH)
  180. .attr('height', HISTOGRAM_HEIGHT);
  181. histogram
  182. .selectAll('.bar')
  183. .data(histogramData)
  184. .enter()
  185. .append('rect')
  186. .attr('x', d => {
  187. return histXScale(d[0]);
  188. })
  189. .attr('width', barWidth)
  190. .attr('y', d => {
  191. return HISTOGRAM_HEIGHT - histYScale(d[1]);
  192. })
  193. .attr('height', d => {
  194. return histYScale(d[1]);
  195. });
  196. }
  197. move(pos) {
  198. if (!this.tooltip) {
  199. return;
  200. }
  201. let elem = $(this.tooltip.node())[0];
  202. let tooltipWidth = elem.clientWidth;
  203. let tooltipHeight = elem.clientHeight;
  204. let left = pos.pageX + TOOLTIP_PADDING_X;
  205. let top = pos.pageY + TOOLTIP_PADDING_Y;
  206. if (pos.pageX + tooltipWidth + 40 > window.innerWidth) {
  207. left = pos.pageX - tooltipWidth - TOOLTIP_PADDING_X;
  208. }
  209. if (pos.pageY - window.pageYOffset + tooltipHeight + 20 > window.innerHeight) {
  210. top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
  211. }
  212. return this.tooltip.style('left', left + 'px').style('top', top + 'px');
  213. }
  214. valueFormatter(decimals, scaledDecimals = null) {
  215. let format = this.panel.yAxis.format;
  216. return function(value) {
  217. return kbn.valueFormats[format](value, decimals, scaledDecimals);
  218. };
  219. }
  220. }