heatmap_tooltip.ts 6.8 KB

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