diff --git a/packages/grafana-data/src/dataframe/ArrayDataFrame.test.ts b/packages/grafana-data/src/dataframe/ArrayDataFrame.test.ts new file mode 100644 index 00000000000..604afecba46 --- /dev/null +++ b/packages/grafana-data/src/dataframe/ArrayDataFrame.test.ts @@ -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", + } + `); + }); +}); diff --git a/packages/grafana-data/src/dataframe/ArrayDataFrame.ts b/packages/grafana-data/src/dataframe/ArrayDataFrame.ts new file mode 100644 index 00000000000..e799ceb94ff --- /dev/null +++ b/packages/grafana-data/src/dataframe/ArrayDataFrame.ts @@ -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 = (val: any) => T; + +const NOOP: ValueConverter = v => v; + +class ArrayPropertyVector implements Vector { + 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 extends FunctionalVector 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); + } +} diff --git a/packages/grafana-data/src/dataframe/DataFrameView.ts b/packages/grafana-data/src/dataframe/DataFrameView.ts index bf304e2f539..0568bbc690c 100644 --- a/packages/grafana-data/src/dataframe/DataFrameView.ts +++ b/packages/grafana-data/src/dataframe/DataFrameView.ts @@ -1,6 +1,6 @@ -import { Vector } from '../types/vector'; import { DataFrame } from '../types/dataFrame'; import { DisplayProcessor } from '../types'; +import { FunctionalVector } from '../vector/FunctionalVector'; /** * 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. * @beta */ -export class DataFrameView implements Vector { +export class DataFrameView extends FunctionalVector { private index = 0; private obj: T; constructor(private data: DataFrame) { + super(); const obj = ({} as unknown) as T; for (let i = 0; i < data.fields.length; i++) { @@ -91,24 +92,8 @@ export class DataFrameView implements Vector { } toArray(): T[] { - return new Array(this.data.length).fill(0).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(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; + return new Array(this.data.length) + .fill(0) // Needs to make a full copy + .map((_, i) => ({ ...this.get(i) })); } } diff --git a/packages/grafana-data/src/dataframe/MutableDataFrame.ts b/packages/grafana-data/src/dataframe/MutableDataFrame.ts index 96a2a38ec82..acbec81be3c 100644 --- a/packages/grafana-data/src/dataframe/MutableDataFrame.ts +++ b/packages/grafana-data/src/dataframe/MutableDataFrame.ts @@ -6,7 +6,7 @@ import isString from 'lodash/isString'; import { makeFieldParser } from '../utils/fieldParser'; import { MutableVector, Vector } from '../types/vector'; import { ArrayVector } from '../vector/ArrayVector'; -import { vectorToArray } from '../vector/vectorToArray'; +import { FunctionalVector } from '../vector/FunctionalVector'; export type MutableField = Field>; @@ -14,7 +14,7 @@ type MutableVectorCreator = (buffer?: any[]) => MutableVector; export const MISSING_VALUE: any = null; -export class MutableDataFrame implements DataFrame, MutableVector { +export class MutableDataFrame extends FunctionalVector implements DataFrame, MutableVector { name?: string; refId?: string; meta?: QueryResultMeta; @@ -26,6 +26,8 @@ export class MutableDataFrame implements DataFrame, MutableVector { private creator: MutableVectorCreator; constructor(source?: DataFrame | DataFrameDTO, creator?: MutableVectorCreator) { + super(); + // This creates the underlying storage buffers this.creator = creator ? creator @@ -267,10 +269,6 @@ export class MutableDataFrame implements DataFrame, MutableVector { return v as T; } - toArray(): T[] { - return vectorToArray(this); - } - /** * The simplified JSON values used in JSON.stringify() */ diff --git a/packages/grafana-data/src/dataframe/index.ts b/packages/grafana-data/src/dataframe/index.ts index 1626c3294fd..dbdd20c5cd9 100644 --- a/packages/grafana-data/src/dataframe/index.ts +++ b/packages/grafana-data/src/dataframe/index.ts @@ -5,3 +5,4 @@ export * from './MutableDataFrame'; export * from './processDataFrame'; export * from './dimensions'; export * from './ArrowDataFrame'; +export * from './ArrayDataFrame'; diff --git a/packages/grafana-data/src/dataframe/processDataFrame.ts b/packages/grafana-data/src/dataframe/processDataFrame.ts index 778a98029be..17a4cece868 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.ts @@ -159,6 +159,19 @@ function convertJSONDocumentDataToDataFrame(timeSeries: TimeSeries): DataFrame { // https://github.com/mholt/PapaParse/blob/master/papaparse.js#L998 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 * diff --git a/packages/grafana-data/src/vector/ArrayVector.ts b/packages/grafana-data/src/vector/ArrayVector.ts index 63c614f1cc4..80483f674f7 100644 --- a/packages/grafana-data/src/vector/ArrayVector.ts +++ b/packages/grafana-data/src/vector/ArrayVector.ts @@ -1,9 +1,11 @@ import { MutableVector } from '../types/vector'; +import { FunctionalVector } from './FunctionalVector'; -export class ArrayVector implements MutableVector { +export class ArrayVector extends FunctionalVector implements MutableVector { buffer: T[]; constructor(buffer?: T[]) { + super(); this.buffer = buffer ? buffer : []; } diff --git a/packages/grafana-data/src/vector/CircularVector.ts b/packages/grafana-data/src/vector/CircularVector.ts index f0eeb6579f0..95b93f3311d 100644 --- a/packages/grafana-data/src/vector/CircularVector.ts +++ b/packages/grafana-data/src/vector/CircularVector.ts @@ -1,5 +1,6 @@ import { MutableVector } from '../types/vector'; import { vectorToArray } from './vectorToArray'; +import { FunctionalVector } from './FunctionalVector'; interface CircularOptions { buffer?: T[]; @@ -14,13 +15,15 @@ interface CircularOptions { * This supports addting to the 'head' or 'tail' and will grow the buffer * to match a configured capacity. */ -export class CircularVector implements MutableVector { +export class CircularVector extends FunctionalVector implements MutableVector { private buffer: T[]; private index: number; private capacity: number; private tail: boolean; constructor(options: CircularOptions) { + super(); + this.buffer = options.buffer || []; this.capacity = this.buffer.length; this.tail = 'head' !== options.append; diff --git a/packages/grafana-data/src/vector/FunctionalVector.ts b/packages/grafana-data/src/vector/FunctionalVector.ts new file mode 100644 index 00000000000..6569881ec52 --- /dev/null +++ b/packages/grafana-data/src/vector/FunctionalVector.ts @@ -0,0 +1,77 @@ +import { vectorToArray } from './vectorToArray'; +import { Vector } from '../types'; + +export abstract class FunctionalVector implements Vector, Iterable { + 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(transform: (item: T, index: number) => V) { + return vectorator(this).map(transform); + } + + filter(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(vector: Vector) { + 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(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(predicate: (item: T) => V) { + const result: T[] = []; + for (const val of this) { + if (predicate(val)) { + result.push(val); + } + } + return result; + }, + }; +} diff --git a/packages/grafana-data/src/vector/index.ts b/packages/grafana-data/src/vector/index.ts index 392cb379610..88ef69542f0 100644 --- a/packages/grafana-data/src/vector/index.ts +++ b/packages/grafana-data/src/vector/index.ts @@ -4,3 +4,5 @@ export * from './CircularVector'; export * from './ConstantVector'; export * from './ScaledVector'; export * from './SortedVector'; + +export { vectorator } from './FunctionalVector'; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 805506d6384..89733b7eddb 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -646,7 +646,7 @@ export class LokiDatasource extends DataSourceApi { } const view = new DataFrameView<{ ts: string; line: string }>(frame); - view.forEachRow(row => { + view.forEach(row => { annotations.push({ time: new Date(row.ts).valueOf(), text: row.line, diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index 8c0d5928dae..b9915d0ae91 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -420,7 +420,7 @@ export const enhanceDataFrame = (dataFrame: DataFrame, config: LokiOptions | nul }, {} as Record); const view = new DataFrameView(dataFrame); - view.forEachRow((row: { line: string }) => { + view.forEach((row: { line: string }) => { for (const field of derivedFields) { const logMatch = row.line.match(field.matcherRegex); fields[field.name].values.add(logMatch && logMatch[1]);