mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TimeSeries: sync minimum bar width across all bar series (#48030)
This commit is contained in:
parent
2b2f275a08
commit
1c977281c8
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
@ -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>) {
|
||||
|
Loading…
Reference in New Issue
Block a user