mirror of
https://github.com/grafana/grafana.git
synced 2025-02-09 23:16:16 -06:00
TimeSeries: Add support for negative Y and constant transform (#44774)
* add negative y config
* Handle negative y and constant transform in Timeseries panel
* Typechecks
* Add migration from old graph panel
* Docs update
* Revert "Add migration from old graph panel"
This reverts commit 33b5a90b66
.
* Revert VizLegendItem changes
* Automatically separate positive and negative stacks within a group
* Update packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx
* Remove SeriesLabel component
* Update docs/sources/visualizations/time-series/_index.md
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
* Update docs/sources/visualizations/time-series/_index.md
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
* Leftover reverts
* Don't crate bands (for now0 for negative -y stack
* Add docs note about transform being only available as an override
* Fill negative bands in reversed direction
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
parent
ea236c276e
commit
ffea53f2f6
@ -35,6 +35,15 @@ Use these options to choose how to display your time series data.
|
||||
- [Graph stacked time series]({{< relref "./graph-time-series-stacking.md" >}})
|
||||
- [Graph and color schemes]({{< relref "./graph-color-scheme.md" >}})
|
||||
|
||||
### Transform
|
||||
|
||||
Use this option to transform the series values without affecting the values shown in the tooltip, context menu, and legend.
|
||||
|
||||
- **Negative Y transform -** Flip the results to negative values on the Y axis.
|
||||
- **Constant -** Show first value as a constant line.
|
||||
|
||||
> **Note:** Transform option is only available as an override.
|
||||
|
||||
## Axis
|
||||
|
||||
For more information about adjusting your time series axes, refer to [Change axis display]({{< relref "change-axis-display.md" >}}).
|
||||
|
@ -74,6 +74,8 @@ export const valueMappingsOverrideProcessor = (
|
||||
export interface SelectFieldConfigSettings<T> {
|
||||
allowCustomValue?: boolean;
|
||||
|
||||
isClearable?: boolean;
|
||||
|
||||
/** The default options */
|
||||
options: Array<SelectableValue<T>>;
|
||||
|
||||
|
@ -7,6 +7,7 @@ LineInterpolation: "linear" | "smooth" | "stepBefore" | "stepAfter" @cue
|
||||
ScaleDistribution: "linear" | "log" | "ordinal" @cuetsy(kind="enum")
|
||||
GraphGradientMode: "none" | "opacity" | "hue" | "scheme" @cuetsy(kind="enum")
|
||||
StackingMode: "none" | "normal" | "percent" @cuetsy(kind="enum")
|
||||
GraphTransform: "constant" | "negative-Y" @cuetsy(kind="enum")
|
||||
BarAlignment: -1 | 0 | 1 @cuetsy(kind="enum",memberNames="Before|Center|After")
|
||||
ScaleOrientation: 0 | 1 @cuetsy(kind="enum",memberNames="Horizontal|Vertical")
|
||||
ScaleDirection: 1 | 1 | -1 | -1 @cuetsy(kind="enum",memberNames="Up|Right|Down|Left")
|
||||
@ -84,4 +85,5 @@ GraphFieldConfig: {
|
||||
drawStyle?: GraphDrawStyle
|
||||
gradientMode?: GraphGradientMode
|
||||
thresholdsStyle?: GraphThresholdsStyleConfig
|
||||
transform?: GraphTransform
|
||||
} @cuetsy(kind="interface")
|
||||
|
@ -24,6 +24,11 @@ export enum GraphDrawStyle {
|
||||
Points = 'points',
|
||||
}
|
||||
|
||||
export enum GraphTransform {
|
||||
Constant = 'constant',
|
||||
NegativeY = 'negative-Y',
|
||||
}
|
||||
|
||||
export enum LineInterpolation {
|
||||
Linear = 'linear',
|
||||
Smooth = 'smooth',
|
||||
@ -256,6 +261,7 @@ export interface GraphFieldConfig
|
||||
drawStyle?: GraphDrawStyle;
|
||||
gradientMode?: GraphGradientMode;
|
||||
thresholdsStyle?: GraphThresholdsStyleConfig;
|
||||
transform?: GraphTransform;
|
||||
}
|
||||
|
||||
export interface VizLegendOptions {
|
||||
|
@ -60,12 +60,14 @@ Object {
|
||||
],
|
||||
"bands": Array [
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
2,
|
||||
1,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
4,
|
||||
3,
|
||||
|
@ -67,7 +67,8 @@ export class SelectValueEditor<T> extends React.PureComponent<Props<T>, State<T>
|
||||
value={current}
|
||||
defaultValue={value}
|
||||
allowCustomValue={settings?.allowCustomValue}
|
||||
onChange={(e) => onChange(e.value)}
|
||||
isClearable={settings?.isClearable}
|
||||
onChange={(e) => onChange(e?.value)}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
VizLegendOptions,
|
||||
StackingMode,
|
||||
} from '@grafana/schema';
|
||||
import { collectStackingGroups, orderIdsByCalcs, preparePlotData } from '../uPlot/utils';
|
||||
import { collectStackingGroups, INTERNAL_NEGATIVE_Y_PREFIX, orderIdsByCalcs, preparePlotData } from '../uPlot/utils';
|
||||
import uPlot from 'uplot';
|
||||
import { buildScaleKey } from '../GraphNG/utils';
|
||||
|
||||
@ -332,11 +332,12 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
}
|
||||
|
||||
if (stackingGroups.size !== 0) {
|
||||
for (const [_, seriesIds] of stackingGroups.entries()) {
|
||||
for (const [group, seriesIds] of stackingGroups.entries()) {
|
||||
const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame });
|
||||
for (let j = seriesIdxs.length - 1; j > 0; j--) {
|
||||
builder.addBand({
|
||||
series: [seriesIdxs[j], seriesIdxs[j - 1]],
|
||||
dir: group.startsWith(INTERNAL_NEGATIVE_Y_PREFIX) ? 1 : -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import { useStyles2 } from '../../themes';
|
||||
*/
|
||||
export interface SeriesTableRowProps {
|
||||
color?: string;
|
||||
label?: string;
|
||||
label?: React.ReactNode;
|
||||
value?: string | GraphSeriesValue;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
@ -231,6 +231,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
||||
|
||||
const v = otherProps.data.fields[i].values.get(focusedPointIdxs[i]!);
|
||||
const display = field.display!(v);
|
||||
|
||||
sortIdx.push([series.length, v]);
|
||||
series.push({
|
||||
color: display.color || FALLBACK_COLOR,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { orderIdsByCalcs, preparePlotData, timeFormatToTemplate } from './utils';
|
||||
import { FieldType, MutableDataFrame } from '@grafana/data';
|
||||
import { StackingMode } from '@grafana/schema';
|
||||
import { GraphTransform, StackingMode } from '@grafana/schema';
|
||||
|
||||
describe('timeFormatToTemplate', () => {
|
||||
it.each`
|
||||
@ -53,6 +53,76 @@ describe('preparePlotData', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
describe('transforms', () => {
|
||||
it('negative-y transform', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{ name: 'a', values: [-10, 20, 10] },
|
||||
{ name: 'b', values: [10, 10, 10] },
|
||||
{ name: 'c', values: [20, 20, 20], config: { custom: { transform: GraphTransform.NegativeY } } },
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
-10,
|
||||
20,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
-20,
|
||||
-20,
|
||||
-20,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('constant transform', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{ name: 'a', values: [-10, 20, 10], config: { custom: { transform: GraphTransform.Constant } } },
|
||||
{ name: 'b', values: [10, 10, 10] },
|
||||
{ name: 'c', values: [20, 20, 20] },
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
-10,
|
||||
-10,
|
||||
-10,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
20,
|
||||
20,
|
||||
20,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
describe('stacking', () => {
|
||||
it('none', () => {
|
||||
const df = new MutableDataFrame({
|
||||
@ -148,6 +218,67 @@ describe('preparePlotData', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('standard with negative y transform', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-10, 20, 10],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [10, 10, 10],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [20, 20, 20],
|
||||
config: {
|
||||
custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' }, transform: GraphTransform.NegativeY },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
values: [10, 10, 10],
|
||||
config: {
|
||||
custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' }, transform: GraphTransform.NegativeY },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
-10,
|
||||
20,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
0,
|
||||
30,
|
||||
20,
|
||||
],
|
||||
Array [
|
||||
-20,
|
||||
-20,
|
||||
-20,
|
||||
],
|
||||
Array [
|
||||
-30,
|
||||
-30,
|
||||
-30,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('standard with multiple groups', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { DataFrame, ensureTimeField, Field, FieldType } from '@grafana/data';
|
||||
import { StackingMode, VizLegendOptions } from '@grafana/schema';
|
||||
import { GraphFieldConfig, GraphTransform, StackingMode, VizLegendOptions } from '@grafana/schema';
|
||||
import { orderBy } from 'lodash';
|
||||
import uPlot, { AlignedData, Options, PaddingSide } from 'uplot';
|
||||
import { attachDebugger } from '../../utils';
|
||||
import { createLogger } from '../../utils/logger';
|
||||
|
||||
const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g;
|
||||
export const INTERNAL_NEGATIVE_Y_PREFIX = '__internalNegY';
|
||||
|
||||
export function timeFormatToTemplate(f: string) {
|
||||
return f.replace(ALLOWED_FORMAT_STRINGS_REGEX, (match) => `{${match}}`);
|
||||
@ -60,7 +61,16 @@ export function preparePlotData(
|
||||
}
|
||||
|
||||
collectStackingGroups(f, stackingGroups, seriesIndex);
|
||||
result.push(f.values.toArray());
|
||||
const customConfig: GraphFieldConfig = f.config.custom || {};
|
||||
|
||||
const values = f.values.toArray();
|
||||
if (customConfig.transform === GraphTransform.NegativeY) {
|
||||
result.push(values.map((v) => v * -1));
|
||||
} else if (customConfig.transform === GraphTransform.Constant) {
|
||||
result.push(new Array(values.length).fill(values[0]));
|
||||
} else {
|
||||
result.push(values);
|
||||
}
|
||||
seriesIndex++;
|
||||
}
|
||||
|
||||
@ -128,10 +138,15 @@ export function collectStackingGroups(f: Field, groups: Map<string, number[]>, s
|
||||
customConfig.stacking?.group &&
|
||||
!customConfig.hideFrom?.viz
|
||||
) {
|
||||
if (!groups.has(customConfig.stacking.group)) {
|
||||
groups.set(customConfig.stacking.group, [seriesIdx]);
|
||||
const group =
|
||||
customConfig.transform === GraphTransform.NegativeY
|
||||
? `${INTERNAL_NEGATIVE_Y_PREFIX}-${customConfig.stacking.group}`
|
||||
: customConfig.stacking.group;
|
||||
|
||||
if (!groups.has(group)) {
|
||||
groups.set(group, [seriesIdx]);
|
||||
} else {
|
||||
groups.set(customConfig.stacking.group, groups.get(customConfig.stacking.group)!.concat(seriesIdx));
|
||||
groups.set(group, groups.get(group)!.concat(seriesIdx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
VisibilityMode,
|
||||
StackingMode,
|
||||
GraphTresholdsStyleMode,
|
||||
GraphTransform,
|
||||
} from '@grafana/schema';
|
||||
|
||||
import { graphFieldOptions, commonOptionsBuilder } from '@grafana/ui';
|
||||
@ -181,6 +182,29 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption
|
||||
});
|
||||
|
||||
commonOptionsBuilder.addStackingConfig(builder, cfg.stacking, categoryStyles);
|
||||
|
||||
builder.addSelect({
|
||||
category: categoryStyles,
|
||||
name: 'Transform',
|
||||
path: 'transform',
|
||||
settings: {
|
||||
options: [
|
||||
{
|
||||
label: 'Constant',
|
||||
value: GraphTransform.Constant,
|
||||
description: 'The first value will be shown as a constant line',
|
||||
},
|
||||
{
|
||||
label: 'Negative Y',
|
||||
value: GraphTransform.NegativeY,
|
||||
description: 'Flip the results to negative values on the y axis',
|
||||
},
|
||||
],
|
||||
isClearable: true,
|
||||
},
|
||||
hideFromDefaults: true,
|
||||
});
|
||||
|
||||
commonOptionsBuilder.addAxisConfig(builder, cfg);
|
||||
commonOptionsBuilder.addHideFrom(builder);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user