heatmap_tooltip.ts 7.3 KB

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