[graph-ng] add temporal DataFrame alignment/outerJoin & move null-asZero pass inside (#29250)

[GraphNG] update uPlot, add temporal DataFrame alignment/outerJoin, move null-asZero pass inside, merge isGap updates into u.setData() calls.

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Leon Sorokin
2020-11-20 23:49:36 -06:00
committed by GitHub
parent 1076f47509
commit 917b5c5f2a
14 changed files with 211 additions and 194 deletions

172
packages/grafana-ui/src/components/GraphNG/utils.ts Normal file → Executable file
View File

@ -1,31 +1,155 @@
import { DataFrame, FieldType, getTimeField, outerJoinDataFrames, sortDataFrame } from '@grafana/data';
import {
DataFrame,
FieldType,
getTimeField,
ArrayVector,
NullValueMode,
getFieldDisplayName,
Field,
} from '@grafana/data';
import { AlignedFrameWithGapTest } from '../uPlot/types';
import uPlot, { AlignedData, AlignedDataWithGapTest } from 'uplot';
// very time oriented for now
export const alignAndSortDataFramesByFieldName = (data: DataFrame[], fieldName: string): DataFrame | null => {
if (!data.length) {
return null;
}
/**
* Returns a single DataFrame with:
* - A shared time column
* - only numeric fields
*
* The input expects all frames to have a time field with values in ascending order
*
* @alpha
*/
export function mergeTimeSeriesData(frames: DataFrame[]): AlignedFrameWithGapTest | null {
const valuesFromFrames: AlignedData[] = [];
const sourceFields: Field[] = [];
// normalize time field names
// in each frame find first time field and rename it to unified name
for (let i = 0; i < data.length; i++) {
const series = data[i];
for (let j = 0; j < series.fields.length; j++) {
const field = series.fields[j];
if (field.type === FieldType.time) {
field.name = fieldName;
break;
for (const frame of frames) {
const { timeField } = getTimeField(frame);
if (!timeField) {
continue;
}
const alignedData: AlignedData = [
timeField.values.toArray(), // The x axis (time)
];
// find numeric fields
for (const field of frame.fields) {
if (field.type !== FieldType.number) {
continue;
}
let values = field.values.toArray();
if (field.config.nullValueMode === NullValueMode.AsZero) {
values = values.map(v => (v === null ? 0 : v));
}
alignedData.push(values);
// Add the first time field
if (sourceFields.length < 1) {
sourceFields.push(timeField);
}
// This will cache an appropriate field name in the field state
getFieldDisplayName(field, frame, frames);
sourceFields.push(field);
}
// Timeseries has tima and at least one number
if (alignedData.length > 1) {
valuesFromFrames.push(alignedData);
}
}
const dataFramesToPlot = data.filter(frame => {
let { timeIndex } = getTimeField(frame);
// filter out series without time index or if the time column is the only one (i.e. after transformations)
// won't live long as we gona move out from assuming x === time
return timeIndex !== undefined ? frame.fields.length > 1 : false;
});
if (valuesFromFrames.length === 0) {
return null;
}
const aligned = outerJoinDataFrames(dataFramesToPlot, { byField: fieldName })[0];
return sortDataFrame(aligned, getTimeField(aligned).timeIndex!);
};
// do the actual alignment (outerJoin on the first arrays)
const { data: alignedData, isGap } = outerJoinValues(valuesFromFrames);
if (alignedData!.length !== sourceFields.length) {
throw new Error('outerJoinValues lost a field?');
}
// Replace the values from the outer-join field
return {
frame: {
length: alignedData![0].length,
fields: alignedData!.map((vals, idx) => ({
...sourceFields[idx],
values: new ArrayVector(vals),
})),
},
isGap,
};
}
export function outerJoinValues(tables: AlignedData[]): AlignedDataWithGapTest {
if (tables.length === 1) {
return {
data: tables[0],
isGap: () => true,
};
}
let xVals: Set<number> = new Set();
let xNulls: Array<Set<number>> = [new Set()];
for (const t of tables) {
let xs = t[0];
let len = xs.length;
let nulls: Set<number> = new Set();
for (let i = 0; i < len; i++) {
xVals.add(xs[i]);
}
for (let j = 1; j < t.length; j++) {
let ys = t[j];
for (let i = 0; i < len; i++) {
if (ys[i] == null) {
nulls.add(xs[i]);
}
}
}
xNulls.push(nulls);
}
let data: AlignedData = [Array.from(xVals).sort((a, b) => a - b)];
let alignedLen = data[0].length;
let xIdxs = new Map();
for (let i = 0; i < alignedLen; i++) {
xIdxs.set(data[0][i], i);
}
for (const t of tables) {
let xs = t[0];
for (let j = 1; j < t.length; j++) {
let ys = t[j];
let yVals = Array(alignedLen).fill(null);
for (let i = 0; i < ys.length; i++) {
yVals[xIdxs.get(xs[i])] = ys[i];
}
data.push(yVals);
}
}
return {
data: data,
isGap(u: uPlot, seriesIdx: number, dataIdx: number) {
// u.data has to be AlignedDate
let xVal = u.data[0][dataIdx];
return xNulls[seriesIdx].has(xVal!);
},
};
}