From 46c5e5417be8676601d0106a95bde32bd8f09f4c Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 12 Jul 2023 18:23:30 -0500 Subject: [PATCH] Heatmap: Build cell colors during data prep instead of render (#71509) --- .betterer.results | 4 +- .../plugins/panel/heatmap/HeatmapPanel.tsx | 14 ++--- public/app/plugins/panel/heatmap/fields.ts | 57 ++++++++++++++----- public/app/plugins/panel/heatmap/module.tsx | 13 +++-- .../app/plugins/panel/heatmap/suggestions.ts | 4 +- public/app/plugins/panel/heatmap/utils.ts | 18 ++---- 6 files changed, 67 insertions(+), 43 deletions(-) diff --git a/.betterer.results b/.betterer.results index 74fadb4922c..2638b8fecaf 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5269,9 +5269,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "13"], [0, 0, 0, "Do not use any type assertions.", "14"], [0, 0, 0, "Do not use any type assertions.", "15"], - [0, 0, 0, "Do not use any type assertions.", "16"], - [0, 0, 0, "Do not use any type assertions.", "17"], - [0, 0, 0, "Do not use any type assertions.", "18"] + [0, 0, 0, "Do not use any type assertions.", "16"] ], "public/app/plugins/panel/histogram/Histogram.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index 89c819d13df..a84729f0d83 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -54,13 +54,15 @@ export const HeatmapPanel = ({ [replaceVariables] ); + const palette = useMemo(() => quantizeScheme(options.color, theme), [options.color, theme]); + const info = useMemo(() => { try { - return prepareHeatmapData(data.series, data.annotations, options, theme, getFieldLinksSupplier); + return prepareHeatmapData(data.series, data.annotations, options, palette, theme, getFieldLinksSupplier); } catch (ex) { return { warning: `${ex}` }; } - }, [data.series, data.annotations, options, theme, getFieldLinksSupplier]); + }, [data.series, data.annotations, options, palette, theme, getFieldLinksSupplier]); const facets = useMemo(() => { let exemplarsXFacet: number[] = []; // "Time" field @@ -84,8 +86,6 @@ export const HeatmapPanel = ({ return [null, info.heatmap?.fields.map((f) => f.values), [exemplarsXFacet, exemplarsyFacet]]; }, [info.heatmap, info.exemplars]); - const palette = useMemo(() => quantizeScheme(options.color, theme), [options.color, theme]); - const [hover, setHover] = useState(undefined); const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState(false); const isToolTipOpen = useRef(false); @@ -118,6 +118,7 @@ export const HeatmapPanel = ({ const builder = useMemo(() => { const scaleConfig = dataRef.current?.heatmap?.fields[1].config?.custom ?.scaleDistribution as ScaleDistributionConfig; + return prepConfig({ dataRef, theme, @@ -134,7 +135,6 @@ export const HeatmapPanel = ({ timeZone, getTimeRange: () => timeRangeRef.current, sync, - palette, cellGap: options.cellGap, hideLE: options.filterValues?.le, hideGE: options.filterValues?.ge, @@ -167,8 +167,8 @@ export const HeatmapPanel = ({ diff --git a/public/app/plugins/panel/heatmap/fields.ts b/public/app/plugins/panel/heatmap/fields.ts index a11390b7ef4..87e3593bb83 100644 --- a/public/app/plugins/panel/heatmap/fields.ts +++ b/public/app/plugins/panel/heatmap/fields.ts @@ -21,10 +21,21 @@ import { import { parseSampleValue, sortSeriesByLabel } from 'app/plugins/datasource/prometheus/result_transformer'; import { CellValues, Options } from './types'; -import { boundedMinMax } from './utils'; +import { boundedMinMax, valuesToFills } from './utils'; export interface HeatmapData { heatmap?: DataFrame; // data we will render + heatmapColors?: { + // quantized palette + palette: string[]; + // indices into palette + values: number[]; + + // color scale range + minValue: number; + maxValue: number; + }; + exemplars?: DataFrame; // optionally linked exemplars exemplarColor?: string; @@ -43,10 +54,6 @@ export interface HeatmapData { xLogSplit?: number; yLogSplit?: number; - // color scale range - minValue?: number; - maxValue?: number; - // Print a heatmap cell value display?: (v: number) => string; @@ -58,6 +65,7 @@ export function prepareHeatmapData( frames: DataFrame[], annotations: DataFrame[] | undefined, options: Options, + palette: string[], theme: GrafanaTheme2, getFieldLinks?: (exemplars: DataFrame, field: Field) => (config: ValueLinkConfig) => Array> ): HeatmapData { @@ -74,7 +82,13 @@ export function prepareHeatmapData( } if (options.calculate) { - return getDenseHeatmapData(calculateHeatmapFromData(frames, options.calculation ?? {}), exemplars, options, theme); + return getDenseHeatmapData( + calculateHeatmapFromData(frames, options.calculation ?? {}), + exemplars, + options, + palette, + theme + ); } // Check for known heatmap types @@ -83,8 +97,8 @@ export function prepareHeatmapData( switch (frame.meta?.type) { case DataFrameType.HeatmapCells: return isHeatmapCellsDense(frame) - ? getDenseHeatmapData(frame, exemplars, options, theme) - : getSparseHeatmapData(frame, exemplars, options, theme); + ? getDenseHeatmapData(frame, exemplars, options, palette, theme) + : getSparseHeatmapData(frame, exemplars, options, palette, theme); case DataFrameType.HeatmapRows: rowsHeatmap = frame; // the default format @@ -134,6 +148,7 @@ export function prepareHeatmapData( }), exemplars, options, + palette, theme ); } @@ -142,6 +157,7 @@ const getSparseHeatmapData = ( frame: DataFrame, exemplars: DataFrame | undefined, options: Options, + palette: string[], theme: GrafanaTheme2 ): HeatmapData => { if (frame.meta?.type !== DataFrameType.HeatmapCells || isHeatmapCellsDense(frame)) { @@ -154,11 +170,13 @@ const getSparseHeatmapData = ( // y axis tick label display updateFieldDisplay(frame.fields[1], options.yAxis, theme); + const valueField = frame.fields[3]; + // cell value display - const disp = updateFieldDisplay(frame.fields[3], options.cellValues, theme); + const disp = updateFieldDisplay(valueField, options.cellValues, theme); let [minValue, maxValue] = boundedMinMax( - frame.fields[3].values, + valueField.values, options.color.min, options.color.max, options.filterValues?.le, @@ -167,8 +185,12 @@ const getSparseHeatmapData = ( return { heatmap: frame, - minValue, - maxValue, + heatmapColors: { + palette, + values: valuesToFills(valueField.values, palette, minValue, maxValue), + minValue, + maxValue, + }, exemplars, display: (v) => formattedValueToString(disp(v)), }; @@ -178,6 +200,7 @@ const getDenseHeatmapData = ( frame: DataFrame, exemplars: DataFrame | undefined, options: Options, + palette: string[], theme: GrafanaTheme2 ): HeatmapData => { if (frame.meta?.type !== DataFrameType.HeatmapCells) { @@ -269,6 +292,13 @@ const getDenseHeatmapData = ( const data: HeatmapData = { heatmap: frame, + heatmapColors: { + palette, + values: valuesToFills(valueField.values, palette, minValue, maxValue), + minValue, + maxValue, + }, + exemplars: exemplars?.length ? exemplars : undefined, xBucketSize: xBinIncr, yBucketSize: yBinIncr, @@ -281,9 +311,6 @@ const getDenseHeatmapData = ( xLogSplit: calcX?.scale?.log ? +(calcX?.value ?? '1') : 1, yLogSplit: calcY?.scale?.log ? +(calcY?.value ?? '1') : 1, - minValue, - maxValue, - // TODO: improve heuristic xLayout: xName === 'xMax' ? HeatmapCellLayout.le : xName === 'xMin' ? HeatmapCellLayout.ge : HeatmapCellLayout.unknown, diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index ee4967d26dd..2fb79d39655 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -47,10 +47,15 @@ export const plugin = new PanelPlugin(HeatmapPanel) let isOrdinalY = false; - try { - const v = prepareHeatmapData(context.data, undefined, opts, config.theme2); - isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null; - } catch {} + if (context.data.length > 0) { + try { + // NOTE: this feels like overkill/expensive just to assert if we have an ordinal y + // can probably simplify without doing full dataprep + const palette = quantizeScheme(opts.color, config.theme2); + const v = prepareHeatmapData(context.data, undefined, opts, palette, config.theme2); + isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null; + } catch {} + } let category = ['Heatmap']; diff --git a/public/app/plugins/panel/heatmap/suggestions.ts b/public/app/plugins/panel/heatmap/suggestions.ts index b6c65a7c5cd..e9778229e7e 100644 --- a/public/app/plugins/panel/heatmap/suggestions.ts +++ b/public/app/plugins/panel/heatmap/suggestions.ts @@ -2,6 +2,7 @@ import { VisualizationSuggestionsBuilder } from '@grafana/data'; import { config } from '@grafana/runtime'; import { prepareHeatmapData } from './fields'; +import { quantizeScheme } from './palettes'; import { Options, defaultOptions } from './types'; export class HeatmapSuggestionsSupplier { @@ -18,7 +19,8 @@ export class HeatmapSuggestionsSupplier { return; } - const info = prepareHeatmapData(builder.data.series, undefined, defaultOptions, config.theme2); + const palette = quantizeScheme(defaultOptions.color, config.theme2); + const info = prepareHeatmapData(builder.data.series, undefined, defaultOptions, palette, config.theme2); if (!info || info.warning) { return; } diff --git a/public/app/plugins/panel/heatmap/utils.ts b/public/app/plugins/panel/heatmap/utils.ts index 163f3905577..73466f46b79 100644 --- a/public/app/plugins/panel/heatmap/utils.ts +++ b/public/app/plugins/panel/heatmap/utils.ts @@ -67,7 +67,6 @@ interface PrepConfigOpts { isToolTipOpen: MutableRefObject; timeZone: string; getTimeRange: () => TimeRange; - palette: string[]; exemplarColor: string; cellGap?: number | null; // in css pixels hideLE?: number; @@ -88,7 +87,6 @@ export function prepConfig(opts: PrepConfigOpts) { isToolTipOpen, timeZone, getTimeRange, - palette, cellGap, hideLE, hideGE, @@ -97,6 +95,8 @@ export function prepConfig(opts: PrepConfigOpts) { sync, } = opts; + // console.log(dataRef.current); + const xScaleKey = 'x'; let xScaleUnit = 'time'; let isTime = true; @@ -525,16 +525,8 @@ export function prepConfig(opts: PrepConfigOpts) { ySizeDivisor, disp: { fill: { - values: (u, seriesIdx) => { - let countFacetIdx = !isSparseHeatmap ? 2 : 3; - return valuesToFills( - u.data[seriesIdx][countFacetIdx] as unknown as number[], - palette, - dataRef.current?.minValue!, - dataRef.current?.maxValue! - ); - }, - index: palette, + values: (u, seriesIdx) => dataRef.current?.heatmapColors?.values!, + index: dataRef.current?.heatmapColors?.palette!, }, }, }), @@ -965,7 +957,7 @@ export const boundedMinMax = ( return [minValue, maxValue]; }; -export const valuesToFills = (values: number[], palette: string[], minValue: number, maxValue: number) => { +export const valuesToFills = (values: number[], palette: string[], minValue: number, maxValue: number): number[] => { let range = maxValue - minValue || 1; let paletteSize = palette.length;