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:
Dominik Prokop
2022-02-08 07:27:02 -08:00
committed by GitHub
parent ea236c276e
commit ffea53f2f6
12 changed files with 204 additions and 10 deletions

View File

@@ -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 stacked time series]({{< relref "./graph-time-series-stacking.md" >}})
- [Graph and color schemes]({{< relref "./graph-color-scheme.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 ## Axis
For more information about adjusting your time series axes, refer to [Change axis display]({{< relref "change-axis-display.md" >}}). For more information about adjusting your time series axes, refer to [Change axis display]({{< relref "change-axis-display.md" >}}).

View File

@@ -74,6 +74,8 @@ export const valueMappingsOverrideProcessor = (
export interface SelectFieldConfigSettings<T> { export interface SelectFieldConfigSettings<T> {
allowCustomValue?: boolean; allowCustomValue?: boolean;
isClearable?: boolean;
/** The default options */ /** The default options */
options: Array<SelectableValue<T>>; options: Array<SelectableValue<T>>;

View File

@@ -7,6 +7,7 @@ LineInterpolation: "linear" | "smooth" | "stepBefore" | "stepAfter" @cue
ScaleDistribution: "linear" | "log" | "ordinal" @cuetsy(kind="enum") ScaleDistribution: "linear" | "log" | "ordinal" @cuetsy(kind="enum")
GraphGradientMode: "none" | "opacity" | "hue" | "scheme" @cuetsy(kind="enum") GraphGradientMode: "none" | "opacity" | "hue" | "scheme" @cuetsy(kind="enum")
StackingMode: "none" | "normal" | "percent" @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") BarAlignment: -1 | 0 | 1 @cuetsy(kind="enum",memberNames="Before|Center|After")
ScaleOrientation: 0 | 1 @cuetsy(kind="enum",memberNames="Horizontal|Vertical") ScaleOrientation: 0 | 1 @cuetsy(kind="enum",memberNames="Horizontal|Vertical")
ScaleDirection: 1 | 1 | -1 | -1 @cuetsy(kind="enum",memberNames="Up|Right|Down|Left") ScaleDirection: 1 | 1 | -1 | -1 @cuetsy(kind="enum",memberNames="Up|Right|Down|Left")
@@ -84,4 +85,5 @@ GraphFieldConfig: {
drawStyle?: GraphDrawStyle drawStyle?: GraphDrawStyle
gradientMode?: GraphGradientMode gradientMode?: GraphGradientMode
thresholdsStyle?: GraphThresholdsStyleConfig thresholdsStyle?: GraphThresholdsStyleConfig
transform?: GraphTransform
} @cuetsy(kind="interface") } @cuetsy(kind="interface")

View File

@@ -24,6 +24,11 @@ export enum GraphDrawStyle {
Points = 'points', Points = 'points',
} }
export enum GraphTransform {
Constant = 'constant',
NegativeY = 'negative-Y',
}
export enum LineInterpolation { export enum LineInterpolation {
Linear = 'linear', Linear = 'linear',
Smooth = 'smooth', Smooth = 'smooth',
@@ -256,6 +261,7 @@ export interface GraphFieldConfig
drawStyle?: GraphDrawStyle; drawStyle?: GraphDrawStyle;
gradientMode?: GraphGradientMode; gradientMode?: GraphGradientMode;
thresholdsStyle?: GraphThresholdsStyleConfig; thresholdsStyle?: GraphThresholdsStyleConfig;
transform?: GraphTransform;
} }
export interface VizLegendOptions { export interface VizLegendOptions {

View File

@@ -60,12 +60,14 @@ Object {
], ],
"bands": Array [ "bands": Array [
Object { Object {
"dir": -1,
"series": Array [ "series": Array [
2, 2,
1, 1,
], ],
}, },
Object { Object {
"dir": -1,
"series": Array [ "series": Array [
4, 4,
3, 3,

View File

@@ -67,7 +67,8 @@ export class SelectValueEditor<T> extends React.PureComponent<Props<T>, State<T>
value={current} value={current}
defaultValue={value} defaultValue={value}
allowCustomValue={settings?.allowCustomValue} allowCustomValue={settings?.allowCustomValue}
onChange={(e) => onChange(e.value)} isClearable={settings?.isClearable}
onChange={(e) => onChange(e?.value)}
options={options} options={options}
/> />
); );

View File

@@ -26,7 +26,7 @@ import {
VizLegendOptions, VizLegendOptions,
StackingMode, StackingMode,
} from '@grafana/schema'; } 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 uPlot from 'uplot';
import { buildScaleKey } from '../GraphNG/utils'; import { buildScaleKey } from '../GraphNG/utils';
@@ -332,11 +332,12 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
} }
if (stackingGroups.size !== 0) { if (stackingGroups.size !== 0) {
for (const [_, seriesIds] of stackingGroups.entries()) { for (const [group, seriesIds] of stackingGroups.entries()) {
const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame }); const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame });
for (let j = seriesIdxs.length - 1; j > 0; j--) { for (let j = seriesIdxs.length - 1; j > 0; j--) {
builder.addBand({ builder.addBand({
series: [seriesIdxs[j], seriesIdxs[j - 1]], series: [seriesIdxs[j], seriesIdxs[j - 1]],
dir: group.startsWith(INTERNAL_NEGATIVE_Y_PREFIX) ? 1 : -1,
}); });
} }
} }

View File

@@ -9,7 +9,7 @@ import { useStyles2 } from '../../themes';
*/ */
export interface SeriesTableRowProps { export interface SeriesTableRowProps {
color?: string; color?: string;
label?: string; label?: React.ReactNode;
value?: string | GraphSeriesValue; value?: string | GraphSeriesValue;
isActive?: boolean; isActive?: boolean;
} }

View File

@@ -231,6 +231,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
const v = otherProps.data.fields[i].values.get(focusedPointIdxs[i]!); const v = otherProps.data.fields[i].values.get(focusedPointIdxs[i]!);
const display = field.display!(v); const display = field.display!(v);
sortIdx.push([series.length, v]); sortIdx.push([series.length, v]);
series.push({ series.push({
color: display.color || FALLBACK_COLOR, color: display.color || FALLBACK_COLOR,

View File

@@ -1,6 +1,6 @@
import { orderIdsByCalcs, preparePlotData, timeFormatToTemplate } from './utils'; import { orderIdsByCalcs, preparePlotData, timeFormatToTemplate } from './utils';
import { FieldType, MutableDataFrame } from '@grafana/data'; import { FieldType, MutableDataFrame } from '@grafana/data';
import { StackingMode } from '@grafana/schema'; import { GraphTransform, StackingMode } from '@grafana/schema';
describe('timeFormatToTemplate', () => { describe('timeFormatToTemplate', () => {
it.each` 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', () => { describe('stacking', () => {
it('none', () => { it('none', () => {
const df = new MutableDataFrame({ 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', () => { it('standard with multiple groups', () => {
const df = new MutableDataFrame({ const df = new MutableDataFrame({
fields: [ fields: [

View File

@@ -1,11 +1,12 @@
import { DataFrame, ensureTimeField, Field, FieldType } from '@grafana/data'; 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 { orderBy } from 'lodash';
import uPlot, { AlignedData, Options, PaddingSide } from 'uplot'; import uPlot, { AlignedData, Options, PaddingSide } from 'uplot';
import { attachDebugger } from '../../utils'; import { attachDebugger } from '../../utils';
import { createLogger } from '../../utils/logger'; 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; 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) { export function timeFormatToTemplate(f: string) {
return f.replace(ALLOWED_FORMAT_STRINGS_REGEX, (match) => `{${match}}`); return f.replace(ALLOWED_FORMAT_STRINGS_REGEX, (match) => `{${match}}`);
@@ -60,7 +61,16 @@ export function preparePlotData(
} }
collectStackingGroups(f, stackingGroups, seriesIndex); 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++; seriesIndex++;
} }
@@ -128,10 +138,15 @@ export function collectStackingGroups(f: Field, groups: Map<string, number[]>, s
customConfig.stacking?.group && customConfig.stacking?.group &&
!customConfig.hideFrom?.viz !customConfig.hideFrom?.viz
) { ) {
if (!groups.has(customConfig.stacking.group)) { const group =
groups.set(customConfig.stacking.group, [seriesIdx]); customConfig.transform === GraphTransform.NegativeY
? `${INTERNAL_NEGATIVE_Y_PREFIX}-${customConfig.stacking.group}`
: customConfig.stacking.group;
if (!groups.has(group)) {
groups.set(group, [seriesIdx]);
} else { } else {
groups.set(customConfig.stacking.group, groups.get(customConfig.stacking.group)!.concat(seriesIdx)); groups.set(group, groups.get(group)!.concat(seriesIdx));
} }
} }
} }

View File

@@ -16,6 +16,7 @@ import {
VisibilityMode, VisibilityMode,
StackingMode, StackingMode,
GraphTresholdsStyleMode, GraphTresholdsStyleMode,
GraphTransform,
} from '@grafana/schema'; } from '@grafana/schema';
import { graphFieldOptions, commonOptionsBuilder } from '@grafana/ui'; import { graphFieldOptions, commonOptionsBuilder } from '@grafana/ui';
@@ -181,6 +182,29 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption
}); });
commonOptionsBuilder.addStackingConfig(builder, cfg.stacking, categoryStyles); 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.addAxisConfig(builder, cfg);
commonOptionsBuilder.addHideFrom(builder); commonOptionsBuilder.addHideFrom(builder);