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