Histogram: Handle multiple native histograms (#98404)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Domas
2025-02-24 07:00:18 +02:00
committed by GitHub
co-authored by Leon Sorokin
parent 48029e2ed9
commit 8f9972a509
3 changed files with 188 additions and 8 deletions
@@ -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);
});
});
});
@@ -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;
}
@@ -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);