From 3aeb9ace36bf09b5c4be61bff066d0935b9893af Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Tue, 9 Sep 2025 11:18:10 -0400 Subject: [PATCH] fix(histogram): remove CUE config, use pure TypeScript interface --- packages/grafana-histogram/src/Histogram.tsx | 34 +++++---- .../grafana-histogram/src/HistogramPanel.tsx | 7 +- packages/grafana-histogram/src/config.ts | 4 +- packages/grafana-histogram/src/index.ts | 8 +- packages/grafana-histogram/src/migrations.ts | 6 +- .../grafana-histogram/src/panelcfg.gen.ts | 57 -------------- packages/grafana-histogram/src/types.ts | 76 +++++++++++++++++++ .../panel/histogram/HistogramPanel.tsx | 43 ++++++++++- 8 files changed, 149 insertions(+), 86 deletions(-) delete mode 100644 packages/grafana-histogram/src/panelcfg.gen.ts create mode 100644 packages/grafana-histogram/src/types.ts diff --git a/packages/grafana-histogram/src/Histogram.tsx b/packages/grafana-histogram/src/Histogram.tsx index 331acd8b57d..ddb075feb25 100644 --- a/packages/grafana-histogram/src/Histogram.tsx +++ b/packages/grafana-histogram/src/Histogram.tsx @@ -25,7 +25,7 @@ import { preparePlotData2, } from '@grafana/ui'; -import { defaultFieldConfig, FieldConfig, Options } from './panelcfg.gen'; +import { HistogramOptions, HistogramFieldConfig, defaultHistogramFieldConfig } from './types'; function incrRoundDn(num: number, incr: number) { return Math.floor(num / incr) * incr; @@ -36,36 +36,36 @@ function incrRoundUp(num: number, incr: number) { } export interface HistogramProps extends Themeable2 { - options: Options; // used for diff - alignedFrame: DataFrame; // This could take HistogramFields + options: HistogramOptions; + alignedFrame: DataFrame; bucketCount?: number; bucketSize: number; width: number; height: number; - structureRev?: number; // a number that will change when the frames[] structure changes + structureRev?: number; legend: VizLegendOptions; rawSeries?: DataFrame[]; children?: (builder: UPlotConfigBuilder, frame: DataFrame, xMinOnlyFrame: DataFrame) => React.ReactNode; } export function getBucketSize(frame: DataFrame) { - // assumes BucketMin is fields[0] and BucktMax is fields[1] + // NOTE: Assumes BucketMin is fields[0] and BucktMax is fields[1] return frame.fields[0].type === FieldType.string ? 1 : roundDecimals(frame.fields[1].values[0] - frame.fields[0].values[0], 9); } export function getBucketSize1(frame: DataFrame) { - // assumes BucketMin is fields[0] and BucktMax is fields[1] + // NOTE: Assumes BucketMin is fields[0] and BucktMax is fields[1] return frame.fields[0].type === FieldType.string ? 1 : roundDecimals(frame.fields[1].values[1] - frame.fields[0].values[1], 9); } const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { - // todo: scan all values in BucketMin and BucketMax fields to assert if uniform bucketSize + // TODO: scan all values in BucketMin and BucketMax fields to assert if uniform bucketSize - // since this is x axis range, this should ideally come from xMin or xMax fields, not a count field + // NOTE: Since this is x axis range, this should ideally come from xMin or xMax fields, not a count field // though both methods are probably hacks, and we should just accept explicit opts into this prepConfig let { min: xScaleMin, max: xScaleMax } = frame.fields[2].config; @@ -73,7 +73,7 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { let isOrdinalX = frame.fields[0].type === FieldType.string; - // assumes BucketMin is fields[0] and BucktMax is fields[1] + // NOTE: Assumes BucketMin is fields[0] and BucktMax is fields[1] let bucketSize = getBucketSize(frame); let bucketSize1 = getBucketSize1(frame); @@ -81,7 +81,7 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { let useLogScale = bucketSize1 !== bucketSize; // (imperfect floats) - // splits shifter, to ensure splits always start at first bucket + // Splits shifter to ensure splits always start at first bucket let xSplits: uPlot.Axis.Splits = (u, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace) => { /** @ts-ignore */ let minSpace = u.axes[axisIdx]._space; @@ -101,7 +101,7 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { }; builder.addScale({ - scaleKey: 'x', // bukkits + scaleKey: 'x', // Buckets isTime: false, distribution: isOrdinalX ? ScaleDistribution.Ordinal @@ -116,7 +116,7 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { return uPlot.rangeLog(wantedMin, wantedMax * bucketFactor, 2, true); } : (u, wantedMin, wantedMax) => { - // these settings will prevent zooming, probably okay? + // TODO: These settings will prevent zooming, probably okay? if (xScaleMin != null) { wantedMin = xScaleMin; } @@ -126,8 +126,10 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { let fullRangeMax = u.data[0][u.data[0].length - 1]; - // isOrdinalX is when we have classic histograms, which are LE, ordinal X, and already have 0 dummy bucket prepended - // else we have calculated histograms which are GE and cardinal+linear X, and have no next dummy bucket appended + // NOTE: isOrdinalX is when we have classic histograms, which are LE, + // ordinal X, and already have 0 dummy bucket prepended + // else we have calculated histograms which are GE and cardinal+linear X, + // and have no next dummy bucket appended. wantedMin = incrRoundUp(wantedMin, bucketSize); wantedMax = !isOrdinalX && wantedMax === fullRangeMax ? wantedMax + bucketSize : incrRoundDn(wantedMax, bucketSize); @@ -137,7 +139,7 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { }); builder.addScale({ - scaleKey: 'y', // counts + scaleKey: 'y', // Counts isTime: false, distribution: ScaleDistribution.Linear, orientation: ScaleOrientation.Vertical, @@ -227,7 +229,7 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { field.state = field.state ?? {}; field.state.seriesIndex = seriesIndex++; - const customConfig: FieldConfig = { ...defaultFieldConfig, ...field.config.custom }; + const customConfig: HistogramFieldConfig = { ...defaultHistogramFieldConfig, ...field.config.custom }; const scaleKey = 'y'; const colorMode = getFieldColorModeForField(field); diff --git a/packages/grafana-histogram/src/HistogramPanel.tsx b/packages/grafana-histogram/src/HistogramPanel.tsx index e93288adad9..28aa8d18b7d 100644 --- a/packages/grafana-histogram/src/HistogramPanel.tsx +++ b/packages/grafana-histogram/src/HistogramPanel.tsx @@ -4,7 +4,6 @@ import { histogramFieldsToFrame, joinHistograms, DataFrameType, - PanelProps, buildHistogram, cacheFieldDisplayNames, getHistogramFields, @@ -14,11 +13,11 @@ import { TooltipDisplayMode, TooltipPlugin2, useTheme2, TooltipHoverMode } from import { Histogram, getBucketSize } from './Histogram'; import { HistogramTooltip } from './HistogramTooltip'; -import { Options } from './panelcfg.gen'; +import { HistogramPanelProps } from './types'; -type Props = PanelProps; +type Props = HistogramPanelProps; -export const HistogramPanel = ({ data, options, width, height }: Props) => { +export const HistogramPanel = ({ data, options, fieldConfig, width, height }: Props) => { const theme = useTheme2(); const histogram = useMemo(() => { diff --git a/packages/grafana-histogram/src/config.ts b/packages/grafana-histogram/src/config.ts index 88ff8f9089a..689e8403b35 100644 --- a/packages/grafana-histogram/src/config.ts +++ b/packages/grafana-histogram/src/config.ts @@ -1,8 +1,8 @@ import { StackingMode } from '@grafana/schema'; -import { FieldConfig } from './panelcfg.gen'; +import { HistogramFieldConfig } from './types'; -export const defaultHistogramConfig: FieldConfig = { +export const defaultHistogramConfig: HistogramFieldConfig = { stacking: { mode: StackingMode.None, group: 'A', diff --git a/packages/grafana-histogram/src/index.ts b/packages/grafana-histogram/src/index.ts index 966e74db780..924036db32b 100644 --- a/packages/grafana-histogram/src/index.ts +++ b/packages/grafana-histogram/src/index.ts @@ -4,4 +4,10 @@ export { HistogramTooltip, type HistogramTooltipProps } from './HistogramTooltip export { changeToHistogramPanelMigrationHandler } from './migrations'; export { defaultHistogramConfig } from './config'; export { originalDataHasHistogram } from './utils'; -export { type Options, type FieldConfig, defaultOptions, defaultFieldConfig } from './panelcfg.gen'; +export { + type HistogramOptions, + type HistogramFieldConfig, + type HistogramPanelProps, + defaultHistogramOptions, + defaultHistogramFieldConfig, +} from './types'; diff --git a/packages/grafana-histogram/src/migrations.ts b/packages/grafana-histogram/src/migrations.ts index 28fe88a1587..6eb34a777f5 100644 --- a/packages/grafana-histogram/src/migrations.ts +++ b/packages/grafana-histogram/src/migrations.ts @@ -4,7 +4,7 @@ import { FieldConfigSource, PanelTypeChangedHandler } from '@grafana/data'; import { LegendDisplayMode, SortOrder, StackingMode, TooltipDisplayMode } from '@grafana/schema'; import { defaultHistogramConfig } from './config'; -import { FieldConfig as HistogramFieldConfig, Options } from './panelcfg.gen'; +import { HistogramFieldConfig, HistogramOptions } from './types'; /* * This is called when the panel changes from another panel @@ -35,10 +35,10 @@ export const changeToHistogramPanelMigrationHandler: PanelTypeChangedHandler = ( function graphToHistogramOptions(graphOptions: GraphOptions): { fieldConfig: FieldConfigSource; - options: Options; + options: HistogramOptions; } { let histogramFieldConfig: HistogramFieldConfig = {}; - const options: Options = { + const options: HistogramOptions = { legend: { displayMode: LegendDisplayMode.List, showLegend: true, diff --git a/packages/grafana-histogram/src/panelcfg.gen.ts b/packages/grafana-histogram/src/panelcfg.gen.ts deleted file mode 100644 index 8eff826f474..00000000000 --- a/packages/grafana-histogram/src/panelcfg.gen.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// public/app/plugins/gen.go -// Using jennies: -// TSTypesJenny -// PluginTsTypesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -import * as common from '@grafana/schema'; - -export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip { - /** - * Bucket count (approx) - */ - bucketCount?: number; - /** - * Offset buckets by this amount - */ - bucketOffset?: number; - /** - * Size of each bucket - */ - bucketSize?: number; - /** - * Combines multiple series into a single histogram - */ - combine?: boolean; -} - -export const defaultOptions: Partial = { - bucketCount: 30, - bucketOffset: 0, -}; - -export interface FieldConfig extends common.AxisConfig, common.HideableFieldConfig, common.StackableFieldConfig { - /** - * Controls the fill opacity of the bars. - */ - fillOpacity?: number; - /** - * Set the mode of the gradient fill. Fill gradient is based on the line color. To change the color, use the standard color scheme field option. - * Gradient appearance is influenced by the Fill opacity setting. - */ - gradientMode?: common.GraphGradientMode; - /** - * Controls line width of the bars. - */ - lineWidth?: number; -} - -export const defaultFieldConfig: Partial = { - fillOpacity: 80, - gradientMode: common.GraphGradientMode.None, - lineWidth: 1, -}; diff --git a/packages/grafana-histogram/src/types.ts b/packages/grafana-histogram/src/types.ts new file mode 100644 index 00000000000..9021a1400a7 --- /dev/null +++ b/packages/grafana-histogram/src/types.ts @@ -0,0 +1,76 @@ +import { PanelData } from '@grafana/data'; +import { + VizLegendOptions, + VizTooltipOptions, + GraphGradientMode, + StackingConfig, + AxisConfig, + HideableFieldConfig, +} from '@grafana/schema'; + +export interface HistogramOptions { + /** + * Bucket count (approx) + */ + bucketCount?: number; + /** + * Size of each bucket + */ + bucketSize?: number; + /** + * Offset buckets by this amount + */ + bucketOffset?: number; + /** + * Combines multiple series into a single histogram + */ + combine?: boolean; + /** + * Legend configuration + */ + legend: VizLegendOptions; + /** + * Tooltip configuration + */ + tooltip: VizTooltipOptions; +} + +export interface HistogramFieldConfig extends AxisConfig, HideableFieldConfig { + /** + * Controls line width of the bars. + */ + lineWidth?: number; + /** + * Controls the fill opacity of the bars. + */ + fillOpacity?: number; + /** + * Set the mode of the gradient fill. + */ + gradientMode?: GraphGradientMode; + /** + * Stacking configuration + */ + stacking?: StackingConfig; +} + +export interface HistogramPanelProps { + data: PanelData; + options: HistogramOptions; + fieldConfig: HistogramFieldConfig; + width: number; + height: number; +} + +// Default values +export const defaultHistogramOptions: Partial = { + bucketCount: 30, + bucketOffset: 0, + combine: false, +}; + +export const defaultHistogramFieldConfig: Partial = { + fillOpacity: 80, + gradientMode: GraphGradientMode.None, + lineWidth: 1, +}; diff --git a/public/app/plugins/panel/histogram/HistogramPanel.tsx b/public/app/plugins/panel/histogram/HistogramPanel.tsx index ce69ca98296..d5d7be19e96 100644 --- a/public/app/plugins/panel/histogram/HistogramPanel.tsx +++ b/public/app/plugins/panel/histogram/HistogramPanel.tsx @@ -1,10 +1,47 @@ import { PanelProps } from '@grafana/data'; -import { HistogramPanel as HistogramPanelComponent } from '@grafana/histogram'; +import { HistogramPanel as HistogramPanelComponent, HistogramOptions, HistogramFieldConfig } from '@grafana/histogram'; -import { Options } from './panelcfg.gen'; +import { Options, FieldConfig } from './panelcfg.gen'; type Props = PanelProps; +// Adapter function: Transform Grafana's CUE-generated config to clean histogram config +function adaptGrafanaToHistogram( + grafanaOptions: Options, + grafanaFieldConfig: FieldConfig +): { options: HistogramOptions; fieldConfig: HistogramFieldConfig } { + return { + options: { + bucketCount: grafanaOptions.bucketCount, + bucketSize: grafanaOptions.bucketSize, + bucketOffset: grafanaOptions.bucketOffset, + combine: grafanaOptions.combine ?? false, + legend: grafanaOptions.legend, + tooltip: grafanaOptions.tooltip, + }, + fieldConfig: { + lineWidth: grafanaFieldConfig.lineWidth, + fillOpacity: grafanaFieldConfig.fillOpacity, + gradientMode: grafanaFieldConfig.gradientMode, + stacking: grafanaFieldConfig.stacking, + // Include extended properties from AxisConfig and HideableFieldConfig + hideFrom: grafanaFieldConfig.hideFrom, + axisSoftMin: grafanaFieldConfig.axisSoftMin, + axisSoftMax: grafanaFieldConfig.axisSoftMax, + }, + }; +} + export const HistogramPanel = (props: Props) => { - return ; + const { options, fieldConfig } = adaptGrafanaToHistogram(props.options, props.fieldConfig?.defaults?.custom || {}); + + return ( + + ); };