TimeSeries: sync minimum bar width across all bar series (#48030)

This commit is contained in:
Leon Sorokin 2022-04-22 19:33:25 -05:00 committed by GitHub
parent 2b2f275a08
commit 1c977281c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 367 additions and 18 deletions

View File

@ -1,6 +1,8 @@
import {
ArrayVector,
createTheme,
DashboardCursorSync,
DataFrame,
DefaultTimeZone,
EventBusSrv,
FieldConfig,
@ -204,4 +206,301 @@ describe('GraphNG utils', () => {
}).getConfig();
expect(result).toMatchSnapshot();
});
test('preparePlotFrame appends min bar spaced nulls when > 1 bar series', () => {
const df1: DataFrame = {
name: 'A',
length: 5,
fields: [
{
name: 'time',
type: FieldType.time,
config: {},
values: new ArrayVector([1, 2, 4, 6, 100]), // should find smallest delta === 1 from here
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Bars,
},
},
values: new ArrayVector([1, 1, 1, 1, 1]),
},
],
};
const df2: DataFrame = {
name: 'B',
length: 5,
fields: [
{
name: 'time',
type: FieldType.time,
config: {},
values: new ArrayVector([30, 40, 50, 90, 100]), // should be appended with two smallest-delta increments
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Bars,
},
},
values: new ArrayVector([2, 2, 2, 2, 2]), // bar series should be appended with nulls
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Line,
},
},
values: new ArrayVector([3, 3, 3, 3, 3]), // line series should be appended with undefineds
},
],
};
const df3: DataFrame = {
name: 'C',
length: 2,
fields: [
{
name: 'time',
type: FieldType.time,
config: {},
values: new ArrayVector([1, 1.1]), // should not trip up on smaller deltas of non-bars
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Line,
},
},
values: new ArrayVector([4, 4]),
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Bars,
hideFrom: {
viz: true, // should ignore hidden bar series
},
},
},
values: new ArrayVector([4, 4]),
},
],
};
let aligndFrame = preparePlotFrame([df1, df2, df3], {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
});
expect(aligndFrame).toMatchInlineSnapshot(`
Object {
"fields": Array [
Object {
"config": Object {},
"name": "time",
"state": Object {
"origin": Object {
"fieldIndex": 0,
"frameIndex": 0,
},
},
"type": "time",
"values": Array [
1,
1.1,
2,
4,
6,
30,
40,
50,
90,
100,
101,
102,
],
},
Object {
"config": Object {
"custom": Object {
"drawStyle": "bars",
"spanNulls": -1,
},
},
"labels": Object {
"name": "A",
},
"name": "value",
"state": Object {
"origin": Object {
"fieldIndex": 1,
"frameIndex": 0,
},
},
"type": "number",
"values": Array [
1,
undefined,
1,
1,
1,
undefined,
undefined,
undefined,
undefined,
1,
null,
null,
],
},
Object {
"config": Object {
"custom": Object {
"drawStyle": "bars",
"spanNulls": -1,
},
},
"labels": Object {
"name": "B",
},
"name": "value",
"state": Object {
"origin": Object {
"fieldIndex": 1,
"frameIndex": 1,
},
},
"type": "number",
"values": Array [
undefined,
undefined,
undefined,
undefined,
undefined,
2,
2,
2,
2,
2,
null,
null,
],
},
Object {
"config": Object {
"custom": Object {
"drawStyle": "line",
},
},
"labels": Object {
"name": "B",
},
"name": "value",
"state": Object {
"origin": Object {
"fieldIndex": 2,
"frameIndex": 1,
},
},
"type": "number",
"values": Array [
undefined,
undefined,
undefined,
undefined,
undefined,
3,
3,
3,
3,
3,
undefined,
undefined,
],
},
Object {
"config": Object {
"custom": Object {
"drawStyle": "line",
},
},
"labels": Object {
"name": "C",
},
"name": "value",
"state": Object {
"origin": Object {
"fieldIndex": 1,
"frameIndex": 2,
},
},
"type": "number",
"values": Array [
4,
4,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
],
},
Object {
"config": Object {
"custom": Object {
"drawStyle": "bars",
"hideFrom": Object {
"viz": true,
},
},
},
"labels": Object {
"name": "C",
},
"name": "value",
"state": Object {
"origin": Object {
"fieldIndex": 2,
"frameIndex": 2,
},
},
"type": "number",
"values": Array [
4,
4,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
],
},
],
"length": 10,
}
`);
});
});

