diff --git a/packages/grafana-data/src/transformations/transformers/histogram.test.ts b/packages/grafana-data/src/transformations/transformers/histogram.test.ts index 7bbd03397a0..f8aa3f9c991 100644 --- a/packages/grafana-data/src/transformations/transformers/histogram.test.ts +++ b/packages/grafana-data/src/transformations/transformers/histogram.test.ts @@ -1,8 +1,14 @@ import { toDataFrame } from '../../dataframe/processDataFrame'; -import { FieldType } from '../../types/dataFrame'; +import { Field, FieldType } from '../../types/dataFrame'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; -import { histogramTransformer, buildHistogram, histogramFieldsToFrame } from './histogram'; +import { + histogramTransformer, + buildHistogram, + histogramFieldsToFrame, + HistogramFields, + joinHistograms, +} from './histogram'; describe('histogram frames frames', () => { beforeAll(() => { @@ -280,3 +286,110 @@ describe('histogram frames frames', () => { `); }); }); + +describe('joinHistograms', () => { + type TestHistogram = { + xMin: number[]; + xMax: number[]; + counts: number[][]; + }; + + function toField(name: string, values: number[]): Field { + return { + config: {}, + name, + type: FieldType.number, + values, + }; + } + + function testHistogramToHistogram(test: TestHistogram): HistogramFields { + return { + xMin: toField('xMin', test.xMin), + xMax: toField('xMax', test.xMax), + counts: test.counts.map((values) => toField(`count`, values)), + }; + } + + type TestCase = { + name: string; + histograms: TestHistogram[]; + expected: TestHistogram; + }; + + const testCases: TestCase[] = [ + { + name: 'just one histogram', + histograms: [ + { + xMin: [1, 2, 3], + xMax: [2, 3, 4], + counts: [[1, 2, 3]], + }, + ], + expected: { + xMin: [1, 2, 3], + xMax: [2, 3, 4], + counts: [[1, 2, 3]], + }, + }, + { + name: 'two histograms with same bucket sizes', + histograms: [ + { + xMin: [1, 3, 4], + xMax: [2, 4, 5], + counts: [[1, 2, 3]], + }, + { + xMin: [1, 3, 4], + xMax: [2, 4, 5], + counts: [[4, 5, 6]], + }, + ], + expected: { + xMin: [1, 3, 4], + xMax: [2, 4, 5], + counts: [ + [1, 2, 3], + [4, 5, 6], + ], + }, + }, + { + name: 'two histograms with same bucket sizes but counts in some different buckets', + histograms: [ + { + xMin: [1, 3, 4], + xMax: [2, 4, 5], + counts: [[1, 2, 3]], + }, + { + xMin: [2, 3, 6], + xMax: [3, 4, 7], + counts: [[4, 5, 6]], + }, + ], + expected: { + xMin: [1, 2, 3, 4, 6], + xMax: [2, 3, 4, 5, 7], + counts: [ + [1, 0, 2, 3, 0], + [0, 4, 5, 0, 6], + ], + }, + }, + ]; + + testCases.forEach((tc) => { + it(tc.name, () => { + const result = joinHistograms(tc.histograms.map(testHistogramToHistogram)); + + expect({ + xMin: result.xMin.values, + xMax: result.xMax.values, + counts: result.counts.map((f) => f.values), + }).toEqual(tc.expected); + }); + }); +}); diff --git a/packages/grafana-data/src/transformations/transformers/histogram.ts b/packages/grafana-data/src/transformations/transformers/histogram.ts index 7e5def97a2f..00549ad07ae 100644 --- a/packages/grafana-data/src/transformations/transformers/histogram.ts +++ b/packages/grafana-data/src/transformations/transformers/histogram.ts @@ -1,5 +1,6 @@ import { map } from 'rxjs/operators'; +import { outerJoinDataFrames } from '../..'; import { getDisplayProcessor } from '../../field/displayProcessor'; import { createTheme } from '../../themes/createTheme'; import { GrafanaTheme2 } from '../../themes/types'; @@ -588,3 +589,63 @@ export function histogramFieldsToFrame(info: HistogramFields, theme?: GrafanaThe refId: `${DataTransformerID.histogram}`, }; } + +/** + * + * Join multiple histograms into a histogram with multiple counts. + * Useful eg if you want to overlay them for comparison. + * + * This is needed because histogram results from database + * will have buckets omitted for 0 counts, but when joining multiple histograms + * we need to fill in the 0 values for missing buckets. + * + * Returns field configs of the first provided histogram. + * @alpha + */ + +export function joinHistograms(histograms: HistogramFields[]): HistogramFields { + if (histograms.length === 1) { + return histograms[0]; + } + + let joined = outerJoinDataFrames({ + frames: histograms.map((h) => ({ + length: h.xMax.values.length, + fields: [h.xMax, h.xMin, ...h.counts], + })), + joinBy: (field) => field.name === 'xMax', + })!; + + let xMaxField: Field | null = null; + let xMinField: Field | null = null; + let countFields: Field[] = []; + + // merge all xMin fields into first xMin field + // and default all count fields to 0 + joined.fields.forEach((f) => { + if (f.name === 'xMax') { + xMaxField = f; + } else if (f.name === 'xMin') { + if (xMinField == null) { + xMinField = f; + } else { + for (let i = 0; i < f.values.length; i++) { + xMinField.values[i] ??= f.values[i]; + } + } + } else { + countFields.push({ + ...f, + values: f.values.map((v) => v ?? 0), + }); + } + }); + + const result: HistogramFields = { + xMin: xMinField!, + xMax: xMaxField!, + counts: countFields, + }; + + return result; +} diff --git a/public/app/plugins/panel/histogram/HistogramPanel.tsx b/public/app/plugins/panel/histogram/HistogramPanel.tsx index 2c2805069a8..03938736458 100644 --- a/public/app/plugins/panel/histogram/HistogramPanel.tsx +++ b/public/app/plugins/panel/histogram/HistogramPanel.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; -import { PanelProps, buildHistogram, cacheFieldDisplayNames, getHistogramFields } from '@grafana/data'; -import { histogramFieldsToFrame } from '@grafana/data/src/transformations/transformers/histogram'; +import { DataFrameType, PanelProps, buildHistogram, cacheFieldDisplayNames, getHistogramFields } from '@grafana/data'; +import { histogramFieldsToFrame, joinHistograms } from '@grafana/data/src/transformations/transformers/histogram'; import { TooltipDisplayMode, TooltipPlugin2, useTheme2 } from '@grafana/ui'; import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; @@ -34,10 +34,16 @@ export const HistogramPanel = ({ data, options, width, height }: Props) => { cacheFieldDisplayNames(data.series); - if (data.series.length === 1) { - const info = getHistogramFields(data.series[0]); - if (info) { - return histogramFieldsToFrame(info); + if ( + data.series.length === 1 || + data.series.every( + (frame) => frame.meta?.type === DataFrameType.HeatmapCells || frame.meta?.type === DataFrameType.HeatmapRows + ) + ) { + const histograms = data.series.map((frame) => getHistogramFields(frame)).filter((hist) => hist != null); + + if (histograms.length) { + return histogramFieldsToFrame(joinHistograms(histograms), theme); } } const hist = buildHistogram(data.series, options);