Histogram: Handle multiple native histograms (#98404)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user