HeatmapHoverView.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import React, { useEffect, useRef, useState } from 'react';
  2. import { DataFrameType, Field, FieldType, formattedValueToString, getFieldDisplayName, LinkModel } from '@grafana/data';
  3. import { LinkButton, VerticalGroup } from '@grafana/ui';
  4. import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
  5. import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
  6. import { HeatmapCellLayout } from 'app/features/transformers/calculateHeatmap/models.gen';
  7. import { DataHoverView } from '../geomap/components/DataHoverView';
  8. import { HeatmapData } from './fields';
  9. import { HeatmapHoverEvent } from './utils';
  10. type Props = {
  11. data: HeatmapData;
  12. hover: HeatmapHoverEvent;
  13. showHistogram?: boolean;
  14. };
  15. export const HeatmapHoverView = (props: Props) => {
  16. if (props.hover.seriesIdx === 2) {
  17. return <DataHoverView data={props.data.exemplars} rowIndex={props.hover.dataIdx} />;
  18. }
  19. return <HeatmapHoverCell {...props} />;
  20. };
  21. const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => {
  22. const index = hover.dataIdx;
  23. const xField = data.heatmap?.fields[0];
  24. const yField = data.heatmap?.fields[1];
  25. const countField = data.heatmap?.fields[2];
  26. const xDisp = (v: any) => {
  27. if (xField?.display) {
  28. return formattedValueToString(xField.display(v));
  29. }
  30. if (xField?.type === FieldType.time) {
  31. const tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
  32. const dashboard = getDashboardSrv().getCurrent();
  33. return dashboard?.formatDate(v, tooltipTimeFormat);
  34. }
  35. return `${v}`;
  36. };
  37. const xVals = xField?.values.toArray();
  38. const yVals = yField?.values.toArray();
  39. const countVals = countField?.values.toArray();
  40. // labeled buckets
  41. const meta = readHeatmapRowsCustomMeta(data.heatmap);
  42. const yDispSrc = meta.yOrdinalDisplay ?? yVals;
  43. const yDisp = yField?.display ? (v: any) => formattedValueToString(yField.display!(v)) : (v: any) => `${v}`;
  44. const yValueIdx = index % data.yBucketCount! ?? 0;
  45. const yMinIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx - 1 : yValueIdx;
  46. const yMaxIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx : yValueIdx + 1;
  47. const yBucketMin = yDispSrc?.[yMinIdx];
  48. const yBucketMax = yDispSrc?.[yMaxIdx];
  49. const xBucketMin = xVals?.[index];
  50. const xBucketMax = xBucketMin + data.xBucketSize;
  51. const count = countVals?.[index];
  52. const visibleFields = data.heatmap?.fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip));
  53. const links: Array<LinkModel<Field>> = [];
  54. const linkLookup = new Set<string>();
  55. for (const field of visibleFields ?? []) {
  56. // TODO: Currently always undefined? (getLinks)
  57. if (field.getLinks) {
  58. const v = field.values.get(index);
  59. const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v };
  60. field.getLinks({ calculatedValue: disp, valueRowIndex: index }).forEach((link) => {
  61. const key = `${link.title}/${link.href}`;
  62. if (!linkLookup.has(key)) {
  63. links.push(link);
  64. linkLookup.add(key);
  65. }
  66. });
  67. }
  68. }
  69. let can = useRef<HTMLCanvasElement>(null);
  70. let histCssWidth = 150;
  71. let histCssHeight = 50;
  72. let histCanWidth = Math.round(histCssWidth * devicePixelRatio);
  73. let histCanHeight = Math.round(histCssHeight * devicePixelRatio);
  74. useEffect(
  75. () => {
  76. if (showHistogram) {
  77. let histCtx = can.current?.getContext('2d');
  78. if (histCtx && xVals && yVals && countVals) {
  79. let fromIdx = index;
  80. while (xVals[fromIdx--] === xVals[index]) {}
  81. fromIdx++;
  82. let toIdx = fromIdx + data.yBucketCount!;
  83. let maxCount = 0;
  84. let i = fromIdx;
  85. while (i < toIdx) {
  86. let c = countVals[i];
  87. maxCount = Math.max(maxCount, c);
  88. i++;
  89. }
  90. let pHov = new Path2D();
  91. let pRest = new Path2D();
  92. i = fromIdx;
  93. let j = 0;
  94. while (i < toIdx) {
  95. let c = countVals[i];
  96. if (c > 0) {
  97. let pctY = c / maxCount;
  98. let pctX = j / (data.yBucketCount! + 1);
  99. let p = i === index ? pHov : pRest;
  100. p.rect(
  101. Math.round(histCanWidth * pctX),
  102. Math.round(histCanHeight * (1 - pctY)),
  103. Math.round(histCanWidth / data.yBucketCount!),
  104. Math.round(histCanHeight * pctY)
  105. );
  106. }
  107. i++;
  108. j++;
  109. }
  110. histCtx.clearRect(0, 0, histCanWidth, histCanHeight);
  111. histCtx.fillStyle = '#ffffff80';
  112. histCtx.fill(pRest);
  113. histCtx.fillStyle = '#ff000080';
  114. histCtx.fill(pHov);
  115. }
  116. }
  117. },
  118. // eslint-disable-next-line react-hooks/exhaustive-deps
  119. [index]
  120. );
  121. const [isSparse] = useState(
  122. () => data.heatmap?.meta?.type === DataFrameType.HeatmapCells && !isHeatmapCellsDense(data.heatmap)
  123. );
  124. if (isSparse) {
  125. return (
  126. <div>
  127. <DataHoverView data={data.heatmap} rowIndex={index} />
  128. </div>
  129. );
  130. }
  131. const renderYBuckets = () => {
  132. switch (data.yLayout) {
  133. case HeatmapCellLayout.unknown:
  134. return <div>{yDisp(yBucketMin)}</div>;
  135. }
  136. return (
  137. <div>
  138. Bucket: {yDisp(yBucketMin)} - {yDisp(yBucketMax)}
  139. </div>
  140. );
  141. };
  142. return (
  143. <>
  144. <div>
  145. <div>{xDisp(xBucketMin)}</div>
  146. <div>{xDisp(xBucketMax)}</div>
  147. </div>
  148. {showHistogram && (
  149. <canvas
  150. width={histCanWidth}
  151. height={histCanHeight}
  152. ref={can}
  153. style={{ width: histCanWidth + 'px', height: histCanHeight + 'px' }}
  154. />
  155. )}
  156. <div>
  157. {renderYBuckets()}
  158. <div>
  159. {getFieldDisplayName(countField!, data.heatmap)}: {data.display!(count)}
  160. </div>
  161. </div>
  162. {links.length > 0 && (
  163. <VerticalGroup>
  164. {links.map((link, i) => (
  165. <LinkButton
  166. key={i}
  167. icon={'external-link-alt'}
  168. target={link.target}
  169. href={link.href}
  170. onClick={link.onClick}
  171. fill="text"
  172. style={{ width: '100%' }}
  173. >
  174. {link.title}
  175. </LinkButton>
  176. ))}
  177. </VerticalGroup>
  178. )}
  179. </>
  180. );
  181. };