mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Histogram: Render heatmap-cells and heatmap-rows frames (#77111)
This commit is contained in:
parent
72a085ea20
commit
45aa58c4a8
@ -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"],
|
||||
|
@ -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[] = [];
|
||||
|
@ -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]];
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user