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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 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" >}}).

View File

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

View File

@ -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")

View File

@ -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 {

View File

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

View File

@ -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}
/>
);

View File

@ -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,
});
}
}

View File

@ -9,7 +9,7 @@ import { useStyles2 } from '../../themes';
*/
export interface SeriesTableRowProps {
color?: string;
label?: string;
label?: React.ReactNode;
value?: string | GraphSeriesValue;
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 display = field.display!(v);
sortIdx.push([series.length, v]);
series.push({
color: display.color || FALLBACK_COLOR,

View File

@ -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: [

View File

@ -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));
}
}
}

View File

@ -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);