heatmap_tooltip.ts 7.0 KB

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