mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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:
@@ -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" >}}).
|
||||||
|
|||||||
@@ -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>>;
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user