Heatmap: Build cell colors during data prep instead of render (#71509)

This commit is contained in:
Leon Sorokin 2023-07-12 18:23:30 -05:00 committed by GitHub
parent c3a0207615
commit 46c5e5417b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 67 additions and 43 deletions

View File

@ -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"],

View File

@ -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<HeatmapHoverEvent | undefined>(undefined);
const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
const isToolTipOpen = useRef<boolean>(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 = ({
<ColorScale
hoverValue={hoverValue}
colorPalette={palette}
min={dataRef.current.minValue!}
max={dataRef.current.maxValue!}
min={dataRef.current.heatmapColors?.minValue!}
max={dataRef.current.heatmapColors?.maxValue!}
display={info.display}
/>
</div>

View File

@ -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<LinkModel<Field>>
): 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,

View File

@ -47,10 +47,15 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(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'];

View File

@ -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;
}

View File

@ -67,7 +67,6 @@ interface PrepConfigOpts {
isToolTipOpen: MutableRefObject<boolean>;
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;