From 854f872b40bbf1f41ba4604a48e48d1906bf3a94 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 2 Mar 2022 18:21:12 -0600 Subject: [PATCH] StateTimeline: insert trailing null value at +interval (#45997) --- .../src/components/GraphNG/GraphNG.tsx | 3 +- .../GraphNG/nullInsertThreshold.test.ts | 30 +++++++++---------- .../components/GraphNG/nullInsertThreshold.ts | 16 ++++++++-- .../src/components/GraphNG/utils.ts | 6 ++-- .../app/plugins/panel/state-timeline/utils.ts | 11 ------- 5 files changed, 34 insertions(+), 32 deletions(-) diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx index e33de639ef2..1581c4abd34 100755 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -116,7 +116,8 @@ export class GraphNG extends React.Component { fields || { x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), y: fieldMatchers.get(FieldMatcherID.numeric).get({}), - } + }, + props.timeRange ); pluginLog('GraphNG', false, 'data aligned', alignedFrame); diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts index 01ec591f9ca..144f9ddb7d8 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts +++ b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts @@ -97,39 +97,39 @@ describe('nullInsertThreshold Transformer', () => { expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']); }); - // TODO: make this work - test.skip('should insert nulls at +threshold (when defined) instead of +interval', () => { + test('should insert trailing null at end +interval when timeRange.to.valueOf() exceeds threshold', () => { const df = new MutableDataFrame({ refId: 'A', fields: [ - { name: 'Time', type: FieldType.time, config: { interval: 2 }, values: [5, 7, 11] }, - { name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] }, - { name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] }, + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, ], }); - const result = applyNullInsertThreshold(df); + const result = applyNullInsertThreshold(df, null, 13); - expect(result.fields[0].values.toArray()).toStrictEqual([5, 6, 7, 8, 11]); - expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8]); - expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']); + expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 10, 11]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8, null]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c', null]); }); - test('should insert nulls at midpoints between adjacent > interval: 2', () => { + // TODO: make this work + test.skip('should insert nulls at +threshold (when defined) instead of +interval', () => { const df = new MutableDataFrame({ refId: 'A', fields: [ { name: 'Time', type: FieldType.time, config: { interval: 2 }, values: [5, 7, 11] }, - { name: 'One', type: FieldType.number, values: [4, 6, 8] }, - { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + { name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] }, ], }); const result = applyNullInsertThreshold(df); - expect(result.fields[0].values.toArray()).toStrictEqual([5, 7, 9, 11]); - expect(result.fields[1].values.toArray()).toStrictEqual([4, 6, null, 8]); - expect(result.fields[2].values.toArray()).toStrictEqual(['a', 'b', null, 'c']); + expect(result.fields[0].values.toArray()).toStrictEqual([5, 6, 7, 8, 11]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']); }); test('should noop on fewer than two values', () => { diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts index 4e22d0af2b1..35b0fcd325f 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts +++ b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts @@ -12,6 +12,7 @@ const INSERT_MODES = { export function applyNullInsertThreshold( frame: DataFrame, refFieldName?: string | null, + refFieldPseudoMax: number | null = null, insertMode: InsertMode = INSERT_MODES.threshold ): DataFrame { if (frame.length < 2) { @@ -48,7 +49,7 @@ export function applyNullInsertThreshold( const frameValues = frame.fields.map((field) => field.values.toArray()); - const filledFieldValues = nullInsertThreshold(refValues, frameValues, threshold, insertMode); + const filledFieldValues = nullInsertThreshold(refValues, frameValues, threshold, refFieldPseudoMax, insertMode); if (filledFieldValues === frameValues) { return frame; @@ -70,7 +71,14 @@ export function applyNullInsertThreshold( return frame; } -function nullInsertThreshold(refValues: number[], frameValues: any[][], threshold: number, getInsertValue: InsertMode) { +function nullInsertThreshold( + refValues: number[], + frameValues: any[][], + threshold: number, + // will insert a trailing null when refFieldPseudoMax > last datapoint + threshold + refFieldPseudoMax: number | null = null, + getInsertValue: InsertMode +) { const len = refValues.length; let prevValue: number = refValues[0]; const refValuesNew: number[] = [prevValue]; @@ -87,6 +95,10 @@ function nullInsertThreshold(refValues: number[], frameValues: any[][], threshol prevValue = curValue; } + if (refFieldPseudoMax != null && prevValue + threshold <= refFieldPseudoMax) { + refValuesNew.push(getInsertValue(prevValue, refFieldPseudoMax, threshold)); + } + const filledLen = refValuesNew.length; if (filledLen === len) { diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index cc7feecd4a0..b67d694d793 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -1,5 +1,5 @@ import { XYFieldMatchers } from './types'; -import { ArrayVector, DataFrame, FieldConfig, FieldType, outerJoinDataFrames } from '@grafana/data'; +import { ArrayVector, DataFrame, FieldConfig, FieldType, outerJoinDataFrames, TimeRange } from '@grafana/data'; import { nullToUndefThreshold } from './nullToUndefThreshold'; import { applyNullInsertThreshold } from './nullInsertThreshold'; import { AxisPlacement, GraphFieldConfig, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema'; @@ -29,9 +29,9 @@ function applySpanNullsThresholds(frame: DataFrame) { return frame; } -export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers) { +export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) { let alignedFrame = outerJoinDataFrames({ - frames: frames.map((frame) => applyNullInsertThreshold(frame)), + frames: frames.map((frame) => applyNullInsertThreshold(frame, null, timeRange?.to.valueOf())), joinBy: dimFields.x, keep: dimFields.y, keepOriginIndices: true, diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index dd71932f9f8..0bf27bd90ba 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -1,5 +1,4 @@ import React from 'react'; -import { XYFieldMatchers } from '@grafana/ui/src/components/GraphNG/types'; import { ArrayVector, DataFrame, @@ -19,7 +18,6 @@ import { getActiveThreshold, Threshold, getFieldConfigWithMinMax, - outerJoinDataFrames, ThresholdsMode, } from '@grafana/data'; import { @@ -48,15 +46,6 @@ export function mapMouseEventToMode(event: React.MouseEvent): SeriesVisibilityCh return SeriesVisibilityChangeMode.ToggleSelection; } -export function preparePlotFrame(data: DataFrame[], dimFields: XYFieldMatchers) { - return outerJoinDataFrames({ - frames: data, - joinBy: dimFields.x, - keep: dimFields.y, - keepOriginIndices: true, - }); -} - export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ frame, theme,