import { css } from '@emotion/css'; import React, { useMemo, useRef, useState } from 'react'; import { DashboardCursorSync, PanelProps, TimeRange } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { ScaleDistributionConfig } from '@grafana/schema'; import { ScaleDistribution, TooltipPlugin2, TooltipDisplayMode, UPlotChart, usePanelContext, useStyles2, useTheme2, VizLayout, EventBusPlugin, } from '@grafana/ui'; import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { ColorScale } from 'app/core/components/ColorScale/ColorScale'; import { readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2'; import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; import { HeatmapTooltip } from './HeatmapTooltip'; import { prepareHeatmapData } from './fields'; import { quantizeScheme } from './palettes'; import { Options } from './types'; import { prepConfig } from './utils'; interface HeatmapPanelProps extends PanelProps {} export const HeatmapPanel = ({ data, id, timeRange, timeZone, width, height, options, fieldConfig, eventBus, onChangeTimeRange, replaceVariables, }: HeatmapPanelProps) => { const theme = useTheme2(); const styles = useStyles2(getStyles); const { sync, eventsScope, canAddAnnotations } = usePanelContext(); const cursorSync = sync?.() ?? DashboardCursorSync.Off; // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); // ugh let timeRangeRef = useRef(timeRange); timeRangeRef.current = timeRange; const palette = useMemo(() => quantizeScheme(options.color, theme), [options.color, theme]); const info = useMemo(() => { try { return prepareHeatmapData(data.series, data.annotations, options, palette, theme, replaceVariables); } catch (ex) { return { warning: `${ex}` }; } }, [data.series, data.annotations, options, palette, theme, replaceVariables]); const facets = useMemo(() => { let exemplarsXFacet: number[] | undefined = []; // "Time" field let exemplarsYFacet: Array = []; const meta = readHeatmapRowsCustomMeta(info.heatmap); if (info.exemplars?.length) { exemplarsXFacet = info.exemplars?.fields[0].values; // render by match on ordinal y label if (meta.yMatchWithLabel) { // ordinal/labeled heatmap-buckets? const hasLabeledY = meta.yOrdinalDisplay != null; if (hasLabeledY) { let matchExemplarsBy = info.exemplars?.fields.find((field) => field.name === meta.yMatchWithLabel)!.values; exemplarsYFacet = matchExemplarsBy.map((label) => meta.yOrdinalLabel?.indexOf(label)); } else { exemplarsYFacet = info.exemplars?.fields[1].values; // "Value" field } } // render by raw value else { exemplarsYFacet = info.exemplars?.fields[1].values; // "Value" field } } return [null, info.heatmap?.fields.map((f) => f.values), [exemplarsXFacet, exemplarsYFacet]]; }, [info.heatmap, info.exemplars]); // ugh const dataRef = useRef(info); dataRef.current = info; const builder = useMemo(() => { const scaleConfig: ScaleDistributionConfig = dataRef.current?.heatmap?.fields[1].config?.custom?.scaleDistribution; return prepConfig({ dataRef, theme, timeZone, getTimeRange: () => timeRangeRef.current, cellGap: options.cellGap, hideLE: options.filterValues?.le, hideGE: options.filterValues?.ge, exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)', yAxisConfig: options.yAxis, ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [options, timeZone, data.structureRev, cursorSync]); const renderLegend = () => { if (!info.heatmap || !options.legend.show) { return null; } let hoverValue: number | undefined = undefined; // let heatmapType = dataRef.current?.heatmap?.meta?.type; // let isSparseHeatmap = heatmapType === DataFrameType.HeatmapCells && !isHeatmapCellsDense(dataRef.current?.heatmap!); // let countFieldIdx = !isSparseHeatmap ? 2 : 3; // const countField = info.heatmap.fields[countFieldIdx]; // seriesIdx: 1 is heatmap layer; 2 is exemplar layer // if (hover && info.heatmap.fields && hover.seriesIdx === 1) { // hoverValue = countField.values[hover.dataIdx]; // } return (
); }; if (info.warning || !info.heatmap) { return ( ); } const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); return ( <> {(vizWidth: number, vizHeight: number) => ( {cursorSync !== DashboardCursorSync.Off && ( )} {options.tooltip.mode !== TooltipDisplayMode.None && ( { if (enableAnnotationCreation && timeRange2 != null) { setNewAnnotationRange(timeRange2); dismiss(); return; } const annotate = () => { let xVal = u.posToVal(u.cursor.left!, 'x'); setNewAnnotationRange({ from: xVal, to: xVal }); dismiss(); }; return ( ); }} maxWidth={options.tooltip.maxWidth} /> )} )} ); }; const getStyles = () => ({ colorScaleWrapper: css({ marginLeft: '25px', padding: '10px 0', maxWidth: '300px', }), });