Histogram: Add support for stacking mode (#84693)

This commit is contained in:
Adela Almasan 2024-03-21 05:57:10 -06:00 committed by GitHub
parent b525de07cd
commit 25a058f7ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 175 additions and 52 deletions

View File

@ -5716,8 +5716,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "15"],
[0, 0, 0, "Do not use any type assertions.", "16"]
],
"public/app/plugins/panel/histogram/Histogram.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
"public/app/plugins/panel/histogram/migrations.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/live/LiveChannelEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -36,7 +36,7 @@ export const defaultOptions: Partial<Options> = {
bucketOffset: 0,
};
export interface FieldConfig extends common.AxisConfig, common.HideableFieldConfig {
export interface FieldConfig extends common.AxisConfig, common.HideableFieldConfig, common.StackableFieldConfig {
/**
* Controls the fill opacity of the bars.
*/

View File

@ -24,6 +24,7 @@ import {
measureText,
UPLOT_AXIS_FONT_SIZE,
} from '@grafana/ui';
import { getStackingGroups, preparePlotData2 } from '@grafana/ui/src/components/uPlot/utils';
import { defaultFieldConfig, FieldConfig, Options } from './panelcfg.gen';
@ -207,6 +208,9 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => {
},
});
let stackingGroups = getStackingGroups(xMinOnlyFrame(frame));
builder.setStackingGroups(stackingGroups);
let pathBuilder = uPlot.paths.bars!({ align: 1, size: [1, Infinity] });
let seriesIndex = 0;
@ -252,31 +256,32 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => {
return builder;
};
const preparePlotData = (frame: DataFrame) => {
let data = [];
for (const field of frame.fields) {
if (field.name !== histogramFrameBucketMaxFieldName) {
data.push(field.values);
}
}
// since we're reusing timeseries prep for stacking, we need to make a tmp frame where fields match the uplot data
// by removing the x bucket max field to make sure stacking group series idxs match up
const xMinOnlyFrame = (frame: DataFrame) => ({
...frame,
fields: frame.fields.filter((f) => f.name !== histogramFrameBucketMaxFieldName),
});
const preparePlotData = (builder: UPlotConfigBuilder, xMinOnlyFrame: DataFrame) => {
// uPlot's bars pathBuilder will draw rects even if 0 (to distinguish them from nulls)
// but for histograms we want to omit them, so remap 0s -> nulls
for (let i = 1; i < data.length; i++) {
let counts = data[i];
for (let i = 1; i < xMinOnlyFrame.fields.length; i++) {
let counts = xMinOnlyFrame.fields[i].values;
for (let j = 0; j < counts.length; j++) {
if (counts[j] === 0) {
counts[j] = null;
counts[j] = null; // mutates!
}
}
}
return data as AlignedData;
return preparePlotData2(xMinOnlyFrame, builder.getStackingGroups());
};
interface State {
alignedData: AlignedData;
alignedFrame: DataFrame;
config?: UPlotConfigBuilder;
}
@ -286,23 +291,17 @@ export class Histogram extends React.Component<HistogramProps, State> {
this.state = this.prepState(props);
}
prepState(props: HistogramProps, withConfig = true) {
let state: State = {
alignedData: [],
};
prepState(props: HistogramProps, withConfig = true): State {
const { alignedFrame } = props;
if (alignedFrame) {
state = {
alignedData: preparePlotData(alignedFrame),
const config = withConfig ? prepConfig(alignedFrame, this.props.theme) : this.state.config!;
const alignedData = preparePlotData(config, xMinOnlyFrame(alignedFrame));
return {
alignedFrame,
alignedData,
config,
};
if (withConfig) {
state.config = prepConfig(alignedFrame, this.props.theme);
}
}
return state;
}
renderLegend(config: UPlotConfigBuilder) {
@ -321,10 +320,8 @@ export class Histogram extends React.Component<HistogramProps, State> {
const { structureRev, alignedFrame, bucketSize, bucketCount } = this.props;
if (alignedFrame !== prevProps.alignedFrame) {
let newState = this.prepState(this.props, false);
if (newState) {
const shouldReconfig =
this.state.config == null ||
bucketCount !== prevProps.bucketCount ||
bucketSize !== prevProps.bucketSize ||
this.props.options !== prevProps.options ||
@ -332,12 +329,9 @@ export class Histogram extends React.Component<HistogramProps, State> {
structureRev !== prevProps.structureRev ||
!structureRev;
if (shouldReconfig) {
newState.config = prepConfig(alignedFrame, this.props.theme);
}
}
const newState = this.prepState(this.props, shouldReconfig);
newState && this.setState(newState);
this.setState(newState);
}
}

View File

@ -0,0 +1,10 @@
import { StackingMode } from '@grafana/schema';
import { FieldConfig } from './panelcfg.gen';
export const defaultHistogramConfig: FieldConfig = {
stacking: {
mode: StackingMode.None,
group: 'A',
},
};

View File

@ -1,4 +1,5 @@
import { FieldConfigSource, PanelModel } from '@grafana/data';
import { StackingMode } from '@grafana/ui';
import { changeToHistogramPanelMigrationHandler } from './migrations';
@ -12,7 +13,7 @@ describe('Histogram migrations', () => {
};
});
it('From old graph', () => {
it('Should migrate from old graph', () => {
const old = {
angular: {
xaxis: {
@ -23,6 +24,39 @@ describe('Histogram migrations', () => {
const panel = {} as PanelModel;
panel.options = changeToHistogramPanelMigrationHandler(panel, 'graph', old, prevFieldConfig);
expect(panel.options.combine).toBe(true);
expect(panel.options.combine).toBe(false);
});
it('Should migrate from old graph with percent stacking', () => {
const old = {
angular: {
xaxis: {
mode: 'histogram',
},
stack: true,
percentage: true,
},
};
const panel = {} as PanelModel;
panel.options = changeToHistogramPanelMigrationHandler(panel, 'graph', old, prevFieldConfig);
expect(panel.fieldConfig.defaults.custom.stacking.mode).toBe(StackingMode.Percent);
expect(panel.options.combine).toBe(false);
});
it('Should migrate from old graph with normal stacking', () => {
const old = {
angular: {
xaxis: {
mode: 'histogram',
},
stack: true,
},
};
const panel = {} as PanelModel;
panel.options = changeToHistogramPanelMigrationHandler(panel, 'graph', old, prevFieldConfig);
expect(panel.fieldConfig.defaults.custom.stacking.mode).toBe(StackingMode.Normal);
expect(panel.options.combine).toBe(false);
});
});

View File

@ -1,4 +1,15 @@
import { PanelTypeChangedHandler } from '@grafana/data';
import { isNil, omitBy } from 'lodash';
import { FieldConfigSource, PanelTypeChangedHandler } from '@grafana/data';
import {
LegendDisplayMode,
SortOrder,
StackingMode,
TooltipDisplayMode,
} from '@grafana/schema/dist/esm/common/common.gen';
import { defaultHistogramConfig } from './config';
import { FieldConfig as HistogramFieldConfig, Options } from './panelcfg.gen';
/*
* This is called when the panel changes from another panel
@ -13,16 +24,66 @@ export const changeToHistogramPanelMigrationHandler: PanelTypeChangedHandler = (
const graphOptions: GraphOptions = prevOptions.angular;
if (graphOptions.xaxis?.mode === 'histogram') {
return {
combine: true,
};
const { fieldConfig, options } = graphToHistogramOptions({
...prevOptions.angular,
fieldConfig: prevFieldConfig,
});
panel.fieldConfig = fieldConfig; // Mutates the incoming panel
return options;
}
}
return {};
};
function graphToHistogramOptions(angular: any): {
fieldConfig: FieldConfigSource;
options: Options;
} {
const graphOptions: GraphOptions = angular;
let histogramFieldConfig: HistogramFieldConfig = {};
const options: Options = {
legend: {
displayMode: LegendDisplayMode.List,
showLegend: true,
placement: 'bottom',
calcs: [],
},
tooltip: {
mode: TooltipDisplayMode.Single,
sort: SortOrder.None,
},
combine: false,
};
if (graphOptions.stack) {
histogramFieldConfig.stacking = {
mode: graphOptions.percentage ? StackingMode.Percent : StackingMode.Normal,
group: defaultHistogramConfig.stacking!.group,
};
options.combine = false;
}
return {
fieldConfig: {
defaults: omitBy(
{
custom: histogramFieldConfig,
},
isNil
),
overrides: [],
},
options,
};
}
interface GraphOptions {
stack?: boolean;
percentage?: boolean;
xaxis: {
mode: 'series' | 'time' | 'histogram';
values?: string[];

View File

@ -1,8 +1,16 @@
import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data';
import {
FieldColorModeId,
FieldConfigProperty,
FieldType,
identityOverrideProcessor,
PanelPlugin,
} from '@grafana/data';
import { histogramFieldInfo } from '@grafana/data/src/transformations/transformers/histogram';
import { commonOptionsBuilder, graphFieldOptions } from '@grafana/ui';
import { StackingEditor } from '@grafana/ui/src/options/builder';
import { HistogramPanel } from './HistogramPanel';
import { defaultHistogramConfig } from './config';
import { changeToHistogramPanelMigrationHandler } from './migrations';
import { FieldConfig, Options, defaultFieldConfig, defaultOptions } from './panelcfg.gen';
import { originalDataHasHistogram } from './utils';
@ -78,6 +86,21 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(HistogramPanel)
const cfg = defaultFieldConfig;
builder
.addCustomEditor({
id: 'stacking',
path: 'stacking',
name: 'Stacking',
category: ['Histogram'],
defaultValue: defaultHistogramConfig.stacking,
editor: StackingEditor,
override: StackingEditor,
settings: {
options: graphFieldOptions.stacking,
},
process: identityOverrideProcessor,
shouldApply: (f) => f.type === FieldType.number,
showIf: (opts, data) => !originalDataHasHistogram(data),
})
.addSliderInput({
path: 'lineWidth',
name: 'Line width',

View File

@ -42,6 +42,7 @@ composableKinds: PanelCfg: {
FieldConfig: {
common.AxisConfig
common.HideableFieldConfig
common.StackableFieldConfig
// Controls line width of the bars.
lineWidth?: uint32 & <=10 | *1

View File

@ -34,7 +34,7 @@ export const defaultOptions: Partial<Options> = {
bucketOffset: 0,
};
export interface FieldConfig extends common.AxisConfig, common.HideableFieldConfig {
export interface FieldConfig extends common.AxisConfig, common.HideableFieldConfig, common.StackableFieldConfig {
/**
* Controls the fill opacity of the bars.
*/