diff --git a/.betterer.results b/.betterer.results index 00598e6a077..196906a6074 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"], diff --git a/packages/grafana-data/src/transformations/transformers/histogram.ts b/packages/grafana-data/src/transformations/transformers/histogram.ts index a82217c2467..1a8f8d858e7 100644 --- a/packages/grafana-data/src/transformations/transformers/histogram.ts +++ b/packages/grafana-data/src/transformations/transformers/histogram.ts @@ -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(); + 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[] = []; diff --git a/public/app/plugins/panel/heatmap/utils.ts b/public/app/plugins/panel/heatmap/utils.ts index d5d09e238c1..7dcbf6643a6 100644 --- a/public/app/plugins/panel/heatmap/utils.ts +++ b/public/app/plugins/panel/heatmap/utils.ts @@ -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]]; diff --git a/public/app/plugins/panel/histogram/Histogram.tsx b/public/app/plugins/panel/histogram/Histogram.tsx index 58ec3eab141..77d3c3127bd 100644 --- a/public/app/plugins/panel/histogram/Histogram.tsx +++ b/public/app/plugins/panel/histogram/Histogram.tsx @@ -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,