Heatmap: Use dashboard timeRange for auto-sizing x buckets (#89382)

This commit is contained in:
Leon Sorokin 2024-06-18 21:51:20 -05:00 committed by GitHub
parent 7c69f3657b
commit 74bcfb284c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 63 additions and 21 deletions

View File

@ -15,6 +15,7 @@ import {
durationToMilliseconds, durationToMilliseconds,
parseDuration, parseDuration,
TransformationApplicabilityLevels, TransformationApplicabilityLevels,
TimeRange,
} from '@grafana/data'; } from '@grafana/data';
import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
@ -292,7 +293,14 @@ export function prepBucketFrames(frames: DataFrame[]): DataFrame[] {
})); }));
} }
export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCalculationOptions): DataFrame { interface HeatmapCalculationOptionsWithTimeRange extends HeatmapCalculationOptions {
timeRange?: TimeRange;
}
export function calculateHeatmapFromData(
frames: DataFrame[],
options: HeatmapCalculationOptionsWithTimeRange
): DataFrame {
// Find fields in the heatmap // Find fields in the heatmap
const { xField, yField, xs, ys } = findHeatmapFields(frames); const { xField, yField, xs, ys } = findHeatmapFields(frames);
@ -329,6 +337,9 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa
ySize: yBucketsCfg.value ? +yBucketsCfg.value : undefined, ySize: yBucketsCfg.value ? +yBucketsCfg.value : undefined,
yLog: yLog:
scaleDistribution?.type === ScaleDistribution.Log ? (scaleDistribution?.log as 2 | 10 | undefined) : undefined, scaleDistribution?.type === ScaleDistribution.Log ? (scaleDistribution?.log as 2 | 10 | undefined) : undefined,
xMin: options.timeRange?.from.valueOf(),
xMax: options.timeRange?.to.valueOf(),
}); });
const frame = { const frame = {
@ -460,20 +471,23 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
let ySorted = opts?.ySorted ?? false; let ySorted = opts?.ySorted ?? false;
// find x and y limits to pre-compute buckets struct // find x and y limits to pre-compute buckets struct
let minX = xSorted ? xs[0] : Infinity; let minX = opts?.xMin ?? (xSorted ? xs[0] : Infinity);
let minY = ySorted ? ys[0] : Infinity; let minY = ySorted ? ys[0] : Infinity;
let maxX = xSorted ? xs[len - 1] : -Infinity; let maxX = opts?.xMax ?? (xSorted ? xs[len - 1] : -Infinity);
let maxY = ySorted ? ys[len - 1] : -Infinity; let maxY = ySorted ? ys[len - 1] : -Infinity;
let yExp = opts?.yLog; let yExp = opts?.yLog;
let withPredefX = opts?.xMin != null && opts?.xMax != null;
let withPredefY = opts?.yMin != null && opts?.yMax != null;
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
if (!xSorted) { if (!xSorted && !withPredefX) {
minX = Math.min(minX, xs[i]); minX = Math.min(minX, xs[i]);
maxX = Math.max(maxX, xs[i]); maxX = Math.max(maxX, xs[i]);
} }
if (!ySorted) { if (!ySorted && !withPredefY) {
if (!yExp || ys[i] > 0) { if (!yExp || ys[i] > 0) {
minY = Math.min(minY, ys[i]); minY = Math.min(minY, ys[i]);
maxY = Math.max(maxY, ys[i]); maxY = Math.max(maxY, ys[i]);
@ -500,7 +514,6 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
} }
if (xMode === HeatmapCalculationMode.Count) { if (xMode === HeatmapCalculationMode.Count) {
// TODO: optionally use view range min/max instead of data range for bucket sizing
let approx = (maxX - minX) / Math.max(xBinIncr - 1, 1); let approx = (maxX - minX) / Math.max(xBinIncr - 1, 1);
// nice-ify // nice-ify
let xIncrs = opts?.xTime ? niceTimeIncrs : niceLinearIncrs; let xIncrs = opts?.xTime ? niceTimeIncrs : niceLinearIncrs;
@ -509,7 +522,6 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
} }
if (yMode === HeatmapCalculationMode.Count) { if (yMode === HeatmapCalculationMode.Count) {
// TODO: optionally use view range min/max instead of data range for bucket sizing
let approx = (maxY - minY) / Math.max(yBinIncr - 1, 1); let approx = (maxY - minY) / Math.max(yBinIncr - 1, 1);
// nice-ify // nice-ify
let yIncrs = opts?.yTime ? niceTimeIncrs : niceLinearIncrs; let yIncrs = opts?.yTime ? niceTimeIncrs : niceLinearIncrs;

View File

@ -59,11 +59,19 @@ export const HeatmapPanel = ({
const info = useMemo(() => { const info = useMemo(() => {
try { try {
return prepareHeatmapData(data.series, data.annotations, options, palette, theme, replaceVariables); return prepareHeatmapData({
frames: data.series,
annotations: data.annotations,
options,
palette,
theme,
replaceVariables,
timeRange,
});
} catch (ex) { } catch (ex) {
return { warning: `${ex}` }; return { warning: `${ex}` };
} }
}, [data.series, data.annotations, options, palette, theme, replaceVariables]); }, [data.series, data.annotations, options, palette, theme, replaceVariables, timeRange]);
const facets = useMemo(() => { const facets = useMemo(() => {
let exemplarsXFacet: number[] | undefined = []; // "Time" field let exemplarsXFacet: number[] | undefined = []; // "Time" field

View File

@ -10,6 +10,7 @@ import {
GrafanaTheme2, GrafanaTheme2,
InterpolateFunction, InterpolateFunction,
outerJoinDataFrames, outerJoinDataFrames,
TimeRange,
ValueFormatter, ValueFormatter,
} from '@grafana/data'; } from '@grafana/data';
import { parseSampleValue, sortSeriesByLabel } from '@grafana/prometheus'; import { parseSampleValue, sortSeriesByLabel } from '@grafana/prometheus';
@ -65,14 +66,25 @@ export interface HeatmapData {
warning?: string; warning?: string;
} }
export function prepareHeatmapData( interface PrepareHeatmapDataOptions {
frames: DataFrame[], frames: DataFrame[];
annotations: DataFrame[] | undefined, annotations?: DataFrame[];
options: Options, options: Options;
palette: string[], palette: string[];
theme: GrafanaTheme2, theme: GrafanaTheme2;
replaceVariables: InterpolateFunction = (v) => v replaceVariables?: InterpolateFunction;
): HeatmapData { timeRange?: TimeRange;
}
export function prepareHeatmapData({
frames,
annotations,
options,
palette,
theme,
replaceVariables = (v) => v,
timeRange,
}: PrepareHeatmapDataOptions): HeatmapData {
if (!frames?.length) { if (!frames?.length) {
return {}; return {};
} }
@ -104,7 +116,7 @@ export function prepareHeatmapData(
} }
return getDenseHeatmapData( return getDenseHeatmapData(
calculateHeatmapFromData(frames, optionsCopy.calculation ?? {}), calculateHeatmapFromData(frames, { ...options.calculation, timeRange }),
exemplars, exemplars,
optionsCopy, optionsCopy,
palette, palette,
@ -113,7 +125,7 @@ export function prepareHeatmapData(
} }
return getDenseHeatmapData( return getDenseHeatmapData(
calculateHeatmapFromData(frames, options.calculation ?? {}), calculateHeatmapFromData(frames, { ...options.calculation, timeRange }),
exemplars, exemplars,
options, options,
palette, palette,

View File

@ -53,7 +53,12 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(HeatmapPanel)
// NOTE: this feels like overkill/expensive just to assert if we have an ordinal y // NOTE: this feels like overkill/expensive just to assert if we have an ordinal y
// can probably simplify without doing full dataprep // can probably simplify without doing full dataprep
const palette = quantizeScheme(opts.color, config.theme2); const palette = quantizeScheme(opts.color, config.theme2);
const v = prepareHeatmapData(context.data, undefined, opts, palette, config.theme2); const v = prepareHeatmapData({
frames: context.data,
options: opts,
palette,
theme: config.theme2,
});
isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null; isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null;
} catch {} } catch {}
} }

View File

@ -20,7 +20,12 @@ export class HeatmapSuggestionsSupplier {
} }
const palette = quantizeScheme(defaultOptions.color, config.theme2); const palette = quantizeScheme(defaultOptions.color, config.theme2);
const info = prepareHeatmapData(builder.data.series, undefined, defaultOptions, palette, config.theme2); const info = prepareHeatmapData({
frames: builder.data.series,
options: defaultOptions,
palette,
theme: config.theme2,
});
if (!info || info.warning) { if (!info || info.warning) {
return; return;
} }