View File

@ -1,4 +1,4 @@
import { ArrayVector, DataFrame, FieldConfig, FieldType, outerJoinDataFrames, TimeRange } from '@grafana/data';
import { ArrayVector, DataFrame, Field, FieldConfig, FieldType, outerJoinDataFrames, TimeRange } from '@grafana/data';
import {
AxisPlacement,
GraphDrawStyle,
@ -12,6 +12,10 @@ import { applyNullInsertThreshold } from './nullInsertThreshold';
import { nullToUndefThreshold } from './nullToUndefThreshold';
import { XYFieldMatchers } from './types';
function isVisibleBarField(f: Field) {
return f.config.custom?.drawStyle === GraphDrawStyle.Bars && !f.config.custom?.hideFrom?.viz;
}
// will mutate the DataFrame's fields' values
function applySpanNullsThresholds(frame: DataFrame) {
let refField = frame.fields.find((field) => field.type === FieldType.time); // this doesnt need to be time, just any numeric/asc join field
@ -20,7 +24,7 @@ function applySpanNullsThresholds(frame: DataFrame) {
for (let i = 0; i < frame.fields.length; i++) {
let field = frame.fields[i];
if (field === refField) {
if (field === refField || isVisibleBarField(field)) {
continue;
}
@ -37,30 +41,76 @@ function applySpanNullsThresholds(frame: DataFrame) {
}
export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) {
let alignedFrame = outerJoinDataFrames({
frames: frames.map((frame) => {
let fr = applyNullInsertThreshold(frame, null, timeRange?.to.valueOf());
// apply null insertions at interval
frames = frames.map((frame) => applyNullInsertThreshold(frame, null, timeRange?.to.valueOf()));
// prevent minesweeper-expansion of nulls (gaps) when joining bars
// since bar width is determined from the minimum distance between non-undefined values
// (this strategy will still retain any original pre-join nulls, though)
fr.fields.forEach((f) => {
if (f.type === FieldType.number && f.config.custom?.drawStyle === GraphDrawStyle.Bars) {
f.config.custom = {
...f.config.custom,
spanNulls: -1,
};
let numBarSeries = 0;
frames.forEach((frame) => {
frame.fields.forEach((f) => {
if (isVisibleBarField(f)) {
// prevent minesweeper-expansion of nulls (gaps) when joining bars
// since bar width is determined from the minimum distance between non-undefined values
// (this strategy will still retain any original pre-join nulls, though)
f.config.custom = {
...f.config.custom,
spanNulls: -1,
};
numBarSeries++;
}
});
});
// to make bar widths of all series uniform (equal to narrowest bar series), find smallest distance between x points
let minXDelta = Infinity;
if (numBarSeries > 1) {
frames.forEach((frame) => {
if (!frame.fields.some(isVisibleBarField)) {
return;
}
const xVals = frame.fields[0].values.toArray();
for (let i = 0; i < xVals.length; i++) {
if (i > 0) {
minXDelta = Math.min(minXDelta, xVals[i] - xVals[i - 1]);
}
});
}
});
}
return fr;
}),
let alignedFrame = outerJoinDataFrames({
frames,
joinBy: dimFields.x,
keep: dimFields.y,
keepOriginIndices: true,
});
return alignedFrame && applySpanNullsThresholds(alignedFrame);
if (alignedFrame) {
alignedFrame = applySpanNullsThresholds(alignedFrame);
// append 2 null vals at minXDelta to bar series
if (minXDelta !== Infinity) {
alignedFrame.fields.forEach((f, fi) => {
let vals = f.values.toArray();
if (fi === 0) {
let lastVal = vals[vals.length - 1];
vals.push(lastVal + minXDelta, lastVal + 2 * minXDelta);
} else if (isVisibleBarField(f)) {
vals.push(null, null);
} else {
vals.push(undefined, undefined);
}
});
}
return alignedFrame;
}
return null;
}
export function buildScaleKey(config: FieldConfig<GraphFieldConfig>) {