diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index 080f289aa6f..3e01d84e70a 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -47,12 +47,11 @@ export function sortAscStrInf(aName?: string | null, bName?: string | null) { /** Given existing buckets, create a values style frame */ // Assumes frames have already been sorted ASC and de-accumulated. -export function createHeatmapFromBuckets(frames: DataFrame[]): DataFrame { - // assumes all Time fields are identical +export function bucketsToScanlines(frame: DataFrame): DataFrame { // TODO: handle null-filling w/ fields[0].config.interval? - const xField = frames[0].fields[0]; + const xField = frame.fields[0]; const xValues = xField.values.toArray(); - const yField = frames[0].fields[1]; + const yField = frame.fields[1]; // similar to initBins() below const len = xValues.length * frames.length; @@ -60,7 +59,7 @@ export function createHeatmapFromBuckets(frames: DataFrame[]): DataFrame { const ys = new Array(len); const counts2 = new Array(len); - const counts = frames.map((frame) => frame.fields[1].values.toArray().slice()); + const counts = frame.fields.slice(1).map((field) => field.values.toArray().slice()); // transpose counts.forEach((bucketCounts, bi) => { @@ -69,22 +68,7 @@ export function createHeatmapFromBuckets(frames: DataFrame[]): DataFrame { } }); - const bucketBounds = frames.map((frame, i) => { - return i; // until we have y ordinal scales working for facets/scatter - - /* - let bound: number; - - if (frame.name === '+Inf') { - // TODO: until we have labeled y, treat +Inf as previous bucket + 10% - bound = +(frames[i - 1].name ?? 0) * 1.1; - } else { - bound = +(frame.name ?? 0); - } - - return bound; - */ - }); + const bucketBounds = Array.from({ length: frame.fields.length - 1 }, (v, i) => i); // fill flat/repeating array for (let i = 0, yi = 0, xi = 0; i < len; yi = ++i % bucketBounds.length) { diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index 5c819fb679a..4d79fb5527f 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -1,4 +1,4 @@ -import { map, cloneDeep } from 'lodash'; +import { cloneDeep } from 'lodash'; import { of, throwError } from 'rxjs'; import { CoreApp, @@ -6,6 +6,7 @@ import { DataQueryResponseData, DataSourceInstanceSettings, dateTime, + Field, getFieldDisplayName, LoadingState, toDataFrame, @@ -310,8 +311,8 @@ describe('PrometheusDatasource', () => { await expect(ds.query(query)).toEmitValuesWith((result) => { const results = result[0].data; expect(results[0].fields[1].values.toArray()).toEqual([10, 10]); - expect(results[1].fields[1].values.toArray()).toEqual([10, 0]); - expect(results[2].fields[1].values.toArray()).toEqual([5, 0]); + expect(results[0].fields[2].values.toArray()).toEqual([10, 0]); + expect(results[0].fields[3].values.toArray()).toEqual([5, 0]); }); }); @@ -352,7 +353,7 @@ describe('PrometheusDatasource', () => { ds.performTimeSeriesQuery = jest.fn().mockReturnValue(of(responseMock)); await expect(ds.query(query)).toEmitValuesWith((result) => { - const seriesLabels = map(result[0].data, 'name'); + const seriesLabels = result[0].data[0].fields.slice(1).map((field: Field) => getFieldDisplayName(field)); expect(seriesLabels).toEqual(expected); }); }); diff --git a/public/app/plugins/datasource/prometheus/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/result_transformer.test.ts index 57875d6e6ab..d61dee2fbad 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.test.ts @@ -246,10 +246,10 @@ describe('Prometheus Result Transformer', () => { } as unknown as DataQueryResponse; const series = transformV2(response, options, {}); - expect(series.data[0].fields.length).toEqual(2); + expect(series.data[0].fields.length).toEqual(4); expect(series.data[0].fields[1].values.toArray()).toEqual([10, 10, 0]); - expect(series.data[1].fields[1].values.toArray()).toEqual([10, 0, 30]); - expect(series.data[2].fields[1].values.toArray()).toEqual([10, 0, 10]); + expect(series.data[0].fields[2].values.toArray()).toEqual([10, 0, 30]); + expect(series.data[0].fields[3].values.toArray()).toEqual([10, 0, 10]); }); }); describe('transformDFToTable', () => { @@ -513,10 +513,8 @@ describe('Prometheus Result Transformer', () => { const result = transform({ data: response } as any, { query: options, target: options } as any); expect(result[0].fields[0].values.toArray()).toEqual([1445000010000, 1445000020000, 1445000030000]); expect(result[0].fields[1].values.toArray()).toEqual([10, 10, 0]); - expect(result[1].fields[0].values.toArray()).toEqual([1445000010000, 1445000020000, 1445000030000]); - expect(result[1].fields[1].values.toArray()).toEqual([10, 0, 30]); - expect(result[2].fields[0].values.toArray()).toEqual([1445000010000, 1445000020000, 1445000030000]); - expect(result[2].fields[1].values.toArray()).toEqual([10, 0, 10]); + expect(result[0].fields[2].values.toArray()).toEqual([10, 0, 30]); + expect(result[0].fields[3].values.toArray()).toEqual([10, 0, 10]); }); it('should handle missing datapoints', () => { @@ -546,8 +544,8 @@ describe('Prometheus Result Transformer', () => { ]); const result = transform({ data: response } as any, { query: options, target: options } as any); expect(result[0].fields[1].values.toArray()).toEqual([1, 2]); - expect(result[1].fields[1].values.toArray()).toEqual([1, 3, 1]); - expect(result[2].fields[1].values.toArray()).toEqual([1, 2]); + expect(result[0].fields[2].values.toArray()).toEqual([1, 3, 1]); + expect(result[0].fields[3].values.toArray()).toEqual([1, 2]); }); }); diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index 80b29d4292c..d3d5b1f9595 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -17,6 +17,7 @@ import { DataQueryRequest, PreferredVisualisationType, CoreApp, + DataFrameType, } from '@grafana/data'; import { FetchResponse, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime'; import { partition, groupBy } from 'lodash'; @@ -74,7 +75,10 @@ export function transformV2( const [heatmapResults, framesWithoutTableAndHeatmaps] = partition(framesWithoutTable, (df) => isHeatmapResult(df, request) ); - const processedHeatmapFrames = transformToHistogramOverTime(heatmapResults.sort(sortSeriesByLabel)); + + const processedHeatmapFrames = mergeHeatmapFrames( + transformToHistogramOverTime(heatmapResults.sort(sortSeriesByLabel)) + ); const [exemplarFrames, framesWithoutTableHeatmapsAndExemplars] = partition( framesWithoutTableAndHeatmaps, @@ -272,9 +276,7 @@ export function transform( // When format is heatmap use the already created data frames and transform it more if (options.format === 'heatmap') { - dataFrame.sort(sortSeriesByLabel); - const seriesList = transformToHistogramOverTime(dataFrame); - return seriesList; + return mergeHeatmapFrames(transformToHistogramOverTime(dataFrame.sort(sortSeriesByLabel))); } // Return matrix or vector result as DataFrame[] @@ -533,6 +535,33 @@ export function getOriginalMetricName(labelData: { [key: string]: string }) { return `${metricName}{${labelPart}}`; } +function mergeHeatmapFrames(frames: DataFrame[]): DataFrame[] { + if (frames.length === 0) { + return []; + } + + const timeField = frames[0].fields.find((field) => field.type === FieldType.time)!; + const countFields = frames.map((frame) => { + let field = frame.fields.find((field) => field.type === FieldType.number)!; + + return { + ...field, + name: field.config.displayNameFromDS!, + }; + }); + + return [ + { + ...frames[0], + meta: { + ...frames[0].meta, + type: DataFrameType.HeatmapBuckets, + }, + fields: [timeField!, ...countFields], + }, + ]; +} + function transformToHistogramOverTime(seriesList: DataFrame[]) { /* t1 = timestamp1, t2 = timestamp2 etc. t1 t2 t3 t1 t2 t3 diff --git a/public/app/plugins/panel/heatmap-new/fields.ts b/public/app/plugins/panel/heatmap-new/fields.ts index e1c0cf6f197..63002910c69 100644 --- a/public/app/plugins/panel/heatmap-new/fields.ts +++ b/public/app/plugins/panel/heatmap-new/fields.ts @@ -1,5 +1,12 @@ -import { DataFrame, DataFrameType, getDisplayProcessor, GrafanaTheme2 } from '@grafana/data'; -import { calculateHeatmapFromData, createHeatmapFromBuckets } from 'app/features/transformers/calculateHeatmap/heatmap'; +import { + DataFrame, + DataFrameType, + FieldType, + getDisplayProcessor, + getFieldDisplayName, + GrafanaTheme2, +} from '@grafana/data'; +import { calculateHeatmapFromData, bucketsToScanlines } from 'app/features/transformers/calculateHeatmap/heatmap'; import { HeatmapSourceMode, PanelOptions } from './models.gen'; export const enum BucketLayout { @@ -42,26 +49,23 @@ export function prepareHeatmapData( } // Find a well defined heatmap - let heatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapScanlines); - if (heatmap) { - return getHeatmapData(heatmap, theme); + let scanlinesHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapScanlines); + if (scanlinesHeatmap) { + return getHeatmapData(scanlinesHeatmap, theme); + } + + let bucketsHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapBuckets); + if (bucketsHeatmap) { + return { + yAxisValues: frames[0].fields.flatMap((field) => + field.type === FieldType.number ? getFieldDisplayName(field) : [] + ), + ...getHeatmapData(bucketsToScanlines(bucketsHeatmap), theme), + }; } if (source === HeatmapSourceMode.Data) { - // TODO: check for names xMin, yMin etc... - return getHeatmapData(createHeatmapFromBuckets(frames), theme); - } - - // detect a frame-per-bucket heatmap frame - // TODO: improve heuristic? infer from fields[1].labels.le === '+Inf' ? - if (frames[0].meta?.custom?.resultType === 'matrix' && frames.some((f) => f.name?.startsWith('+Inf'))) { - // already done by the Prometheus datasource frontend - //frames = prepBucketFrames(frames); - - return { - yAxisValues: frames.map((f) => f.name ?? null), - ...getHeatmapData(createHeatmapFromBuckets(frames), theme), - }; + return getHeatmapData(bucketsToScanlines(frames[0]), theme); } // TODO, check for error etc