mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Histogram: Add support for stacking mode (#84693)
This commit is contained in:
parent
b525de07cd
commit
25a058f7ec
@ -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"],
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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),
|
||||
};
|
||||
|
||||
if (withConfig) {
|
||||
state.config = prepConfig(alignedFrame, this.props.theme);
|
||||
}
|
||||
}
|
||||
const config = withConfig ? prepConfig(alignedFrame, this.props.theme) : this.state.config!;
|
||||
const alignedData = preparePlotData(config, xMinOnlyFrame(alignedFrame));
|
||||
|
||||
return state;
|
||||
return {
|
||||
alignedFrame,
|
||||
alignedData,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
renderLegend(config: UPlotConfigBuilder) {
|
||||
@ -321,23 +320,18 @@ 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);
|
||||
const shouldReconfig =
|
||||
this.state.config == null ||
|
||||
bucketCount !== prevProps.bucketCount ||
|
||||
bucketSize !== prevProps.bucketSize ||
|
||||
this.props.options !== prevProps.options ||
|
||||
this.state.config === undefined ||
|
||||
structureRev !== prevProps.structureRev ||
|
||||
!structureRev;
|
||||
|
||||
if (newState) {
|
||||
const shouldReconfig =
|
||||
bucketCount !== prevProps.bucketCount ||
|
||||
bucketSize !== prevProps.bucketSize ||
|
||||
this.props.options !== prevProps.options ||
|
||||
this.state.config === undefined ||
|
||||
structureRev !== prevProps.structureRev ||
|
||||
!structureRev;
|
||||
const newState = this.prepState(this.props, shouldReconfig);
|
||||
|
||||
if (shouldReconfig) {
|
||||
newState.config = prepConfig(alignedFrame, this.props.theme);
|
||||
}
|
||||
}
|
||||
|
||||
newState && this.setState(newState);
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
|
||||
|
10
public/app/plugins/panel/histogram/config.ts
Normal file
10
public/app/plugins/panel/histogram/config.ts
Normal 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',
|
||||
},
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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[];
|
||||
|
@ -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',
|
||||
|
@ -42,6 +42,7 @@ composableKinds: PanelCfg: {
|
||||
FieldConfig: {
|
||||
common.AxisConfig
|
||||
common.HideableFieldConfig
|
||||
common.StackableFieldConfig
|
||||
|
||||
// Controls line width of the bars.
|
||||
lineWidth?: uint32 & <=10 | *1
|
||||
|
@ -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.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user