HeatmapPanel.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import { css } from '@emotion/css';
  2. import React, { useCallback, useMemo, useRef, useState } from 'react';
  3. import { DataFrameType, GrafanaTheme2, PanelProps, TimeRange } from '@grafana/data';
  4. import { PanelDataErrorView } from '@grafana/runtime';
  5. import { ScaleDistributionConfig } from '@grafana/schema';
  6. import {
  7. Portal,
  8. ScaleDistribution,
  9. UPlotChart,
  10. usePanelContext,
  11. useStyles2,
  12. useTheme2,
  13. VizLayout,
  14. VizTooltipContainer,
  15. } from '@grafana/ui';
  16. import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
  17. import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
  18. import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
  19. import { HeatmapHoverView } from './HeatmapHoverView';
  20. import { prepareHeatmapData } from './fields';
  21. import { PanelOptions } from './models.gen';
  22. import { quantizeScheme } from './palettes';
  23. import { HeatmapHoverEvent, prepConfig } from './utils';
  24. interface HeatmapPanelProps extends PanelProps<PanelOptions> {}
  25. export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
  26. data,
  27. id,
  28. timeRange,
  29. timeZone,
  30. width,
  31. height,
  32. options,
  33. fieldConfig,
  34. eventBus,
  35. onChangeTimeRange,
  36. replaceVariables,
  37. }) => {
  38. const theme = useTheme2();
  39. const styles = useStyles2(getStyles);
  40. const { sync } = usePanelContext();
  41. // ugh
  42. let timeRangeRef = useRef<TimeRange>(timeRange);
  43. timeRangeRef.current = timeRange;
  44. const info = useMemo(() => {
  45. try {
  46. return prepareHeatmapData(data, options, theme);
  47. } catch (ex) {
  48. return { warning: `${ex}` };
  49. }
  50. }, [data, options, theme]);
  51. const facets = useMemo(() => {
  52. let exemplarsXFacet: number[] = []; // "Time" field
  53. let exemplarsyFacet: number[] = [];
  54. const meta = readHeatmapRowsCustomMeta(info.heatmap);
  55. if (info.exemplars?.length && meta.yMatchWithLabel) {
  56. exemplarsXFacet = info.exemplars?.fields[0].values.toArray();
  57. // ordinal/labeled heatmap-buckets?
  58. const hasLabeledY = meta.yOrdinalDisplay != null;
  59. if (hasLabeledY) {
  60. let matchExemplarsBy = info.exemplars?.fields
  61. .find((field) => field.name === meta.yMatchWithLabel)!
  62. .values.toArray();
  63. exemplarsyFacet = matchExemplarsBy.map((label) => meta.yOrdinalLabel?.indexOf(label)) as number[];
  64. } else {
  65. exemplarsyFacet = info.exemplars?.fields[1].values.toArray() as number[]; // "Value" field
  66. }
  67. }
  68. return [null, info.heatmap?.fields.map((f) => f.values.toArray()), [exemplarsXFacet, exemplarsyFacet]];
  69. }, [info.heatmap, info.exemplars]);
  70. const palette = useMemo(() => quantizeScheme(options.color, theme), [options.color, theme]);
  71. const [hover, setHover] = useState<HeatmapHoverEvent | undefined>(undefined);
  72. const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
  73. const isToolTipOpen = useRef<boolean>(false);
  74. const onCloseToolTip = () => {
  75. isToolTipOpen.current = false;
  76. setShouldDisplayCloseButton(false);
  77. onhover(null);
  78. };
  79. const onclick = () => {
  80. isToolTipOpen.current = !isToolTipOpen.current;
  81. // Linking into useState required to re-render tooltip
  82. setShouldDisplayCloseButton(isToolTipOpen.current);
  83. };
  84. const onhover = useCallback(
  85. (evt?: HeatmapHoverEvent | null) => {
  86. setHover(evt ?? undefined);
  87. },
  88. // eslint-disable-next-line react-hooks/exhaustive-deps
  89. [options, data.structureRev]
  90. );
  91. // ugh
  92. const dataRef = useRef(info);
  93. dataRef.current = info;
  94. const builder = useMemo(() => {
  95. const scaleConfig = dataRef.current?.heatmap?.fields[1].config?.custom
  96. ?.scaleDistribution as ScaleDistributionConfig;
  97. return prepConfig({
  98. dataRef,
  99. theme,
  100. eventBus,
  101. onhover: onhover,
  102. onclick: options.tooltip.show ? onclick : null,
  103. onzoom: (evt) => {
  104. const delta = evt.xMax - evt.xMin;
  105. if (delta > 1) {
  106. onChangeTimeRange({ from: evt.xMin, to: evt.xMax });
  107. }
  108. },
  109. isToolTipOpen,
  110. timeZone,
  111. getTimeRange: () => timeRangeRef.current,
  112. sync,
  113. palette,
  114. cellGap: options.cellGap,
  115. hideLE: options.filterValues?.le,
  116. hideGE: options.filterValues?.ge,
  117. exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)',
  118. yAxisConfig: options.yAxis,
  119. ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1,
  120. });
  121. // eslint-disable-next-line react-hooks/exhaustive-deps
  122. }, [options, data.structureRev]);
  123. const renderLegend = () => {
  124. if (!info.heatmap || !options.legend.show) {
  125. return null;
  126. }
  127. let heatmapType = dataRef.current?.heatmap?.meta?.type;
  128. let isSparseHeatmap = heatmapType === DataFrameType.HeatmapCells && !isHeatmapCellsDense(dataRef.current?.heatmap!);
  129. let countFieldIdx = !isSparseHeatmap ? 2 : 3;
  130. const countField = info.heatmap.fields[countFieldIdx];
  131. let hoverValue: number | undefined = undefined;
  132. // seriesIdx: 1 is heatmap layer; 2 is exemplar layer
  133. if (hover && info.heatmap.fields && hover.seriesIdx === 1) {
  134. hoverValue = countField.values.get(hover.dataIdx);
  135. }
  136. return (
  137. <VizLayout.Legend placement="bottom" maxHeight="20%">
  138. <div className={styles.colorScaleWrapper}>
  139. <ColorScale
  140. hoverValue={hoverValue}
  141. colorPalette={palette}
  142. min={dataRef.current.minValue!}
  143. max={dataRef.current.maxValue!}
  144. display={info.display}
  145. />
  146. </div>
  147. </VizLayout.Legend>
  148. );
  149. };
  150. if (info.warning || !info.heatmap) {
  151. return (
  152. <PanelDataErrorView
  153. panelId={id}
  154. fieldConfig={fieldConfig}
  155. data={data}
  156. needsNumberField={true}
  157. message={info.warning}
  158. />
  159. );
  160. }
  161. return (
  162. <>
  163. <VizLayout width={width} height={height} legend={renderLegend()}>
  164. {(vizWidth: number, vizHeight: number) => (
  165. <UPlotChart config={builder} data={facets as any} width={vizWidth} height={vizHeight} timeRange={timeRange}>
  166. {/*children ? children(config, alignedFrame) : null*/}
  167. </UPlotChart>
  168. )}
  169. </VizLayout>
  170. <Portal>
  171. {hover && options.tooltip.show && (
  172. <VizTooltipContainer
  173. position={{ x: hover.pageX, y: hover.pageY }}
  174. offset={{ x: 10, y: 10 }}
  175. allowPointerEvents={isToolTipOpen.current}
  176. >
  177. {shouldDisplayCloseButton && (
  178. <>
  179. <CloseButton onClick={onCloseToolTip} />
  180. <div className={styles.closeButtonSpacer} />
  181. </>
  182. )}
  183. <HeatmapHoverView data={info} hover={hover} showHistogram={options.tooltip.yHistogram} />
  184. </VizTooltipContainer>
  185. )}
  186. </Portal>
  187. </>
  188. );
  189. };
  190. const getStyles = (theme: GrafanaTheme2) => ({
  191. closeButtonSpacer: css`
  192. margin-bottom: 15px;
  193. `,
  194. colorScaleWrapper: css`
  195. margin-left: 25px;
  196. padding: 10px 0;
  197. max-width: 300px;
  198. `,
  199. });