Histogram: Render heatmap-cells and heatmap-rows frames (#77111)

This commit is contained in:
Leon Sorokin 2023-10-26 23:23:01 -05:00 committed by GitHub
parent 72a085ea20
commit 45aa58c4a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 160 additions and 40 deletions

View File

@ -7263,9 +7263,10 @@ exports[`better eslint`] = {
],
"public/app/plugins/panel/histogram/Histogram.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/plugins/panel/live/LiveChannelEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -183,6 +183,95 @@ export interface HistogramFields {
* @alpha
*/
export function getHistogramFields(frame: DataFrame): HistogramFields | undefined {
// we ignore xMax (time field) and sum all counts together for each found bucket
if (frame.meta?.type === DataFrameType.HeatmapCells) {
// we assume uniform bucket size for now
// we assume xMax, yMin, yMax fields
let yMinField = frame.fields.find((f) => f.name === 'yMin')!;
let yMaxField = frame.fields.find((f) => f.name === 'yMax')!;
let countField = frame.fields.find((f) => f.name === 'count')!;
let uniqueMaxs = [...new Set(yMaxField.values)].sort((a, b) => a - b);
let uniqueMins = [...new Set(yMinField.values)].sort((a, b) => a - b);
let countsByMax = new Map<number, number>();
uniqueMaxs.forEach((max) => countsByMax.set(max, 0));
for (let i = 0; i < yMaxField.values.length; i++) {
let max = yMaxField.values[i];
countsByMax.set(max, countsByMax.get(max) + countField.values[i]);
}
let fields = {
xMin: {
...yMinField,
name: 'xMin',
values: uniqueMins,
},
xMax: {
...yMaxField,
name: 'xMax',
values: uniqueMaxs,
},
counts: [
{
...countField,
values: [...countsByMax.values()],
},
],
};
return fields;
} else if (frame.meta?.type === DataFrameType.HeatmapRows) {
// assumes le
// tick label strings (will be ordinal-ized)
let minVals: string[] = [];
let maxVals: string[] = [];
// sums of all timstamps per bucket
let countVals: number[] = [];
let minVal = '0';
frame.fields.forEach((f) => {
if (f.type === FieldType.number) {
let countsSum = f.values.reduce((acc, v) => acc + v, 0);
countVals.push(countsSum);
minVals.push(minVal);
maxVals.push((minVal = f.name));
}
});
// fake extra value for +Inf (for x scale ranging since bars are right-aligned)
countVals.push(0);
minVals.push(minVal);
maxVals.push(minVal);
let fields = {
xMin: {
...frame.fields[1],
name: 'xMin',
type: FieldType.string,
values: minVals,
},
xMax: {
...frame.fields[1],
name: 'xMax',
type: FieldType.string,
values: maxVals,
},
counts: [
{
...frame.fields[1],
name: 'count',
type: FieldType.number,
values: countVals,
},
],
};
return fields;
}
let xMin: Field | undefined = undefined;
let xMax: Field | undefined = undefined;
const counts: Field[] = [];

View File

@ -314,6 +314,12 @@ export function prepConfig(opts: PrepConfigOpts) {
// sparse already accounts for le/ge by explicit yMin & yMax cell bounds, so no need to expand y range
isSparseHeatmap
? (u, dataMin, dataMax) => {
// ...but uPlot currently only auto-ranges from the yMin facet data, so we have to grow by 1 extra factor
// @ts-ignore
let bucketFactor = u.data[1][2][0] / u.data[1][1][0];
dataMax *= bucketFactor;
let scaleMin: number | null, scaleMax: number | null;
[scaleMin, scaleMax] = shouldUseLogScale
@ -900,8 +906,8 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) {
xSize = Math.max(1, xSize - cellGap);
ySize = Math.max(1, ySize - cellGap);
let x = xMaxPx;
let y = yMinPx;
let x = xMaxPx - cellGap / 2 - xSize;
let y = yMaxPx + cellGap / 2;
let fillPath = fillPaths[fills[i]];

View File

@ -3,6 +3,7 @@ import uPlot, { AlignedData } from 'uplot';
import {
DataFrame,
FieldType,
formattedValueToString,
getFieldColorModeForField,
getFieldSeriesColor,
@ -47,7 +48,12 @@ export interface HistogramProps extends Themeable2 {
export function getBucketSize(frame: DataFrame) {
// assumes BucketMin is fields[0] and BucktMax is fields[1]
return frame.fields[1].values[0] - frame.fields[0].values[0];
return frame.fields[0].type === FieldType.string ? 1 : frame.fields[1].values[0] - frame.fields[0].values[0];
}
export function getBucketSize1(frame: DataFrame) {
// assumes BucketMin is fields[0] and BucktMax is fields[1]
return frame.fields[0].type === FieldType.string ? 1 : frame.fields[1].values[1] - frame.fields[0].values[1];
}
const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => {
@ -59,8 +65,15 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => {
let builder = new UPlotConfigBuilder();
let isOrdinalX = frame.fields[0].type === FieldType.string;
// assumes BucketMin is fields[0] and BucktMax is fields[1]
let bucketSize = getBucketSize(frame);
let bucketSize1 = getBucketSize1(frame);
let bucketFactor = bucketSize1 / bucketSize;
let useLogScale = bucketSize1 !== bucketSize; // (imperfect floats)
// splits shifter, to ensure splits always start at first bucket
let xSplits: uPlot.Axis.Splits = (u, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace) => {
@ -84,35 +97,44 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => {
builder.addScale({
scaleKey: 'x', // bukkits
isTime: false,
distribution: ScaleDistribution.Linear,
distribution: isOrdinalX
? ScaleDistribution.Ordinal
: useLogScale
? ScaleDistribution.Log
: ScaleDistribution.Linear,
log: 2,
orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right,
range: (u, wantedMin, wantedMax) => {
// these settings will prevent zooming, probably okay?
if (xScaleMin != null) {
wantedMin = xScaleMin;
}
if (xScaleMax != null) {
wantedMax = xScaleMax;
}
range: useLogScale
? (u, wantedMin, wantedMax) => {
return uPlot.rangeLog(wantedMin, wantedMax * bucketFactor, 2, true);
}
: (u, wantedMin, wantedMax) => {
// these settings will prevent zooming, probably okay?
if (xScaleMin != null) {
wantedMin = xScaleMin;
}
if (xScaleMax != null) {
wantedMax = xScaleMax;
}
let fullRangeMin = u.data[0][0];
let fullRangeMax = u.data[0][u.data[0].length - 1];
let fullRangeMin = u.data[0][0];
let fullRangeMax = u.data[0][u.data[0].length - 1];
// snap to bucket divisors...
// snap to bucket divisors...
if (wantedMax === fullRangeMax) {
wantedMax += bucketSize;
} else {
wantedMax = incrRoundUp(wantedMax, bucketSize);
}
if (wantedMax === fullRangeMax) {
wantedMax += bucketSize;
} else {
wantedMax = incrRoundUp(wantedMax, bucketSize);
}
if (wantedMin > fullRangeMin) {
wantedMin = incrRoundDn(wantedMin, bucketSize);
}
if (wantedMin > fullRangeMin) {
wantedMin = incrRoundDn(wantedMin, bucketSize);
}
return [wantedMin, wantedMax];
},
return [wantedMin, wantedMax];
},
});
builder.addScale({
@ -132,22 +154,24 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => {
scaleKey: 'x',
isTime: false,
placement: AxisPlacement.Bottom,
incrs: histogramBucketSizes,
splits: xSplits,
values: (u: uPlot, splits: any[]) => {
const tickLabels = splits.map(xAxisFormatter);
incrs: isOrdinalX ? [1] : useLogScale ? undefined : histogramBucketSizes,
splits: useLogScale || isOrdinalX ? undefined : xSplits,
values: isOrdinalX
? (u: uPlot, splits: any[]) => splits
: (u: uPlot, splits: any[]) => {
const tickLabels = splits.map(xAxisFormatter);
const maxWidth = tickLabels.reduce(
(curMax, label) => Math.max(measureText(label, UPLOT_AXIS_FONT_SIZE).width, curMax),
0
);
const maxWidth = tickLabels.reduce(
(curMax, label) => Math.max(measureText(label, UPLOT_AXIS_FONT_SIZE).width, curMax),
0
);
const labelSpacing = 10;
const maxCount = u.bbox.width / ((maxWidth + labelSpacing) * devicePixelRatio);
const keepMod = Math.ceil(tickLabels.length / maxCount);
const labelSpacing = 10;
const maxCount = u.bbox.width / ((maxWidth + labelSpacing) * devicePixelRatio);
const keepMod = Math.ceil(tickLabels.length / maxCount);
return tickLabels.map((label, i) => (i % keepMod === 0 ? label : null));
},
return tickLabels.map((label, i) => (i % keepMod === 0 ? label : null));
},
//incrs: () => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((mult) => mult * bucketSize),
//splits: config.xSplits,
//values: config.xValues,