From 74bcfb284c16f99518d48d61c09e563b28ef6502 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 18 Jun 2024 21:51:20 -0500 Subject: [PATCH] Heatmap: Use dashboard timeRange for auto-sizing x buckets (#89382) --- .../transformers/calculateHeatmap/heatmap.ts | 26 +++++++++++---- .../plugins/panel/heatmap/HeatmapPanel.tsx | 12 +++++-- public/app/plugins/panel/heatmap/fields.ts | 32 +++++++++++++------ public/app/plugins/panel/heatmap/module.tsx | 7 +++- .../app/plugins/panel/heatmap/suggestions.ts | 7 +++- 5 files changed, 63 insertions(+), 21 deletions(-) diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index 1de1d638361..538b2c9a97a 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -15,6 +15,7 @@ import { durationToMilliseconds, parseDuration, TransformationApplicabilityLevels, + TimeRange, } from '@grafana/data'; import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames'; 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 const { xField, yField, xs, ys } = findHeatmapFields(frames); @@ -329,6 +337,9 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa ySize: yBucketsCfg.value ? +yBucketsCfg.value : undefined, yLog: scaleDistribution?.type === ScaleDistribution.Log ? (scaleDistribution?.log as 2 | 10 | undefined) : undefined, + + xMin: options.timeRange?.from.valueOf(), + xMax: options.timeRange?.to.valueOf(), }); const frame = { @@ -460,20 +471,23 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) { let ySorted = opts?.ySorted ?? false; // 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 maxX = xSorted ? xs[len - 1] : -Infinity; + let maxX = opts?.xMax ?? (xSorted ? xs[len - 1] : -Infinity); let maxY = ySorted ? ys[len - 1] : -Infinity; 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++) { - if (!xSorted) { + if (!xSorted && !withPredefX) { minX = Math.min(minX, xs[i]); maxX = Math.max(maxX, xs[i]); } - if (!ySorted) { + if (!ySorted && !withPredefY) { if (!yExp || ys[i] > 0) { minY = Math.min(minY, ys[i]); maxY = Math.max(maxY, ys[i]); @@ -500,7 +514,6 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) { } 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); // nice-ify let xIncrs = opts?.xTime ? niceTimeIncrs : niceLinearIncrs; @@ -509,7 +522,6 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) { } 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); // nice-ify let yIncrs = opts?.yTime ? niceTimeIncrs : niceLinearIncrs; diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index f3cde37e399..b7ce5ab2be9 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -59,11 +59,19 @@ export const HeatmapPanel = ({ const info = useMemo(() => { 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) { return { warning: `${ex}` }; } - }, [data.series, data.annotations, options, palette, theme, replaceVariables]); + }, [data.series, data.annotations, options, palette, theme, replaceVariables, timeRange]); const facets = useMemo(() => { let exemplarsXFacet: number[] | undefined = []; // "Time" field diff --git a/public/app/plugins/panel/heatmap/fields.ts b/public/app/plugins/panel/heatmap/fields.ts index aeb79a3538f..534d963e92e 100644 --- a/public/app/plugins/panel/heatmap/fields.ts +++ b/public/app/plugins/panel/heatmap/fields.ts @@ -10,6 +10,7 @@ import { GrafanaTheme2, InterpolateFunction, outerJoinDataFrames, + TimeRange, ValueFormatter, } from '@grafana/data'; import { parseSampleValue, sortSeriesByLabel } from '@grafana/prometheus'; @@ -65,14 +66,25 @@ export interface HeatmapData { warning?: string; } -export function prepareHeatmapData( - frames: DataFrame[], - annotations: DataFrame[] | undefined, - options: Options, - palette: string[], - theme: GrafanaTheme2, - replaceVariables: InterpolateFunction = (v) => v -): HeatmapData { +interface PrepareHeatmapDataOptions { + frames: DataFrame[]; + annotations?: DataFrame[]; + options: Options; + palette: string[]; + theme: GrafanaTheme2; + replaceVariables?: InterpolateFunction; + timeRange?: TimeRange; +} + +export function prepareHeatmapData({ + frames, + annotations, + options, + palette, + theme, + replaceVariables = (v) => v, + timeRange, +}: PrepareHeatmapDataOptions): HeatmapData { if (!frames?.length) { return {}; } @@ -104,7 +116,7 @@ export function prepareHeatmapData( } return getDenseHeatmapData( - calculateHeatmapFromData(frames, optionsCopy.calculation ?? {}), + calculateHeatmapFromData(frames, { ...options.calculation, timeRange }), exemplars, optionsCopy, palette, @@ -113,7 +125,7 @@ export function prepareHeatmapData( } return getDenseHeatmapData( - calculateHeatmapFromData(frames, options.calculation ?? {}), + calculateHeatmapFromData(frames, { ...options.calculation, timeRange }), exemplars, options, palette, diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index 8c35609950a..660e71e7261 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -53,7 +53,12 @@ export const plugin = new PanelPlugin(HeatmapPanel) // 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); + const v = prepareHeatmapData({ + frames: context.data, + options: opts, + palette, + theme: config.theme2, + }); isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null; } catch {} } diff --git a/public/app/plugins/panel/heatmap/suggestions.ts b/public/app/plugins/panel/heatmap/suggestions.ts index e9778229e7e..f49de761e85 100644 --- a/public/app/plugins/panel/heatmap/suggestions.ts +++ b/public/app/plugins/panel/heatmap/suggestions.ts @@ -20,7 +20,12 @@ export class HeatmapSuggestionsSupplier { } 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) { return; }