fix(histogram): remove CUE config, use pure TypeScript interface

This commit is contained in:
Jesse David Peterson
2025-09-09 13:35:12 -04:00
parent 43f030ba5f
commit 3aeb9ace36
8 changed files with 149 additions and 86 deletions
+18 -16
View File
@@ -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);
@@ -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<Options>;
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(() => {
+2 -2
View File
@@ -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',
+7 -1
View File
@@ -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';
+3 -3
View File
@@ -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,
@@ -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<Options> = {
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<FieldConfig> = {
fillOpacity: 80,
gradientMode: common.GraphGradientMode.None,
lineWidth: 1,
};
+76
View File
@@ -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<HistogramOptions> = {
bucketCount: 30,
bucketOffset: 0,
combine: false,
};
export const defaultHistogramFieldConfig: Partial<HistogramFieldConfig> = {
fillOpacity: 80,
gradientMode: GraphGradientMode.None,
lineWidth: 1,
};
@@ -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<Options>;
// 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 <HistogramPanelComponent {...props} />;
const { options, fieldConfig } = adaptGrafanaToHistogram(props.options, props.fieldConfig?.defaults?.custom || {});
return (
<HistogramPanelComponent
data={props.data}
options={options}
fieldConfig={fieldConfig}
width={props.width}
height={props.height}
/>
);
};