TimeSeries: sync minimum bar width across all bar series (#48030)
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
|
ArrayVector,
|
||||||
createTheme,
|
createTheme,
|
||||||
DashboardCursorSync,
|
DashboardCursorSync,
|
||||||
|
DataFrame,
|
||||||
DefaultTimeZone,
|
DefaultTimeZone,
|
||||||
EventBusSrv,
|
EventBusSrv,
|
||||||
FieldConfig,
|
FieldConfig,
|
||||||
@@ -204,4 +206,301 @@ describe('GraphNG utils', () => {
|
|||||||
}).getConfig();
|
}).getConfig();
|
||||||
expect(result).toMatchSnapshot();
|
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,
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
AxisPlacement,
|
AxisPlacement,
|
||||||
GraphDrawStyle,
|
GraphDrawStyle,
|
||||||
@@ -12,6 +12,10 @@ import { applyNullInsertThreshold } from './nullInsertThreshold';
|
|||||||
import { nullToUndefThreshold } from './nullToUndefThreshold';
|
import { nullToUndefThreshold } from './nullToUndefThreshold';
|
||||||
import { XYFieldMatchers } from './types';
|
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
|
// will mutate the DataFrame's fields' values
|
||||||
function applySpanNullsThresholds(frame: DataFrame) {
|
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
|
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++) {
|
for (let i = 0; i < frame.fields.length; i++) {
|
||||||
let field = frame.fields[i];
|
let field = frame.fields[i];
|
||||||
|
|
||||||
if (field === refField) {
|
if (field === refField || isVisibleBarField(field)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,30 +41,76 @@ function applySpanNullsThresholds(frame: DataFrame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) {
|
export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) {
|
||||||
let alignedFrame = outerJoinDataFrames({
|
// apply null insertions at interval
|
||||||
frames: frames.map((frame) => {
|
frames = frames.map((frame) => applyNullInsertThreshold(frame, null, timeRange?.to.valueOf()));
|
||||||
let fr = applyNullInsertThreshold(frame, null, timeRange?.to.valueOf());
|
|
||||||
|
|
||||||
// prevent minesweeper-expansion of nulls (gaps) when joining bars
|
let numBarSeries = 0;
|
||||||
// since bar width is determined from the minimum distance between non-undefined values
|
|
||||||
// (this strategy will still retain any original pre-join nulls, though)
|
frames.forEach((frame) => {
|
||||||
fr.fields.forEach((f) => {
|
frame.fields.forEach((f) => {
|
||||||
if (f.type === FieldType.number && f.config.custom?.drawStyle === GraphDrawStyle.Bars) {
|
if (isVisibleBarField(f)) {
|
||||||
f.config.custom = {
|
// prevent minesweeper-expansion of nulls (gaps) when joining bars
|
||||||
...f.config.custom,
|
// since bar width is determined from the minimum distance between non-undefined values
|
||||||
spanNulls: -1,
|
// (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,
|
joinBy: dimFields.x,
|
||||||
keep: dimFields.y,
|
keep: dimFields.y,
|
||||||
keepOriginIndices: true,
|
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>) {
|
export function buildScaleKey(config: FieldConfig<GraphFieldConfig>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user