mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DataFrame: expose an object array as a data frame (#23494)
This commit is contained in:
95
packages/grafana-data/src/dataframe/ArrayDataFrame.test.ts
Normal file
95
packages/grafana-data/src/dataframe/ArrayDataFrame.test.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { ArrayDataFrame } from './ArrayDataFrame';
|
||||||
|
import { toDataFrameDTO } from './processDataFrame';
|
||||||
|
import { FieldType } from '../types';
|
||||||
|
|
||||||
|
describe('Array DataFrame', () => {
|
||||||
|
const input = [
|
||||||
|
{ name: 'first', value: 1, time: 123 },
|
||||||
|
{ name: 'second', value: 2, time: 456, extra: 'here' },
|
||||||
|
{ name: 'third', value: 3, time: 789 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const frame = new ArrayDataFrame(input);
|
||||||
|
frame.name = 'Hello';
|
||||||
|
frame.refId = 'Z';
|
||||||
|
frame.setFieldType('phantom', FieldType.string, v => '🦥');
|
||||||
|
const field = frame.fields.find(f => f.name == 'value');
|
||||||
|
field!.config.unit = 'kwh';
|
||||||
|
|
||||||
|
test('Should support functional methods', () => {
|
||||||
|
const expectedNames = input.map(row => row.name);
|
||||||
|
|
||||||
|
// Check map
|
||||||
|
expect(frame.map(row => row.name)).toEqual(expectedNames);
|
||||||
|
|
||||||
|
let names: string[] = [];
|
||||||
|
for (const row of frame) {
|
||||||
|
names.push(row.name);
|
||||||
|
}
|
||||||
|
expect(names).toEqual(expectedNames);
|
||||||
|
|
||||||
|
names = [];
|
||||||
|
frame.forEach(row => {
|
||||||
|
names.push(row.name);
|
||||||
|
});
|
||||||
|
expect(names).toEqual(expectedNames);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should convert an array of objects to a dataframe', () => {
|
||||||
|
expect(toDataFrameDTO(frame)).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"fields": Array [
|
||||||
|
Object {
|
||||||
|
"config": Object {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "name",
|
||||||
|
"type": "string",
|
||||||
|
"values": Array [
|
||||||
|
"first",
|
||||||
|
"second",
|
||||||
|
"third",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"config": Object {
|
||||||
|
"unit": "kwh",
|
||||||
|
},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "value",
|
||||||
|
"type": "number",
|
||||||
|
"values": Array [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"config": Object {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "time",
|
||||||
|
"type": "time",
|
||||||
|
"values": Array [
|
||||||
|
123,
|
||||||
|
456,
|
||||||
|
789,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"config": Object {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "phantom",
|
||||||
|
"type": "string",
|
||||||
|
"values": Array [
|
||||||
|
"🦥",
|
||||||
|
"🦥",
|
||||||
|
"🦥",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"meta": undefined,
|
||||||
|
"name": "Hello",
|
||||||
|
"refId": "Z",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
119
packages/grafana-data/src/dataframe/ArrayDataFrame.ts
Normal file
119
packages/grafana-data/src/dataframe/ArrayDataFrame.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { Field, FieldType, DataFrame } from '../types/dataFrame';
|
||||||
|
import { vectorToArray } from '../vector/vectorToArray';
|
||||||
|
import { Vector, QueryResultMeta } from '../types';
|
||||||
|
import { guessFieldTypeFromNameAndValue, toDataFrameDTO } from './processDataFrame';
|
||||||
|
import { FunctionalVector } from '../vector/FunctionalVector';
|
||||||
|
|
||||||
|
export type ValueConverter<T = any> = (val: any) => T;
|
||||||
|
|
||||||
|
const NOOP: ValueConverter = v => v;
|
||||||
|
|
||||||
|
class ArrayPropertyVector<T = any> implements Vector<T> {
|
||||||
|
converter = NOOP;
|
||||||
|
|
||||||
|
constructor(private source: any[], private prop: string) {}
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.source.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(index: number): T {
|
||||||
|
return this.converter(this.source[index][this.prop]);
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): T[] {
|
||||||
|
return vectorToArray(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): T[] {
|
||||||
|
return vectorToArray(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ArrayDataFrame takes an array of objects and presents it as a DataFrame
|
||||||
|
*
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export class ArrayDataFrame<T = any> extends FunctionalVector<T> implements DataFrame {
|
||||||
|
name?: string;
|
||||||
|
refId?: string;
|
||||||
|
meta?: QueryResultMeta;
|
||||||
|
|
||||||
|
private theFields: Field[] = [];
|
||||||
|
|
||||||
|
constructor(private source: T[], names?: string[]) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const first: any = source.length ? source[0] : {};
|
||||||
|
if (names) {
|
||||||
|
this.theFields = names.map(name => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
type: guessFieldTypeFromNameAndValue(name, first[name]),
|
||||||
|
config: {},
|
||||||
|
values: new ArrayPropertyVector(source, name),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setFieldsFromObject(first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a field for each property in the object. This will guess the type
|
||||||
|
*/
|
||||||
|
setFieldsFromObject(obj: any) {
|
||||||
|
this.theFields = Object.keys(obj).map(name => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
type: guessFieldTypeFromNameAndValue(name, obj[name]),
|
||||||
|
config: {},
|
||||||
|
values: new ArrayPropertyVector(this.source, name),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure how the object property is passed to the data frame
|
||||||
|
*/
|
||||||
|
setFieldType(name: string, type: FieldType, converter?: ValueConverter): Field {
|
||||||
|
let field = this.fields.find(f => f.name === name);
|
||||||
|
if (field) {
|
||||||
|
field.type = type;
|
||||||
|
} else {
|
||||||
|
field = {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayPropertyVector(this.source, name),
|
||||||
|
};
|
||||||
|
this.fields.push(field);
|
||||||
|
}
|
||||||
|
(field.values as any).converter = converter ?? NOOP;
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fields(): Field[] {
|
||||||
|
return this.theFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined for Vector interface
|
||||||
|
get length() {
|
||||||
|
return this.source.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an object with a property for each field in the DataFrame
|
||||||
|
*/
|
||||||
|
get(idx: number): T {
|
||||||
|
return this.source[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The simplified JSON values used in JSON.stringify()
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return toDataFrameDTO(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Vector } from '../types/vector';
|
|
||||||
import { DataFrame } from '../types/dataFrame';
|
import { DataFrame } from '../types/dataFrame';
|
||||||
import { DisplayProcessor } from '../types';
|
import { DisplayProcessor } from '../types';
|
||||||
|
import { FunctionalVector } from '../vector/FunctionalVector';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This abstraction will present the contents of a DataFrame as if
|
* This abstraction will present the contents of a DataFrame as if
|
||||||
@@ -13,11 +13,12 @@ import { DisplayProcessor } from '../types';
|
|||||||
* @typeParam T - Type of object stored in the DataFrame.
|
* @typeParam T - Type of object stored in the DataFrame.
|
||||||
* @beta
|
* @beta
|
||||||
*/
|
*/
|
||||||
export class DataFrameView<T = any> implements Vector<T> {
|
export class DataFrameView<T = any> extends FunctionalVector<T> {
|
||||||
private index = 0;
|
private index = 0;
|
||||||
private obj: T;
|
private obj: T;
|
||||||
|
|
||||||
constructor(private data: DataFrame) {
|
constructor(private data: DataFrame) {
|
||||||
|
super();
|
||||||
const obj = ({} as unknown) as T;
|
const obj = ({} as unknown) as T;
|
||||||
|
|
||||||
for (let i = 0; i < data.fields.length; i++) {
|
for (let i = 0; i < data.fields.length; i++) {
|
||||||
@@ -91,24 +92,8 @@ export class DataFrameView<T = any> implements Vector<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toArray(): T[] {
|
toArray(): T[] {
|
||||||
return new Array(this.data.length).fill(0).map((_, i) => ({ ...this.get(i) }));
|
return new Array(this.data.length)
|
||||||
}
|
.fill(0) // Needs to make a full copy
|
||||||
|
.map((_, i) => ({ ...this.get(i) }));
|
||||||
toJSON(): T[] {
|
|
||||||
return this.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
forEachRow(iterator: (row: T) => void) {
|
|
||||||
for (let i = 0; i < this.data.length; i++) {
|
|
||||||
iterator(this.get(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
map<V>(iterator: (item: T, index: number) => V) {
|
|
||||||
const acc: V[] = [];
|
|
||||||
for (let i = 0; i < this.data.length; i++) {
|
|
||||||
acc.push(iterator(this.get(i), i));
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import isString from 'lodash/isString';
|
|||||||
import { makeFieldParser } from '../utils/fieldParser';
|
import { makeFieldParser } from '../utils/fieldParser';
|
||||||
import { MutableVector, Vector } from '../types/vector';
|
import { MutableVector, Vector } from '../types/vector';
|
||||||
import { ArrayVector } from '../vector/ArrayVector';
|
import { ArrayVector } from '../vector/ArrayVector';
|
||||||
import { vectorToArray } from '../vector/vectorToArray';
|
import { FunctionalVector } from '../vector/FunctionalVector';
|
||||||
|
|
||||||
export type MutableField<T = any> = Field<T, MutableVector<T>>;
|
export type MutableField<T = any> = Field<T, MutableVector<T>>;
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ type MutableVectorCreator = (buffer?: any[]) => MutableVector;
|
|||||||
|
|
||||||
export const MISSING_VALUE: any = null;
|
export const MISSING_VALUE: any = null;
|
||||||
|
|
||||||
export class MutableDataFrame<T = any> implements DataFrame, MutableVector<T> {
|
export class MutableDataFrame<T = any> extends FunctionalVector<T> implements DataFrame, MutableVector<T> {
|
||||||
name?: string;
|
name?: string;
|
||||||
refId?: string;
|
refId?: string;
|
||||||
meta?: QueryResultMeta;
|
meta?: QueryResultMeta;
|
||||||
@@ -26,6 +26,8 @@ export class MutableDataFrame<T = any> implements DataFrame, MutableVector<T> {
|
|||||||
private creator: MutableVectorCreator;
|
private creator: MutableVectorCreator;
|
||||||
|
|
||||||
constructor(source?: DataFrame | DataFrameDTO, creator?: MutableVectorCreator) {
|
constructor(source?: DataFrame | DataFrameDTO, creator?: MutableVectorCreator) {
|
||||||
|
super();
|
||||||
|
|
||||||
// This creates the underlying storage buffers
|
// This creates the underlying storage buffers
|
||||||
this.creator = creator
|
this.creator = creator
|
||||||
? creator
|
? creator
|
||||||
@@ -267,10 +269,6 @@ export class MutableDataFrame<T = any> implements DataFrame, MutableVector<T> {
|
|||||||
return v as T;
|
return v as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
toArray(): T[] {
|
|
||||||
return vectorToArray(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The simplified JSON values used in JSON.stringify()
|
* The simplified JSON values used in JSON.stringify()
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from './MutableDataFrame';
|
|||||||
export * from './processDataFrame';
|
export * from './processDataFrame';
|
||||||
export * from './dimensions';
|
export * from './dimensions';
|
||||||
export * from './ArrowDataFrame';
|
export * from './ArrowDataFrame';
|
||||||
|
export * from './ArrayDataFrame';
|
||||||
|
|||||||
@@ -159,6 +159,19 @@ function convertJSONDocumentDataToDataFrame(timeSeries: TimeSeries): DataFrame {
|
|||||||
// https://github.com/mholt/PapaParse/blob/master/papaparse.js#L998
|
// https://github.com/mholt/PapaParse/blob/master/papaparse.js#L998
|
||||||
const NUMBER = /^\s*(-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?|NAN)\s*$/i;
|
const NUMBER = /^\s*(-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?|NAN)\s*$/i;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a name and value, this will pick a reasonable field type
|
||||||
|
*/
|
||||||
|
export function guessFieldTypeFromNameAndValue(name: string, v: any): FieldType {
|
||||||
|
if (name) {
|
||||||
|
name = name.toLowerCase();
|
||||||
|
if (name === 'date' || name === 'time') {
|
||||||
|
return FieldType.time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return guessFieldTypeFromValue(v);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a value this will guess the best column type
|
* Given a value this will guess the best column type
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { MutableVector } from '../types/vector';
|
import { MutableVector } from '../types/vector';
|
||||||
|
import { FunctionalVector } from './FunctionalVector';
|
||||||
|
|
||||||
export class ArrayVector<T = any> implements MutableVector<T> {
|
export class ArrayVector<T = any> extends FunctionalVector<T> implements MutableVector<T> {
|
||||||
buffer: T[];
|
buffer: T[];
|
||||||
|
|
||||||
constructor(buffer?: T[]) {
|
constructor(buffer?: T[]) {
|
||||||
|
super();
|
||||||
this.buffer = buffer ? buffer : [];
|
this.buffer = buffer ? buffer : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { MutableVector } from '../types/vector';
|
import { MutableVector } from '../types/vector';
|
||||||
import { vectorToArray } from './vectorToArray';
|
import { vectorToArray } from './vectorToArray';
|
||||||
|
import { FunctionalVector } from './FunctionalVector';
|
||||||
|
|
||||||
interface CircularOptions<T> {
|
interface CircularOptions<T> {
|
||||||
buffer?: T[];
|
buffer?: T[];
|
||||||
@@ -14,13 +15,15 @@ interface CircularOptions<T> {
|
|||||||
* This supports addting to the 'head' or 'tail' and will grow the buffer
|
* This supports addting to the 'head' or 'tail' and will grow the buffer
|
||||||
* to match a configured capacity.
|
* to match a configured capacity.
|
||||||
*/
|
*/
|
||||||
export class CircularVector<T = any> implements MutableVector<T> {
|
export class CircularVector<T = any> extends FunctionalVector implements MutableVector<T> {
|
||||||
private buffer: T[];
|
private buffer: T[];
|
||||||
private index: number;
|
private index: number;
|
||||||
private capacity: number;
|
private capacity: number;
|
||||||
private tail: boolean;
|
private tail: boolean;
|
||||||
|
|
||||||
constructor(options: CircularOptions<T>) {
|
constructor(options: CircularOptions<T>) {
|
||||||
|
super();
|
||||||
|
|
||||||
this.buffer = options.buffer || [];
|
this.buffer = options.buffer || [];
|
||||||
this.capacity = this.buffer.length;
|
this.capacity = this.buffer.length;
|
||||||
this.tail = 'head' !== options.append;
|
this.tail = 'head' !== options.append;
|
||||||
|
|||||||
77
packages/grafana-data/src/vector/FunctionalVector.ts
Normal file
77
packages/grafana-data/src/vector/FunctionalVector.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { vectorToArray } from './vectorToArray';
|
||||||
|
import { Vector } from '../types';
|
||||||
|
|
||||||
|
export abstract class FunctionalVector<T = any> implements Vector<T>, Iterable<T> {
|
||||||
|
abstract get length(): number;
|
||||||
|
|
||||||
|
abstract get(index: number): T;
|
||||||
|
|
||||||
|
// Implement "iterator protocol"
|
||||||
|
*iterator() {
|
||||||
|
for (let i = 0; i < this.length; i++) {
|
||||||
|
yield this.get(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement "iterable protocol"
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(iterator: (row: T) => void) {
|
||||||
|
return vectorator(this).forEach(iterator);
|
||||||
|
}
|
||||||
|
|
||||||
|
map<V>(transform: (item: T, index: number) => V) {
|
||||||
|
return vectorator(this).map(transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
filter<V>(predicate: (item: T) => V) {
|
||||||
|
return vectorator(this).filter(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): T[] {
|
||||||
|
return vectorToArray(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): any {
|
||||||
|
return this.toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use functional programming with your vector
|
||||||
|
*/
|
||||||
|
export function vectorator<T>(vector: Vector<T>) {
|
||||||
|
return {
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
for (let i = 0; i < vector.length; i++) {
|
||||||
|
yield vector.get(i);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
forEach(iterator: (row: T) => void) {
|
||||||
|
for (let i = 0; i < vector.length; i++) {
|
||||||
|
iterator(vector.get(i));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
map<V>(transform: (item: T, index: number) => V) {
|
||||||
|
const result: V[] = [];
|
||||||
|
for (let i = 0; i < vector.length; i++) {
|
||||||
|
result.push(transform(vector.get(i), i));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
filter<V>(predicate: (item: T) => V) {
|
||||||
|
const result: T[] = [];
|
||||||
|
for (const val of this) {
|
||||||
|
if (predicate(val)) {
|
||||||
|
result.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,3 +4,5 @@ export * from './CircularVector';
|
|||||||
export * from './ConstantVector';
|
export * from './ConstantVector';
|
||||||
export * from './ScaledVector';
|
export * from './ScaledVector';
|
||||||
export * from './SortedVector';
|
export * from './SortedVector';
|
||||||
|
|
||||||
|
export { vectorator } from './FunctionalVector';
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
}
|
}
|
||||||
const view = new DataFrameView<{ ts: string; line: string }>(frame);
|
const view = new DataFrameView<{ ts: string; line: string }>(frame);
|
||||||
|
|
||||||
view.forEachRow(row => {
|
view.forEach(row => {
|
||||||
annotations.push({
|
annotations.push({
|
||||||
time: new Date(row.ts).valueOf(),
|
time: new Date(row.ts).valueOf(),
|
||||||
text: row.line,
|
text: row.line,
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ export const enhanceDataFrame = (dataFrame: DataFrame, config: LokiOptions | nul
|
|||||||
}, {} as Record<string, any>);
|
}, {} as Record<string, any>);
|
||||||
|
|
||||||
const view = new DataFrameView(dataFrame);
|
const view = new DataFrameView(dataFrame);
|
||||||
view.forEachRow((row: { line: string }) => {
|
view.forEach((row: { line: string }) => {
|
||||||
for (const field of derivedFields) {
|
for (const field of derivedFields) {
|
||||||
const logMatch = row.line.match(field.matcherRegex);
|
const logMatch = row.line.match(field.matcherRegex);
|
||||||
fields[field.name].values.add(logMatch && logMatch[1]);
|
fields[field.name].values.add(logMatch && logMatch[1]);
|
||||||
|
|||||||
Reference in New Issue
Block a user