diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx index 13eeaa98c16..39805de26ee 100755 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -29,7 +29,7 @@ import { isNumber } from 'lodash'; const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); export interface XYFieldMatchers { - x: FieldMatcher; + x: FieldMatcher; // first match y: FieldMatcher; } export interface GraphNGProps extends Omit { diff --git a/packages/grafana-ui/src/components/GraphNG/utils.test.ts b/packages/grafana-ui/src/components/GraphNG/utils.test.ts index d59005e198a..c97ffd88a80 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.test.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.test.ts @@ -1,6 +1,6 @@ import { ArrayVector, DataFrame, FieldType, toDataFrame } from '@grafana/data'; import { AlignedFrameWithGapTest } from '../uPlot/types'; -import { alignDataFrames } from './utils'; +import { alignDataFrames, isLikelyAscendingVector } from './utils'; describe('alignDataFrames', () => { describe('aligned frame', () => { @@ -215,4 +215,31 @@ describe('alignDataFrames', () => { }); }); }); + + describe('check ascending data', () => { + it('simple ascending', () => { + const v = new ArrayVector([1, 2, 3, 4, 5]); + expect(isLikelyAscendingVector(v)).toBeTruthy(); + }); + it('simple ascending with null', () => { + const v = new ArrayVector([null, 2, 3, 4, null]); + expect(isLikelyAscendingVector(v)).toBeTruthy(); + }); + it('single value', () => { + const v = new ArrayVector([null, null, null, 4, null]); + expect(isLikelyAscendingVector(v)).toBeTruthy(); + expect(isLikelyAscendingVector(new ArrayVector([4]))).toBeTruthy(); + expect(isLikelyAscendingVector(new ArrayVector([]))).toBeTruthy(); + }); + + it('middle values', () => { + const v = new ArrayVector([null, null, 5, 4, null]); + expect(isLikelyAscendingVector(v)).toBeFalsy(); + }); + + it('decending', () => { + expect(isLikelyAscendingVector(new ArrayVector([7, 6, null]))).toBeFalsy(); + expect(isLikelyAscendingVector(new ArrayVector([7, 8, 6]))).toBeFalsy(); + }); + }); }); diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index 3fb355a8dbd..57b631badf6 100755 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -9,6 +9,8 @@ import { FieldType, FieldState, DataFrameFieldIndex, + sortDataFrame, + Vector, } from '@grafana/data'; import { AlignedFrameWithGapTest } from '../uPlot/types'; import uPlot, { AlignedData, JoinNullMode } from 'uplot'; @@ -16,24 +18,23 @@ import { XYFieldMatchers } from './GraphNG'; // the results ofter passing though data export interface XYDimensionFields { - x: Field[]; - y: Field[]; + x: Field; // independent axis (cause) + y: Field[]; // dependent axis (effect) } export function mapDimesions(match: XYFieldMatchers, frame: DataFrame, frames?: DataFrame[]): XYDimensionFields { - const out: XYDimensionFields = { - x: [], - y: [], - }; + let x: Field | undefined; + const y: Field[] = []; + for (const field of frame.fields) { - if (match.x(field, frame, frames ?? [])) { - out.x.push(field); + if (!x && match.x(field, frame, frames ?? [])) { + x = field; } if (match.y(field, frame, frames ?? [])) { - out.y.push(field); + y.push(field); } } - return out; + return { x: x as Field, y }; } /** @@ -58,26 +59,29 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): } for (let frameIndex = 0; frameIndex < frames.length; frameIndex++) { - const frame = frames[frameIndex]; - const dims = mapDimesions(fields, frame, frames); + let frame = frames[frameIndex]; + let dims = mapDimesions(fields, frame, frames); - if (!(dims.x.length && dims.y.length)) { + if (!(dims.x && dims.y.length)) { continue; // no numeric and no time fields } - if (dims.x.length > 1) { - throw new Error('Only a single x field is supported'); + // Quick check that x is ascending order + if (!isLikelyAscendingVector(dims.x.values)) { + const xIndex = frame.fields.indexOf(dims.x); + frame = sortDataFrame(frame, xIndex); + dims = mapDimesions(fields, frame, frames); } let nullModesFrame: JoinNullMode[] = [0]; // Add the first X axis if (!sourceFields.length) { - sourceFields.push(dims.x[0]); + sourceFields.push(dims.x); } const alignedData: AlignedData = [ - dims.x[0].values.toArray(), // The x axis (time) + dims.x.values.toArray(), // The x axis (time) ]; for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) { @@ -150,3 +154,34 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): }, }; } + +// Quick test if the first and last points look to be ascending +export function isLikelyAscendingVector(data: Vector): boolean { + let first: any = undefined; + + for (let idx = 0; idx < data.length; idx++) { + const v = data.get(idx); + if (v != null) { + if (first != null) { + if (first > v) { + return false; // descending + } + break; + } + first = v; + } + } + + let idx = data.length - 1; + while (idx >= 0) { + const v = data.get(idx--); + if (v != null) { + if (first > v) { + return false; + } + return true; + } + } + + return true; // only one non-null point +}