diff --git a/public/app/core/components/GraphNG/utils.test.ts b/public/app/core/components/GraphNG/utils.test.ts index faf0953967f..19d1ec75997 100644 --- a/public/app/core/components/GraphNG/utils.test.ts +++ b/public/app/core/components/GraphNG/utils.test.ts @@ -513,4 +513,288 @@ describe('GraphNG utils', () => { } `); }); + + test('preparePlotFrame DOES NOT append min bar spaced nulls when all visible bar series have same min spacing', () => { + const df1: DataFrame = { + name: 'A', + length: 5, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [1, 2, 4, 6, 100], // should find smallest delta === 1 from here + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + }, + }, + values: [1, 1, 1, 1, 1], + }, + ], + }; + + const df2: DataFrame = { + name: 'B', + length: 5, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [30, 31, 50, 90, 100], + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + }, + }, + values: [2, 2, 2, 2, 2], + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Line, + }, + }, + values: [3, 3, 3, 3, 3], + }, + ], + }; + + const df3: DataFrame = { + name: 'C', + length: 2, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [1, 1.1], // should not trip up on smaller deltas of non-bars + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Line, + }, + }, + values: [4, 4], + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + hideFrom: { + viz: true, // should ignore hidden bar series + }, + }, + }, + values: [4, 4], + }, + ], + }; + + let aligndFrame = preparePlotFrame([df1, df2, df3], { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.numeric).get({}), + }); + + expect(aligndFrame).toMatchInlineSnapshot(` + { + "fields": [ + { + "config": {}, + "name": "time", + "state": { + "nullThresholdApplied": true, + "origin": { + "fieldIndex": 0, + "frameIndex": 0, + }, + }, + "type": "time", + "values": [ + 1, + 1.1, + 2, + 4, + 6, + 30, + 31, + 50, + 90, + 100, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + }, + }, + "labels": { + "name": "A", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 0, + }, + }, + "type": "number", + "values": [ + 1, + undefined, + 1, + 1, + 1, + undefined, + undefined, + undefined, + undefined, + 1, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + }, + }, + "labels": { + "name": "B", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 1, + }, + }, + "type": "number", + "values": [ + undefined, + undefined, + undefined, + undefined, + undefined, + 2, + 2, + 2, + 2, + 2, + ], + }, + { + "config": { + "custom": { + "drawStyle": "line", + }, + }, + "labels": { + "name": "B", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 2, + "frameIndex": 1, + }, + }, + "type": "number", + "values": [ + undefined, + undefined, + undefined, + undefined, + undefined, + 3, + 3, + 3, + 3, + 3, + ], + }, + { + "config": { + "custom": { + "drawStyle": "line", + }, + }, + "labels": { + "name": "C", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 2, + }, + }, + "type": "number", + "values": [ + 4, + 4, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + "hideFrom": { + "viz": true, + }, + }, + }, + "labels": { + "name": "C", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 2, + "frameIndex": 2, + }, + }, + "type": "number", + "values": [ + 4, + 4, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ], + }, + ], + "length": 10, + } + `); + }); }); diff --git a/public/app/core/components/GraphNG/utils.ts b/public/app/core/components/GraphNG/utils.ts index 34757b4d81a..89133149dc0 100644 --- a/public/app/core/components/GraphNG/utils.ts +++ b/public/app/core/components/GraphNG/utils.ts @@ -1,4 +1,12 @@ -import { DataFrame, Field, FieldType, outerJoinDataFrames, TimeRange, applyNullInsertThreshold } from '@grafana/data'; +import { + DataFrame, + Field, + FieldType, + outerJoinDataFrames, + TimeRange, + applyNullInsertThreshold, + roundDecimals, +} from '@grafana/data'; import { NULL_EXPAND, NULL_REMOVE, NULL_RETAIN, nullToUndefThreshold } from '@grafana/data/internal'; import { GraphDrawStyle } from '@grafana/schema'; @@ -42,20 +50,22 @@ function applySpanNullsThresholds(frame: DataFrame, refFieldName?: string | null return frame; } -export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) { - let xField: Field; - loop: for (let frame of frames) { - for (let field of frame.fields) { - if (dimFields.x(field, frame, frames)) { - xField = field; - break loop; - } +function getXField(dimFields: XYFieldMatchers, frame: DataFrame, frames: DataFrame[]) { + for (let field of frame.fields) { + if (dimFields.x(field, frame, frames)) { + return field; } } + return; +} + +export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) { // apply null insertions at interval frames = frames.map((frame) => { - if (!xField?.state?.nullThresholdApplied) { + const xField = getXField(dimFields, frame, frames); + + if (xField != null && !xField.state?.nullThresholdApplied) { return applyNullInsertThreshold({ frame, refFieldName: xField.name, @@ -73,22 +83,43 @@ export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers ); // to make bar widths of all series uniform (equal to narrowest bar series), find smallest distance between x points - let minXDelta = Infinity; + let minXDeltaGlobal: number | null = null; if (numBarSeries > 1) { + // collect for each frame and only set minXDeltaGlobal if they're different + const minXDeltas = new Set(); + frames.forEach((frame) => { if (!frame.fields.some(isVisibleBarField)) { return; } + const xField = getXField(dimFields, frame, frames); + + if (xField == null) { + return; + } + + let minXDeltaFrame = Infinity; + const xVals = xField.values; for (let i = 0; i < xVals.length; i++) { if (i > 0) { - minXDelta = Math.min(minXDelta, xVals[i] - xVals[i - 1]); + minXDeltaFrame = Math.min(minXDeltaFrame, xVals[i] - xVals[i - 1]); } } + + if (!Number.isInteger(minXDeltaFrame)) { + minXDeltaFrame = roundDecimals(minXDeltaFrame, 6); + } + + minXDeltas.add(minXDeltaFrame); }); + + if (minXDeltas.size > 1) { + minXDeltaGlobal = Math.min(...minXDeltas); + } } let alignedFrame = outerJoinDataFrames({ @@ -116,16 +147,16 @@ export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers }); if (alignedFrame) { - alignedFrame = applySpanNullsThresholds(alignedFrame, xField!.name); + alignedFrame = applySpanNullsThresholds(alignedFrame, alignedFrame.fields[0].name); - // append 2 null vals at minXDelta to bar series - if (minXDelta !== Infinity) { + // append 2 null vals at minXDeltaGlobal to bar series + if (minXDeltaGlobal != null) { alignedFrame.fields.forEach((f, fi) => { let vals = f.values; if (fi === 0) { let lastVal = vals[vals.length - 1]; - vals.push(lastVal + minXDelta, lastVal + 2 * minXDelta); + vals.push(lastVal + minXDeltaGlobal, lastVal + 2 * minXDeltaGlobal); } else if (isVisibleBarField(f)) { vals.push(null, null); } else {