Trend: Fix x-axis max affected by null-append to bar series (#110322)

This commit is contained in:
Leon Sorokin
2025-08-29 09:47:14 -05:00
committed by GitHub
parent 6952461362
commit de1cc4c1a7
2 changed files with 331 additions and 16 deletions
@@ -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,
}
`);
});
});
+47 -16
View File
@@ -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<number>();
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 {