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.", "15"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "16"]
|
[0, 0, 0, "Do not use any type assertions.", "16"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/panel/histogram/Histogram.tsx:5381": [
|
"public/app/plugins/panel/histogram/migrations.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/panel/live/LiveChannelEditor.tsx:5381": [
|
"public/app/plugins/panel/live/LiveChannelEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
@ -36,7 +36,7 @@ export const defaultOptions: Partial<Options> = {
|
|||||||
bucketOffset: 0,
|
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.
|
* Controls the fill opacity of the bars.
|
||||||
*/
|
*/
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
measureText,
|
measureText,
|
||||||
UPLOT_AXIS_FONT_SIZE,
|
UPLOT_AXIS_FONT_SIZE,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
import { getStackingGroups, preparePlotData2 } from '@grafana/ui/src/components/uPlot/utils';
|
||||||
|
|
||||||
import { defaultFieldConfig, FieldConfig, Options } from './panelcfg.gen';
|
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 pathBuilder = uPlot.paths.bars!({ align: 1, size: [1, Infinity] });
|
||||||
|
|
||||||
let seriesIndex = 0;
|
let seriesIndex = 0;
|
||||||
@ -252,31 +256,32 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => {
|
|||||||
return builder;
|
return builder;
|
||||||
};
|
};
|
||||||
|
|
||||||
const preparePlotData = (frame: DataFrame) => {
|
// since we're reusing timeseries prep for stacking, we need to make a tmp frame where fields match the uplot data
|
||||||
let data = [];
|
// by removing the x bucket max field to make sure stacking group series idxs match up
|
||||||
|
const xMinOnlyFrame = (frame: DataFrame) => ({
|
||||||
for (const field of frame.fields) {
|
...frame,
|
||||||
if (field.name !== histogramFrameBucketMaxFieldName) {
|
fields: frame.fields.filter((f) => f.name !== histogramFrameBucketMaxFieldName),
|
||||||
data.push(field.values);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const preparePlotData = (builder: UPlotConfigBuilder, xMinOnlyFrame: DataFrame) => {
|
||||||
// uPlot's bars pathBuilder will draw rects even if 0 (to distinguish them from nulls)
|
// 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
|
// but for histograms we want to omit them, so remap 0s -> nulls
|
||||||
for (let i = 1; i < data.length; i++) {
|
for (let i = 1; i < xMinOnlyFrame.fields.length; i++) {
|
||||||
let counts = data[i];
|
let counts = xMinOnlyFrame.fields[i].values;
|
||||||
|
|
||||||
for (let j = 0; j < counts.length; j++) {
|
for (let j = 0; j < counts.length; j++) {
|
||||||
if (counts[j] === 0) {
|
if (counts[j] === 0) {
|
||||||
counts[j] = null;
|
counts[j] = null; // mutates!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data as AlignedData;
|
return preparePlotData2(xMinOnlyFrame, builder.getStackingGroups());
|
||||||
};
|
};
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
alignedData: AlignedData;
|
alignedData: AlignedData;
|
||||||
|
alignedFrame: DataFrame;
|
||||||
config?: UPlotConfigBuilder;
|
config?: UPlotConfigBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,23 +291,17 @@ export class Histogram extends React.Component<HistogramProps, State> {
|
|||||||
this.state = this.prepState(props);
|
this.state = this.prepState(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
prepState(props: HistogramProps, withConfig = true) {
|
prepState(props: HistogramProps, withConfig = true): State {
|
||||||
let state: State = {
|
|
||||||
alignedData: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const { alignedFrame } = props;
|
const { alignedFrame } = props;
|
||||||
if (alignedFrame) {
|
|
||||||
state = {
|
|
||||||
alignedData: preparePlotData(alignedFrame),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (withConfig) {
|
const config = withConfig ? prepConfig(alignedFrame, this.props.theme) : this.state.config!;
|
||||||
state.config = prepConfig(alignedFrame, this.props.theme);
|
const alignedData = preparePlotData(config, xMinOnlyFrame(alignedFrame));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
return {
|
||||||
|
alignedFrame,
|
||||||
|
alignedData,
|
||||||
|
config,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLegend(config: UPlotConfigBuilder) {
|
renderLegend(config: UPlotConfigBuilder) {
|
||||||
@ -321,23 +320,18 @@ export class Histogram extends React.Component<HistogramProps, State> {
|
|||||||
const { structureRev, alignedFrame, bucketSize, bucketCount } = this.props;
|
const { structureRev, alignedFrame, bucketSize, bucketCount } = this.props;
|
||||||
|
|
||||||
if (alignedFrame !== prevProps.alignedFrame) {
|
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 newState = this.prepState(this.props, shouldReconfig);
|
||||||
const shouldReconfig =
|
|
||||||
bucketCount !== prevProps.bucketCount ||
|
|
||||||
bucketSize !== prevProps.bucketSize ||
|
|
||||||
this.props.options !== prevProps.options ||
|
|
||||||
this.state.config === undefined ||
|
|
||||||
structureRev !== prevProps.structureRev ||
|
|
||||||
!structureRev;
|
|
||||||
|
|
||||||
if (shouldReconfig) {
|
this.setState(newState);
|
||||||
newState.config = prepConfig(alignedFrame, this.props.theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 { FieldConfigSource, PanelModel } from '@grafana/data';
|
||||||
|
import { StackingMode } from '@grafana/ui';
|
||||||
|
|
||||||
import { changeToHistogramPanelMigrationHandler } from './migrations';
|
import { changeToHistogramPanelMigrationHandler } from './migrations';
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ describe('Histogram migrations', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('From old graph', () => {
|
it('Should migrate from old graph', () => {
|
||||||
const old = {
|
const old = {
|
||||||
angular: {
|
angular: {
|
||||||
xaxis: {
|
xaxis: {
|
||||||
@ -23,6 +24,39 @@ describe('Histogram migrations', () => {
|
|||||||
|
|
||||||
const panel = {} as PanelModel;
|
const panel = {} as PanelModel;
|
||||||
panel.options = changeToHistogramPanelMigrationHandler(panel, 'graph', old, prevFieldConfig);
|
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
|
* This is called when the panel changes from another panel
|
||||||
@ -13,16 +24,66 @@ export const changeToHistogramPanelMigrationHandler: PanelTypeChangedHandler = (
|
|||||||
const graphOptions: GraphOptions = prevOptions.angular;
|
const graphOptions: GraphOptions = prevOptions.angular;
|
||||||
|
|
||||||
if (graphOptions.xaxis?.mode === 'histogram') {
|
if (graphOptions.xaxis?.mode === 'histogram') {
|
||||||
return {
|
const { fieldConfig, options } = graphToHistogramOptions({
|
||||||
combine: true,
|
...prevOptions.angular,
|
||||||
};
|
fieldConfig: prevFieldConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.fieldConfig = fieldConfig; // Mutates the incoming panel
|
||||||
|
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
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 {
|
interface GraphOptions {
|
||||||
|
stack?: boolean;
|
||||||
|
percentage?: boolean;
|
||||||
xaxis: {
|
xaxis: {
|
||||||
mode: 'series' | 'time' | 'histogram';
|
mode: 'series' | 'time' | 'histogram';
|
||||||
values?: string[];
|
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 { histogramFieldInfo } from '@grafana/data/src/transformations/transformers/histogram';
|
||||||
import { commonOptionsBuilder, graphFieldOptions } from '@grafana/ui';
|
import { commonOptionsBuilder, graphFieldOptions } from '@grafana/ui';
|
||||||
|
import { StackingEditor } from '@grafana/ui/src/options/builder';
|
||||||
|
|
||||||
import { HistogramPanel } from './HistogramPanel';
|
import { HistogramPanel } from './HistogramPanel';
|
||||||
|
import { defaultHistogramConfig } from './config';
|
||||||
import { changeToHistogramPanelMigrationHandler } from './migrations';
|
import { changeToHistogramPanelMigrationHandler } from './migrations';
|
||||||
import { FieldConfig, Options, defaultFieldConfig, defaultOptions } from './panelcfg.gen';
|
import { FieldConfig, Options, defaultFieldConfig, defaultOptions } from './panelcfg.gen';
|
||||||
import { originalDataHasHistogram } from './utils';
|
import { originalDataHasHistogram } from './utils';
|
||||||
@ -78,6 +86,21 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(HistogramPanel)
|
|||||||
const cfg = defaultFieldConfig;
|
const cfg = defaultFieldConfig;
|
||||||
|
|
||||||
builder
|
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({
|
.addSliderInput({
|
||||||
path: 'lineWidth',
|
path: 'lineWidth',
|
||||||
name: 'Line width',
|
name: 'Line width',
|
||||||
|
@ -42,6 +42,7 @@ composableKinds: PanelCfg: {
|
|||||||
FieldConfig: {
|
FieldConfig: {
|
||||||
common.AxisConfig
|
common.AxisConfig
|
||||||
common.HideableFieldConfig
|
common.HideableFieldConfig
|
||||||
|
common.StackableFieldConfig
|
||||||
|
|
||||||
// Controls line width of the bars.
|
// Controls line width of the bars.
|
||||||
lineWidth?: uint32 & <=10 | *1
|
lineWidth?: uint32 & <=10 | *1
|
||||||
|
@ -34,7 +34,7 @@ export const defaultOptions: Partial<Options> = {
|
|||||||
bucketOffset: 0,
|
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.
|
* Controls the fill opacity of the bars.
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user