mirror of https://github.com/grafana/grafana
DataFrame: expose an object array as a data frame (#23494)
parent
6cb7d95916
commit
6f1a25a896
@ -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", |
||||
} |
||||
`);
|
||||
}); |
||||
}); |
@ -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); |
||||
} |
||||
} |
@ -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; |
||||
}, |
||||
}; |
||||
} |
Loading…
Reference in new issue