From 843af51cc662b5d4e636c2c6fd765d55160a0a38 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 16 Sep 2019 16:53:06 +0200 Subject: [PATCH] Organise data frame and vectors code --- .../src/dataframe/CircularDataFrame.ts | 22 ++ .../DataFrameView.test.ts} | 8 +- .../src/dataframe/DataFrameView.ts | 78 ++++ .../FieldCache.test.ts} | 106 ++---- .../grafana-data/src/dataframe/FieldCache.ts | 78 ++++ .../src/dataframe/MutableDataFrame.test.ts | 66 ++++ .../MutableDataFrame.ts} | 133 +------ packages/grafana-data/src/dataframe/index.ts | 4 + packages/grafana-data/src/index.ts | 2 + packages/grafana-data/src/types/dataFrame.ts | 20 +- packages/grafana-data/src/types/vector.ts | 40 +++ packages/grafana-data/src/utils/csv.ts | 2 +- .../grafana-data/src/utils/dataFrameView.ts | 77 ---- .../grafana-data/src/utils/fieldParser.ts | 28 ++ .../src/utils/fieldReducer.test.ts | 4 +- packages/grafana-data/src/utils/index.ts | 3 - packages/grafana-data/src/utils/logs.ts | 2 +- .../src/utils/processDataFrame.test.ts | 2 +- .../src/utils/processDataFrame.ts | 5 +- .../src/utils/transformers/append.ts | 2 +- .../src/utils/transformers/reduce.ts | 2 +- .../utils/transformers/transformers.test.ts | 2 +- packages/grafana-data/src/utils/vector.ts | 338 ------------------ .../src/vector/AppendedVectors.test.ts | 23 ++ .../src/vector/AppendedVectors.ts | 73 ++++ .../grafana-data/src/vector/ArrayVector.ts | 37 ++ .../CircularVector.test.ts} | 50 +-- .../grafana-data/src/vector/CircularVector.ts | 138 +++++++ .../src/vector/ConstantVector.test.ts | 17 + .../grafana-data/src/vector/ConstantVector.ts | 22 ++ .../src/vector/ScaledVector.test.ts | 15 + .../grafana-data/src/vector/ScaledVector.ts | 22 ++ .../grafana-data/src/vector/SortedVector.ts | 25 ++ packages/grafana-data/src/vector/index.ts | 6 + .../grafana-data/src/vector/vectorToArray.ts | 9 + 35 files changed, 750 insertions(+), 711 deletions(-) create mode 100644 packages/grafana-data/src/dataframe/CircularDataFrame.ts rename packages/grafana-data/src/{utils/dataFrameView.test.ts => dataframe/DataFrameView.test.ts} (90%) create mode 100644 packages/grafana-data/src/dataframe/DataFrameView.ts rename packages/grafana-data/src/{utils/dataFrameHelper.test.ts => dataframe/FieldCache.test.ts} (55%) create mode 100644 packages/grafana-data/src/dataframe/FieldCache.ts create mode 100644 packages/grafana-data/src/dataframe/MutableDataFrame.test.ts rename packages/grafana-data/src/{utils/dataFrameHelper.ts => dataframe/MutableDataFrame.ts} (66%) create mode 100644 packages/grafana-data/src/dataframe/index.ts create mode 100644 packages/grafana-data/src/types/vector.ts create mode 100644 packages/grafana-data/src/utils/fieldParser.ts delete mode 100644 packages/grafana-data/src/utils/vector.ts create mode 100644 packages/grafana-data/src/vector/AppendedVectors.test.ts create mode 100644 packages/grafana-data/src/vector/AppendedVectors.ts create mode 100644 packages/grafana-data/src/vector/ArrayVector.ts rename packages/grafana-data/src/{utils/vector.test.ts => vector/CircularVector.test.ts} (65%) create mode 100644 packages/grafana-data/src/vector/CircularVector.ts create mode 100644 packages/grafana-data/src/vector/ConstantVector.test.ts create mode 100644 packages/grafana-data/src/vector/ConstantVector.ts create mode 100644 packages/grafana-data/src/vector/ScaledVector.test.ts create mode 100644 packages/grafana-data/src/vector/ScaledVector.ts create mode 100644 packages/grafana-data/src/vector/SortedVector.ts create mode 100644 packages/grafana-data/src/vector/index.ts create mode 100644 packages/grafana-data/src/vector/vectorToArray.ts diff --git a/packages/grafana-data/src/dataframe/CircularDataFrame.ts b/packages/grafana-data/src/dataframe/CircularDataFrame.ts new file mode 100644 index 00000000000..890be6c2f50 --- /dev/null +++ b/packages/grafana-data/src/dataframe/CircularDataFrame.ts @@ -0,0 +1,22 @@ +import { MutableDataFrame } from './MutableDataFrame'; +import { CircularVector } from '../vector/CircularVector'; + +interface CircularOptions { + append?: 'head' | 'tail'; + capacity?: number; +} + +/** + * This dataframe can have values constantly added, and will never + * exceed the given capacity + */ +export class CircularDataFrame extends MutableDataFrame { + constructor(options: CircularOptions) { + super(undefined, (buffer?: any[]) => { + return new CircularVector({ + ...options, + buffer, + }); + }); + } +} diff --git a/packages/grafana-data/src/utils/dataFrameView.test.ts b/packages/grafana-data/src/dataframe/DataFrameView.test.ts similarity index 90% rename from packages/grafana-data/src/utils/dataFrameView.test.ts rename to packages/grafana-data/src/dataframe/DataFrameView.test.ts index 8755cfc827a..a5a219eb138 100644 --- a/packages/grafana-data/src/utils/dataFrameView.test.ts +++ b/packages/grafana-data/src/dataframe/DataFrameView.test.ts @@ -1,7 +1,7 @@ -import { FieldType, DataFrameDTO } from '../types/index'; -import { MutableDataFrame } from './dataFrameHelper'; -import { DataFrameView } from './dataFrameView'; -import { DateTime } from './moment_wrapper'; +import { FieldType, DataFrameDTO } from '../types/dataFrame'; +import { DateTime } from '../utils/moment_wrapper'; +import { MutableDataFrame } from './MutableDataFrame'; +import { DataFrameView } from './DataFrameView'; interface MySpecialObject { time: DateTime; diff --git a/packages/grafana-data/src/dataframe/DataFrameView.ts b/packages/grafana-data/src/dataframe/DataFrameView.ts new file mode 100644 index 00000000000..cbb0106183c --- /dev/null +++ b/packages/grafana-data/src/dataframe/DataFrameView.ts @@ -0,0 +1,78 @@ +import { Vector } from '../types/vector'; +import { DataFrame } from '../types/dataFrame'; + +/** + * This abstraction will present the contents of a DataFrame as if + * it were a well typed javascript object Vector. + * + * NOTE: The contents of the object returned from `view.get(index)` + * are optimized for use in a loop. All calls return the same object + * but the index has changed. + * + * For example, the three objects: + * const first = view.get(0); + * const second = view.get(1); + * const third = view.get(2); + * will point to the contents at index 2 + * + * If you need three different objects, consider something like: + * const first = { ... view.get(0) }; + * const second = { ... view.get(1) }; + * const third = { ... view.get(2) }; + */ +export class DataFrameView implements Vector { + private index = 0; + private obj: T; + + constructor(private data: DataFrame) { + const obj = ({} as unknown) as T; + for (let i = 0; i < data.fields.length; i++) { + const field = data.fields[i]; + const getter = () => { + return field.values.get(this.index); + }; + if (!(obj as any).hasOwnProperty(field.name)) { + Object.defineProperty(obj, field.name, { + enumerable: true, // Shows up as enumerable property + get: getter, + }); + } + Object.defineProperty(obj, i, { + enumerable: false, // Don't enumerate array index + get: getter, + }); + } + this.obj = obj; + } + + get dataFrame() { + return this.data; + } + + get length() { + return this.data.length; + } + + get(idx: number) { + this.index = idx; + return this.obj; + } + + toArray(): T[] { + const arr: T[] = []; + for (let i = 0; i < this.data.length; i++) { + arr.push({ ...this.get(i) }); + } + return arr; + } + + toJSON(): T[] { + return this.toArray(); + } + + forEachRow(iterator: (row: T) => void) { + for (let i = 0; i < this.data.length; i++) { + iterator(this.get(i)); + } + } +} diff --git a/packages/grafana-data/src/utils/dataFrameHelper.test.ts b/packages/grafana-data/src/dataframe/FieldCache.test.ts similarity index 55% rename from packages/grafana-data/src/utils/dataFrameHelper.test.ts rename to packages/grafana-data/src/dataframe/FieldCache.test.ts index 5555c09cd40..8d888797a61 100644 --- a/packages/grafana-data/src/utils/dataFrameHelper.test.ts +++ b/packages/grafana-data/src/dataframe/FieldCache.test.ts @@ -1,29 +1,6 @@ -import { DataFrameDTO, FieldType } from '../types'; -import { FieldCache, MutableDataFrame } from './dataFrameHelper'; -import { toDataFrame } from './processDataFrame'; - -describe('dataFrameHelper', () => { - const frame = toDataFrame({ - fields: [ - { name: 'time', type: FieldType.time, values: [100, 200, 300] }, - { name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] }, - { name: 'value', type: FieldType.number, values: [1, 2, 3] }, - { name: 'value', type: FieldType.number, values: [4, 5, 6] }, - ], - }); - const ext = new FieldCache(frame); - - it('should get the first field with a duplicate name', () => { - const field = ext.getFieldByName('value'); - expect(field!.name).toEqual('value'); - expect(field!.values.toJSON()).toEqual([1, 2, 3]); - }); - - it('should return index of the field', () => { - const field = ext.getFirstFieldOfType(FieldType.number); - expect(field!.index).toEqual(2); - }); -}); +import { FieldCache } from './FieldCache'; +import { toDataFrame } from '../utils'; +import { FieldType } from '../types/dataFrame'; describe('FieldCache', () => { it('when creating a new FieldCache from fields should be able to query cache', () => { @@ -90,68 +67,27 @@ describe('FieldCache', () => { expect(fieldCache.getFieldByName('undefined')!.name).toEqual(expectedFieldNames[5]); expect(fieldCache.getFieldByName('null')).toBeUndefined(); }); -}); -describe('reverse', () => { - describe('when called with a DataFrame', () => { - it('then it should reverse the order of values in all fields', () => { - const frame: DataFrameDTO = { - fields: [ - { name: 'time', type: FieldType.time, values: [100, 200, 300] }, - { name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] }, - { name: 'value', type: FieldType.number, values: [1, 2, 3] }, - ], - }; + describe('field retrieval', () => { + const frame = toDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [100, 200, 300] }, + { name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] }, + { name: 'value', type: FieldType.number, values: [1, 2, 3] }, + { name: 'value', type: FieldType.number, values: [4, 5, 6] }, + ], + }); + const ext = new FieldCache(frame); - const helper = new MutableDataFrame(frame); + it('should get the first field with a duplicate name', () => { + const field = ext.getFieldByName('value'); + expect(field!.name).toEqual('value'); + expect(field!.values.toJSON()).toEqual([1, 2, 3]); + }); - expect(helper.values.time.toArray()).toEqual([100, 200, 300]); - expect(helper.values.name.toArray()).toEqual(['a', 'b', 'c']); - expect(helper.values.value.toArray()).toEqual([1, 2, 3]); - - helper.reverse(); - - expect(helper.values.time.toArray()).toEqual([300, 200, 100]); - expect(helper.values.name.toArray()).toEqual(['c', 'b', 'a']); - expect(helper.values.value.toArray()).toEqual([3, 2, 1]); + it('should return index of the field', () => { + const field = ext.getFirstFieldOfType(FieldType.number); + expect(field!.index).toEqual(2); }); }); }); - -describe('Apending DataFrame', () => { - it('Should append values', () => { - const dto: DataFrameDTO = { - fields: [ - { name: 'time', type: FieldType.time, values: [100] }, - { name: 'name', type: FieldType.string, values: ['a', 'b'] }, - { name: 'value', type: FieldType.number, values: [1, 2, 3] }, - ], - }; - - const frame = new MutableDataFrame(dto); - expect(frame.values.time.toArray()).toEqual([100, null, null]); - - // Set a value on the second row - frame.set(1, { time: 200, name: 'BB', value: 20 }); - expect(frame.toArray()).toEqual([ - { time: 100, name: 'a', value: 1 }, // 1 - { time: 200, name: 'BB', value: 20 }, // 2 - { time: null, name: null, value: 3 }, // 3 - ]); - - // Set a value on the second row - frame.add({ value2: 'XXX' }, true); - expect(frame.toArray()).toEqual([ - { time: 100, name: 'a', value: 1, value2: null }, // 1 - { time: 200, name: 'BB', value: 20, value2: null }, // 2 - { time: null, name: null, value: 3, value2: null }, // 3 - { time: null, name: null, value: null, value2: 'XXX' }, // 4 - ]); - - // Make sure length survives a spread operator - const keys = Object.keys(frame); - const copy = { ...frame } as any; - expect(keys).toContain('length'); - expect(copy.length).toEqual(frame.length); - }); -}); diff --git a/packages/grafana-data/src/dataframe/FieldCache.ts b/packages/grafana-data/src/dataframe/FieldCache.ts new file mode 100644 index 00000000000..07f6941e21d --- /dev/null +++ b/packages/grafana-data/src/dataframe/FieldCache.ts @@ -0,0 +1,78 @@ +import { Field, DataFrame, FieldType, guessFieldTypeForField } from '../index'; + +interface FieldWithIndex extends Field { + index: number; +} + +export class FieldCache { + fields: FieldWithIndex[] = []; + + private fieldByName: { [key: string]: FieldWithIndex } = {}; + private fieldByType: { [key: string]: FieldWithIndex[] } = {}; + + constructor(data: DataFrame) { + this.fields = data.fields.map((field, idx) => ({ + ...field, + index: idx, + })); + + for (let i = 0; i < data.fields.length; i++) { + const field = data.fields[i]; + // Make sure it has a type + if (field.type === FieldType.other) { + const t = guessFieldTypeForField(field); + if (t) { + field.type = t; + } + } + if (!this.fieldByType[field.type]) { + this.fieldByType[field.type] = []; + } + this.fieldByType[field.type].push({ + ...field, + index: i, + }); + + if (this.fieldByName[field.name]) { + console.warn('Duplicate field names in DataFrame: ', field.name); + } else { + this.fieldByName[field.name] = { ...field, index: i }; + } + } + } + + getFields(type?: FieldType): FieldWithIndex[] { + if (!type) { + return [...this.fields]; // All fields + } + const fields = this.fieldByType[type]; + if (fields) { + return [...fields]; + } + return []; + } + + hasFieldOfType(type: FieldType): boolean { + const types = this.fieldByType[type]; + return types && types.length > 0; + } + + getFirstFieldOfType(type: FieldType): FieldWithIndex | undefined { + const arr = this.fieldByType[type]; + if (arr && arr.length > 0) { + return arr[0]; + } + return undefined; + } + + hasFieldNamed(name: string): boolean { + return !!this.fieldByName[name]; + } + + /** + * Returns the first field with the given name. + */ + getFieldByName(name: string): FieldWithIndex | undefined { + return this.fieldByName[name]; + } +} diff --git a/packages/grafana-data/src/dataframe/MutableDataFrame.test.ts b/packages/grafana-data/src/dataframe/MutableDataFrame.test.ts new file mode 100644 index 00000000000..eb6f8aaee43 --- /dev/null +++ b/packages/grafana-data/src/dataframe/MutableDataFrame.test.ts @@ -0,0 +1,66 @@ +import { DataFrameDTO, FieldType } from '../types/dataFrame'; +import { MutableDataFrame } from './MutableDataFrame'; + +describe('Reversing DataFrame', () => { + describe('when called with a DataFrame', () => { + it('then it should reverse the order of values in all fields', () => { + const frame: DataFrameDTO = { + fields: [ + { name: 'time', type: FieldType.time, values: [100, 200, 300] }, + { name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] }, + { name: 'value', type: FieldType.number, values: [1, 2, 3] }, + ], + }; + + const helper = new MutableDataFrame(frame); + + expect(helper.values.time.toArray()).toEqual([100, 200, 300]); + expect(helper.values.name.toArray()).toEqual(['a', 'b', 'c']); + expect(helper.values.value.toArray()).toEqual([1, 2, 3]); + + helper.reverse(); + + expect(helper.values.time.toArray()).toEqual([300, 200, 100]); + expect(helper.values.name.toArray()).toEqual(['c', 'b', 'a']); + expect(helper.values.value.toArray()).toEqual([3, 2, 1]); + }); + }); +}); + +describe('Apending DataFrame', () => { + it('Should append values', () => { + const dto: DataFrameDTO = { + fields: [ + { name: 'time', type: FieldType.time, values: [100] }, + { name: 'name', type: FieldType.string, values: ['a', 'b'] }, + { name: 'value', type: FieldType.number, values: [1, 2, 3] }, + ], + }; + + const frame = new MutableDataFrame(dto); + expect(frame.values.time.toArray()).toEqual([100, null, null]); + + // Set a value on the second row + frame.set(1, { time: 200, name: 'BB', value: 20 }); + expect(frame.toArray()).toEqual([ + { time: 100, name: 'a', value: 1 }, // 1 + { time: 200, name: 'BB', value: 20 }, // 2 + { time: null, name: null, value: 3 }, // 3 + ]); + + // Set a value on the second row + frame.add({ value2: 'XXX' }, true); + expect(frame.toArray()).toEqual([ + { time: 100, name: 'a', value: 1, value2: null }, // 1 + { time: 200, name: 'BB', value: 20, value2: null }, // 2 + { time: null, name: null, value: 3, value2: null }, // 3 + { time: null, name: null, value: null, value2: 'XXX' }, // 4 + ]); + + // Make sure length survives a spread operator + const keys = Object.keys(frame); + const copy = { ...frame } as any; + expect(keys).toContain('length'); + expect(copy.length).toEqual(frame.length); + }); +}); diff --git a/packages/grafana-data/src/utils/dataFrameHelper.ts b/packages/grafana-data/src/dataframe/MutableDataFrame.ts similarity index 66% rename from packages/grafana-data/src/utils/dataFrameHelper.ts rename to packages/grafana-data/src/dataframe/MutableDataFrame.ts index 53eff25aaa0..ff9971a2206 100644 --- a/packages/grafana-data/src/utils/dataFrameHelper.ts +++ b/packages/grafana-data/src/dataframe/MutableDataFrame.ts @@ -1,111 +1,12 @@ -import { Field, FieldType, DataFrame, Vector, FieldDTO, DataFrameDTO } from '../types/dataFrame'; -import { Labels, QueryResultMeta, KeyValue } from '../types/data'; -import { guessFieldTypeForField, guessFieldTypeFromValue, toDataFrameDTO } from './processDataFrame'; -import { ArrayVector, MutableVector, vectorToArray, CircularVector } from './vector'; +import { Field, DataFrame, DataFrameDTO, FieldDTO, FieldType } from '../types/dataFrame'; +import { KeyValue, QueryResultMeta, Labels } from '../types/data'; +import { guessFieldTypeFromValue, guessFieldTypeForField, toDataFrameDTO } from '../utils/processDataFrame'; import isArray from 'lodash/isArray'; import isString from 'lodash/isString'; - -interface FieldWithIndex extends Field { - index: number; -} -export class FieldCache { - fields: FieldWithIndex[] = []; - - private fieldByName: { [key: string]: FieldWithIndex } = {}; - private fieldByType: { [key: string]: FieldWithIndex[] } = {}; - - constructor(data: DataFrame) { - this.fields = data.fields.map((field, idx) => ({ - ...field, - index: idx, - })); - - for (let i = 0; i < data.fields.length; i++) { - const field = data.fields[i]; - // Make sure it has a type - if (field.type === FieldType.other) { - const t = guessFieldTypeForField(field); - if (t) { - field.type = t; - } - } - if (!this.fieldByType[field.type]) { - this.fieldByType[field.type] = []; - } - this.fieldByType[field.type].push({ - ...field, - index: i, - }); - - if (this.fieldByName[field.name]) { - console.warn('Duplicate field names in DataFrame: ', field.name); - } else { - this.fieldByName[field.name] = { ...field, index: i }; - } - } - } - - getFields(type?: FieldType): FieldWithIndex[] { - if (!type) { - return [...this.fields]; // All fields - } - const fields = this.fieldByType[type]; - if (fields) { - return [...fields]; - } - return []; - } - - hasFieldOfType(type: FieldType): boolean { - const types = this.fieldByType[type]; - return types && types.length > 0; - } - - getFirstFieldOfType(type: FieldType): FieldWithIndex | undefined { - const arr = this.fieldByType[type]; - if (arr && arr.length > 0) { - return arr[0]; - } - return undefined; - } - - hasFieldNamed(name: string): boolean { - return !!this.fieldByName[name]; - } - - /** - * Returns the first field with the given name. - */ - getFieldByName(name: string): FieldWithIndex | undefined { - return this.fieldByName[name]; - } -} - -function makeFieldParser(value: any, field: Field): (value: string) => any { - if (!field.type) { - if (field.name === 'time' || field.name === 'Time') { - field.type = FieldType.time; - } else { - field.type = guessFieldTypeFromValue(value); - } - } - - if (field.type === FieldType.number) { - return (value: string) => { - return parseFloat(value); - }; - } - - // Will convert anything that starts with "T" to true - if (field.type === FieldType.boolean) { - return (value: string) => { - return !(value[0] === 'F' || value[0] === 'f' || value[0] === '0'); - }; - } - - // Just pass the string back - return (value: string) => value; -} +import { makeFieldParser } from '../utils/fieldParser'; +import { MutableVector, Vector } from '../types/vector'; +import { ArrayVector } from '../vector/ArrayVector'; +import { vectorToArray } from '../vector/vectorToArray'; export type MutableField = Field>; @@ -380,23 +281,3 @@ export class MutableDataFrame implements DataFrame, MutableVector { return toDataFrameDTO(this); } } - -interface CircularOptions { - append?: 'head' | 'tail'; - capacity?: number; -} - -/** - * This dataframe can have values constantly added, and will never - * exceed the given capacity - */ -export class CircularDataFrame extends MutableDataFrame { - constructor(options: CircularOptions) { - super(undefined, (buffer?: any[]) => { - return new CircularVector({ - ...options, - buffer, - }); - }); - } -} diff --git a/packages/grafana-data/src/dataframe/index.ts b/packages/grafana-data/src/dataframe/index.ts new file mode 100644 index 00000000000..56d98921612 --- /dev/null +++ b/packages/grafana-data/src/dataframe/index.ts @@ -0,0 +1,4 @@ +export * from './DataFrameView'; +export * from './FieldCache'; +export * from './CircularDataFrame'; +export * from './MutableDataFrame'; diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 53f3ef71011..79d19e620ba 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -1,2 +1,4 @@ export * from './utils/index'; export * from './types/index'; +export * from './vector'; +export * from './dataframe'; diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index add607d0a53..80de074fb83 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -4,6 +4,7 @@ import { QueryResultBase, Labels, NullValueMode } from './data'; import { FieldCalcs } from '../utils/index'; import { DisplayProcessor } from './displayValue'; import { DataLink } from './dataLink'; +import { Vector } from './vector'; export enum FieldType { time = 'time', // or date @@ -44,25 +45,6 @@ export interface FieldConfig { noValue?: string; } -export interface Vector { - length: number; - - /** - * Access the value by index (Like an array) - */ - get(index: number): T; - - /** - * Get the resutls as an array. - */ - toArray(): T[]; - - /** - * Return the values as a simple array for json serialization - */ - toJSON(): any; // same results as toArray() -} - export interface Field> { name: string; // The column name type: FieldType; diff --git a/packages/grafana-data/src/types/vector.ts b/packages/grafana-data/src/types/vector.ts new file mode 100644 index 00000000000..7e667d725a9 --- /dev/null +++ b/packages/grafana-data/src/types/vector.ts @@ -0,0 +1,40 @@ +export interface Vector { + length: number; + + /** + * Access the value by index (Like an array) + */ + get(index: number): T; + + /** + * Get the resutls as an array. + */ + toArray(): T[]; + + /** + * Return the values as a simple array for json serialization + */ + toJSON(): any; // same results as toArray() +} + +/** + * Apache arrow vectors are Read/Write + */ +export interface ReadWriteVector extends Vector { + set: (index: number, value: T) => void; +} + +/** + * Vector with standard manipulation functions + */ +export interface MutableVector extends ReadWriteVector { + /** + * Adds the value to the vector + */ + add: (value: T) => void; + + /** + * modifies the vector so it is now the oposite order + */ + reverse: () => void; +} diff --git a/packages/grafana-data/src/utils/csv.ts b/packages/grafana-data/src/utils/csv.ts index fd3664676e2..6cbbb5737b0 100644 --- a/packages/grafana-data/src/utils/csv.ts +++ b/packages/grafana-data/src/utils/csv.ts @@ -6,7 +6,7 @@ import isNumber from 'lodash/isNumber'; // Types import { DataFrame, Field, FieldType, FieldConfig } from '../types'; import { guessFieldTypeFromValue } from './processDataFrame'; -import { MutableDataFrame } from './dataFrameHelper'; +import { MutableDataFrame } from '../dataframe/MutableDataFrame'; export enum CSVHeaderStyle { full, diff --git a/packages/grafana-data/src/utils/dataFrameView.ts b/packages/grafana-data/src/utils/dataFrameView.ts index 1cf833e9ded..e69de29bb2d 100644 --- a/packages/grafana-data/src/utils/dataFrameView.ts +++ b/packages/grafana-data/src/utils/dataFrameView.ts @@ -1,77 +0,0 @@ -import { DataFrame, Vector } from '../types/index'; - -/** - * This abstraction will present the contents of a DataFrame as if - * it were a well typed javascript object Vector. - * - * NOTE: The contents of the object returned from `view.get(index)` - * are optimized for use in a loop. All calls return the same object - * but the index has changed. - * - * For example, the three objects: - * const first = view.get(0); - * const second = view.get(1); - * const third = view.get(2); - * will point to the contents at index 2 - * - * If you need three different objects, consider something like: - * const first = { ... view.get(0) }; - * const second = { ... view.get(1) }; - * const third = { ... view.get(2) }; - */ -export class DataFrameView implements Vector { - private index = 0; - private obj: T; - - constructor(private data: DataFrame) { - const obj = ({} as unknown) as T; - for (let i = 0; i < data.fields.length; i++) { - const field = data.fields[i]; - const getter = () => { - return field.values.get(this.index); - }; - if (!(obj as any).hasOwnProperty(field.name)) { - Object.defineProperty(obj, field.name, { - enumerable: true, // Shows up as enumerable property - get: getter, - }); - } - Object.defineProperty(obj, i, { - enumerable: false, // Don't enumerate array index - get: getter, - }); - } - this.obj = obj; - } - - get dataFrame() { - return this.data; - } - - get length() { - return this.data.length; - } - - get(idx: number) { - this.index = idx; - return this.obj; - } - - toArray(): T[] { - const arr: T[] = []; - for (let i = 0; i < this.data.length; i++) { - arr.push({ ...this.get(i) }); - } - return arr; - } - - toJSON(): T[] { - return this.toArray(); - } - - forEachRow(iterator: (row: T) => void) { - for (let i = 0; i < this.data.length; i++) { - iterator(this.get(i)); - } - } -} diff --git a/packages/grafana-data/src/utils/fieldParser.ts b/packages/grafana-data/src/utils/fieldParser.ts new file mode 100644 index 00000000000..6df6aa9d7ee --- /dev/null +++ b/packages/grafana-data/src/utils/fieldParser.ts @@ -0,0 +1,28 @@ +import { Field, FieldType } from '../types/dataFrame'; +import { guessFieldTypeFromValue } from './processDataFrame'; + +export function makeFieldParser(value: any, field: Field): (value: string) => any { + if (!field.type) { + if (field.name === 'time' || field.name === 'Time') { + field.type = FieldType.time; + } else { + field.type = guessFieldTypeFromValue(value); + } + } + + if (field.type === FieldType.number) { + return (value: string) => { + return parseFloat(value); + }; + } + + // Will convert anything that starts with "T" to true + if (field.type === FieldType.boolean) { + return (value: string) => { + return !(value[0] === 'F' || value[0] === 'f' || value[0] === '0'); + }; + } + + // Just pass the string back + return (value: string) => value; +} diff --git a/packages/grafana-data/src/utils/fieldReducer.test.ts b/packages/grafana-data/src/utils/fieldReducer.test.ts index c4bb1f1e414..916fd5bc31c 100644 --- a/packages/grafana-data/src/utils/fieldReducer.test.ts +++ b/packages/grafana-data/src/utils/fieldReducer.test.ts @@ -3,9 +3,9 @@ import difference from 'lodash/difference'; import { fieldReducers, ReducerID, reduceField } from './fieldReducer'; import { Field, FieldType } from '../types/index'; -import { MutableDataFrame } from './dataFrameHelper'; -import { ArrayVector } from './vector'; import { guessFieldTypeFromValue } from './processDataFrame'; +import { MutableDataFrame } from '../dataframe/MutableDataFrame'; +import { ArrayVector } from '../vector/ArrayVector'; /** * Run a reducer and get back the value diff --git a/packages/grafana-data/src/utils/index.ts b/packages/grafana-data/src/utils/index.ts index 2479adafe58..c5260ac0bad 100644 --- a/packages/grafana-data/src/utils/index.ts +++ b/packages/grafana-data/src/utils/index.ts @@ -12,9 +12,6 @@ export * from './object'; export * from './moment_wrapper'; export * from './thresholds'; export * from './text'; -export * from './dataFrameHelper'; -export * from './dataFrameView'; -export * from './vector'; export { getMappedValue } from './valueMappings'; diff --git a/packages/grafana-data/src/utils/logs.ts b/packages/grafana-data/src/utils/logs.ts index 196a6a28dd7..b14542d57fe 100644 --- a/packages/grafana-data/src/utils/logs.ts +++ b/packages/grafana-data/src/utils/logs.ts @@ -2,7 +2,7 @@ import { countBy, chain, map, escapeRegExp } from 'lodash'; import { LogLevel, LogRowModel, LogLabelStatsModel, LogsParser } from '../types/logs'; import { DataFrame, FieldType } from '../types/index'; -import { ArrayVector } from './vector'; +import { ArrayVector } from '../vector/ArrayVector'; const LOGFMT_REGEXP = /(?:^|\s)(\w+)=("[^"]*"|\S+)/; diff --git a/packages/grafana-data/src/utils/processDataFrame.test.ts b/packages/grafana-data/src/utils/processDataFrame.test.ts index a71dff6e9ca..978f1d6ada1 100644 --- a/packages/grafana-data/src/utils/processDataFrame.test.ts +++ b/packages/grafana-data/src/utils/processDataFrame.test.ts @@ -9,7 +9,7 @@ import { } from './processDataFrame'; import { FieldType, TimeSeries, TableData, DataFrameDTO } from '../types/index'; import { dateTime } from './moment_wrapper'; -import { MutableDataFrame } from './dataFrameHelper'; +import { MutableDataFrame } from '../dataframe/MutableDataFrame'; describe('toDataFrame', () => { it('converts timeseries to series', () => { diff --git a/packages/grafana-data/src/utils/processDataFrame.ts b/packages/grafana-data/src/utils/processDataFrame.ts index e88bb54ddec..3dfeb59103e 100644 --- a/packages/grafana-data/src/utils/processDataFrame.ts +++ b/packages/grafana-data/src/utils/processDataFrame.ts @@ -18,9 +18,10 @@ import { DataFrameDTO, } from '../types/index'; import { isDateTime } from './moment_wrapper'; -import { ArrayVector, SortedVector } from './vector'; -import { MutableDataFrame } from './dataFrameHelper'; import { deprecationWarning } from './deprecationWarning'; +import { ArrayVector } from '../vector/ArrayVector'; +import { MutableDataFrame } from '../dataframe/MutableDataFrame'; +import { SortedVector } from '../vector/SortedVector'; function convertTableToDataFrame(table: TableData): DataFrame { const fields = table.columns.map(c => { diff --git a/packages/grafana-data/src/utils/transformers/append.ts b/packages/grafana-data/src/utils/transformers/append.ts index b982cdb1478..c1cf37430b1 100644 --- a/packages/grafana-data/src/utils/transformers/append.ts +++ b/packages/grafana-data/src/utils/transformers/append.ts @@ -1,7 +1,7 @@ import { DataTransformerInfo } from './transformers'; import { DataFrame } from '../../types/dataFrame'; import { DataTransformerID } from './ids'; -import { MutableDataFrame } from '../dataFrameHelper'; +import { MutableDataFrame } from '../../dataframe/MutableDataFrame'; export interface AppendOptions {} diff --git a/packages/grafana-data/src/utils/transformers/reduce.ts b/packages/grafana-data/src/utils/transformers/reduce.ts index a30a9c0ef12..30ca389d235 100644 --- a/packages/grafana-data/src/utils/transformers/reduce.ts +++ b/packages/grafana-data/src/utils/transformers/reduce.ts @@ -5,8 +5,8 @@ import { alwaysFieldMatcher } from '../matchers/predicates'; import { DataTransformerID } from './ids'; import { ReducerID, fieldReducers, reduceField } from '../fieldReducer'; import { KeyValue } from '../../types/data'; -import { ArrayVector } from '../vector'; import { guessFieldTypeForField } from '../processDataFrame'; +import { ArrayVector } from '../../vector/ArrayVector'; export interface ReduceTransformerOptions { reducers: ReducerID[]; diff --git a/packages/grafana-data/src/utils/transformers/transformers.test.ts b/packages/grafana-data/src/utils/transformers/transformers.test.ts index 81df7478d59..0a8e29abec8 100644 --- a/packages/grafana-data/src/utils/transformers/transformers.test.ts +++ b/packages/grafana-data/src/utils/transformers/transformers.test.ts @@ -2,7 +2,7 @@ import { DataTransformerID } from './ids'; import { dataTransformers } from './transformers'; import { toDataFrame } from '../processDataFrame'; import { ReducerID } from '../fieldReducer'; -import { DataFrameView } from '../dataFrameView'; +import { DataFrameView } from '../../dataframe/DataFrameView'; describe('Transformers', () => { it('should load all transformeres', () => { diff --git a/packages/grafana-data/src/utils/vector.ts b/packages/grafana-data/src/utils/vector.ts deleted file mode 100644 index 0a625b01b49..00000000000 --- a/packages/grafana-data/src/utils/vector.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { Vector } from '../types/dataFrame'; - -export function vectorToArray(v: Vector): T[] { - const arr: T[] = []; - for (let i = 0; i < v.length; i++) { - arr[i] = v.get(i); - } - return arr; -} - -/** - * Apache arrow vectors are Read/Write - */ -export interface ReadWriteVector extends Vector { - set: (index: number, value: T) => void; -} - -/** - * Vector with standard manipulation functions - */ -export interface MutableVector extends ReadWriteVector { - /** - * Adds the value to the vector - */ - add: (value: T) => void; - - /** - * modifies the vector so it is now the oposite order - */ - reverse: () => void; -} - -export class ArrayVector implements MutableVector { - buffer: T[]; - - constructor(buffer?: T[]) { - this.buffer = buffer ? buffer : []; - } - - get length() { - return this.buffer.length; - } - - add(value: T) { - this.buffer.push(value); - } - - get(index: number): T { - return this.buffer[index]; - } - - set(index: number, value: T) { - this.buffer[index] = value; - } - - reverse() { - this.buffer.reverse(); - } - - toArray(): T[] { - return this.buffer; - } - - toJSON(): T[] { - return this.buffer; - } -} - -export class ConstantVector implements Vector { - constructor(private value: T, private len: number) {} - - get length() { - return this.len; - } - - get(index: number): T { - return this.value; - } - - toArray(): T[] { - const arr = new Array(this.length); - return arr.fill(this.value); - } - - toJSON(): T[] { - return this.toArray(); - } -} - -export class ScaledVector implements Vector { - constructor(private source: Vector, private scale: number) {} - - get length(): number { - return this.source.length; - } - - get(index: number): number { - return this.source.get(index) * this.scale; - } - - toArray(): number[] { - return vectorToArray(this); - } - - toJSON(): number[] { - return vectorToArray(this); - } -} - -/** - * Values are returned in the order defined by the input parameter - */ -export class SortedVector implements Vector { - constructor(private source: Vector, private order: number[]) {} - - get length(): number { - return this.source.length; - } - - get(index: number): T { - return this.source.get(this.order[index]); - } - - toArray(): T[] { - return vectorToArray(this); - } - - toJSON(): T[] { - return vectorToArray(this); - } -} - -interface CircularOptions { - buffer?: T[]; - append?: 'head' | 'tail'; - capacity?: number; -} - -/** - * Circular vector uses a single buffer to capture a stream of values - * overwriting the oldest value on add. - * - * This supports addting to the 'head' or 'tail' and will grow the buffer - * to match a configured capacity. - */ -export class CircularVector implements MutableVector { - private buffer: T[]; - private index: number; - private capacity: number; - private tail: boolean; - - constructor(options: CircularOptions) { - this.buffer = options.buffer || []; - this.capacity = this.buffer.length; - this.tail = 'head' !== options.append; - this.index = 0; - - this.add = this.getAddFunction(); - if (options.capacity) { - this.setCapacity(options.capacity); - } - } - - /** - * This gets the appropriate add function depending on the buffer state: - * * head vs tail - * * growing buffer vs overwriting values - */ - private getAddFunction() { - // When we are not at capacity, it should actually modify the buffer - if (this.capacity > this.buffer.length) { - if (this.tail) { - return (value: T) => { - this.buffer.push(value); - if (this.buffer.length >= this.capacity) { - this.add = this.getAddFunction(); - } - }; - } else { - return (value: T) => { - this.buffer.unshift(value); - if (this.buffer.length >= this.capacity) { - this.add = this.getAddFunction(); - } - }; - } - } - - if (this.tail) { - return (value: T) => { - this.buffer[this.index] = value; - this.index = (this.index + 1) % this.buffer.length; - }; - } - - // Append values to the head - return (value: T) => { - let idx = this.index - 1; - if (idx < 0) { - idx = this.buffer.length - 1; - } - this.buffer[idx] = value; - this.index = idx; - }; - } - - setCapacity(v: number) { - if (this.capacity === v) { - return; - } - // Make a copy so it is in order and new additions can be at the head or tail - const copy = this.toArray(); - if (v > this.length) { - this.buffer = copy; - } else if (v < this.capacity) { - // Shrink the buffer - const delta = this.length - v; - if (this.tail) { - this.buffer = copy.slice(delta, copy.length); // Keep last items - } else { - this.buffer = copy.slice(0, copy.length - delta); // Keep first items - } - } - this.capacity = v; - this.index = 0; - this.add = this.getAddFunction(); - } - - setAppendMode(mode: 'head' | 'tail') { - const tail = 'head' !== mode; - if (tail !== this.tail) { - this.buffer = this.toArray().reverse(); - this.index = 0; - this.tail = tail; - this.add = this.getAddFunction(); - } - } - - reverse() { - this.buffer.reverse(); - } - - /** - * Add the value to the buffer - */ - add: (value: T) => void; - - get(index: number) { - return this.buffer[(index + this.index) % this.buffer.length]; - } - - set(index: number, value: T) { - this.buffer[(index + this.index) % this.buffer.length] = value; - } - - get length() { - return this.buffer.length; - } - - toArray(): T[] { - return vectorToArray(this); - } - - toJSON(): T[] { - return vectorToArray(this); - } -} - -interface AppendedVectorInfo { - start: number; - end: number; - values: Vector; -} - -/** - * This may be more trouble than it is worth. This trades some computation time for - * RAM -- rather than allocate a new array the size of all previous arrays, this just - * points the correct index to their original array values - */ -export class AppendedVectors implements Vector { - length = 0; - source: Array> = new Array>(); - - constructor(startAt = 0) { - this.length = startAt; - } - - /** - * Make the vector look like it is this long - */ - setLength(length: number) { - if (length > this.length) { - // make the vector longer (filling with undefined) - this.length = length; - } else if (length < this.length) { - // make the array shorter - const sources: Array> = new Array>(); - for (const src of this.source) { - sources.push(src); - if (src.end > length) { - src.end = length; - break; - } - } - this.source = sources; - this.length = length; - } - } - - append(v: Vector): AppendedVectorInfo { - const info = { - start: this.length, - end: this.length + v.length, - values: v, - }; - this.length = info.end; - this.source.push(info); - return info; - } - - get(index: number): T { - for (let i = 0; i < this.source.length; i++) { - const src = this.source[i]; - if (index >= src.start && index < src.end) { - return src.values.get(index - src.start); - } - } - return (undefined as unknown) as T; - } - - toArray(): T[] { - return vectorToArray(this); - } - - toJSON(): T[] { - return vectorToArray(this); - } -} diff --git a/packages/grafana-data/src/vector/AppendedVectors.test.ts b/packages/grafana-data/src/vector/AppendedVectors.test.ts new file mode 100644 index 00000000000..9d39b47ce09 --- /dev/null +++ b/packages/grafana-data/src/vector/AppendedVectors.test.ts @@ -0,0 +1,23 @@ +import { ArrayVector } from './ArrayVector'; +import { AppendedVectors } from './AppendedVectors'; + +describe('Check Appending Vector', () => { + it('should transparently join them', () => { + const appended = new AppendedVectors(); + appended.append(new ArrayVector([1, 2, 3])); + appended.append(new ArrayVector([4, 5, 6])); + appended.append(new ArrayVector([7, 8, 9])); + expect(appended.length).toEqual(9); + + appended.setLength(5); + expect(appended.length).toEqual(5); + appended.append(new ArrayVector(['a', 'b', 'c'])); + expect(appended.length).toEqual(8); + expect(appended.toArray()).toEqual([1, 2, 3, 4, 5, 'a', 'b', 'c']); + + appended.setLength(2); + appended.setLength(6); + appended.append(new ArrayVector(['x', 'y', 'z'])); + expect(appended.toArray()).toEqual([1, 2, undefined, undefined, undefined, undefined, 'x', 'y', 'z']); + }); +}); diff --git a/packages/grafana-data/src/vector/AppendedVectors.ts b/packages/grafana-data/src/vector/AppendedVectors.ts new file mode 100644 index 00000000000..7cfc0d42c47 --- /dev/null +++ b/packages/grafana-data/src/vector/AppendedVectors.ts @@ -0,0 +1,73 @@ +import { Vector } from '../types/vector'; +import { vectorToArray } from './vectorToArray'; + +interface AppendedVectorInfo { + start: number; + end: number; + values: Vector; +} + +/** + * This may be more trouble than it is worth. This trades some computation time for + * RAM -- rather than allocate a new array the size of all previous arrays, this just + * points the correct index to their original array values + */ +export class AppendedVectors implements Vector { + length = 0; + source: Array> = new Array>(); + + constructor(startAt = 0) { + this.length = startAt; + } + + /** + * Make the vector look like it is this long + */ + setLength(length: number) { + if (length > this.length) { + // make the vector longer (filling with undefined) + this.length = length; + } else if (length < this.length) { + // make the array shorter + const sources: Array> = new Array>(); + for (const src of this.source) { + sources.push(src); + if (src.end > length) { + src.end = length; + break; + } + } + this.source = sources; + this.length = length; + } + } + + append(v: Vector): AppendedVectorInfo { + const info = { + start: this.length, + end: this.length + v.length, + values: v, + }; + this.length = info.end; + this.source.push(info); + return info; + } + + get(index: number): T { + for (let i = 0; i < this.source.length; i++) { + const src = this.source[i]; + if (index >= src.start && index < src.end) { + return src.values.get(index - src.start); + } + } + return (undefined as unknown) as T; + } + + toArray(): T[] { + return vectorToArray(this); + } + + toJSON(): T[] { + return vectorToArray(this); + } +} diff --git a/packages/grafana-data/src/vector/ArrayVector.ts b/packages/grafana-data/src/vector/ArrayVector.ts new file mode 100644 index 00000000000..63c614f1cc4 --- /dev/null +++ b/packages/grafana-data/src/vector/ArrayVector.ts @@ -0,0 +1,37 @@ +import { MutableVector } from '../types/vector'; + +export class ArrayVector implements MutableVector { + buffer: T[]; + + constructor(buffer?: T[]) { + this.buffer = buffer ? buffer : []; + } + + get length() { + return this.buffer.length; + } + + add(value: T) { + this.buffer.push(value); + } + + get(index: number): T { + return this.buffer[index]; + } + + set(index: number, value: T) { + this.buffer[index] = value; + } + + reverse() { + this.buffer.reverse(); + } + + toArray(): T[] { + return this.buffer; + } + + toJSON(): T[] { + return this.buffer; + } +} diff --git a/packages/grafana-data/src/utils/vector.test.ts b/packages/grafana-data/src/vector/CircularVector.test.ts similarity index 65% rename from packages/grafana-data/src/utils/vector.test.ts rename to packages/grafana-data/src/vector/CircularVector.test.ts index 1806020ed59..abffd15846b 100644 --- a/packages/grafana-data/src/utils/vector.test.ts +++ b/packages/grafana-data/src/vector/CircularVector.test.ts @@ -1,31 +1,4 @@ -import { ConstantVector, ScaledVector, ArrayVector, CircularVector, AppendedVectors } from './vector'; - -describe('Check Proxy Vector', () => { - it('should support constant values', () => { - const value = 3.5; - const v = new ConstantVector(value, 7); - expect(v.length).toEqual(7); - - expect(v.get(0)).toEqual(value); - expect(v.get(1)).toEqual(value); - - // Now check all of them - for (let i = 0; i < 10; i++) { - expect(v.get(i)).toEqual(value); - } - }); - - it('should support multiply operations', () => { - const source = new ArrayVector([1, 2, 3, 4]); - const scale = 2.456; - const v = new ScaledVector(source, scale); - expect(v.length).toEqual(source.length); - // expect(v.push(10)).toEqual(source.length); // not implemented - for (let i = 0; i < 10; i++) { - expect(v.get(i)).toEqual(source.get(i) * scale); - } - }); -}); +import { CircularVector } from './CircularVector'; describe('Check Circular Vector', () => { it('should append values', () => { @@ -156,24 +129,3 @@ describe('Check Circular Vector', () => { expect(v.toArray()).toEqual([3, 4, 5]); }); }); - -describe('Check Appending Vector', () => { - it('should transparently join them', () => { - const appended = new AppendedVectors(); - appended.append(new ArrayVector([1, 2, 3])); - appended.append(new ArrayVector([4, 5, 6])); - appended.append(new ArrayVector([7, 8, 9])); - expect(appended.length).toEqual(9); - - appended.setLength(5); - expect(appended.length).toEqual(5); - appended.append(new ArrayVector(['a', 'b', 'c'])); - expect(appended.length).toEqual(8); - expect(appended.toArray()).toEqual([1, 2, 3, 4, 5, 'a', 'b', 'c']); - - appended.setLength(2); - appended.setLength(6); - appended.append(new ArrayVector(['x', 'y', 'z'])); - expect(appended.toArray()).toEqual([1, 2, undefined, undefined, undefined, undefined, 'x', 'y', 'z']); - }); -}); diff --git a/packages/grafana-data/src/vector/CircularVector.ts b/packages/grafana-data/src/vector/CircularVector.ts new file mode 100644 index 00000000000..f0eeb6579f0 --- /dev/null +++ b/packages/grafana-data/src/vector/CircularVector.ts @@ -0,0 +1,138 @@ +import { MutableVector } from '../types/vector'; +import { vectorToArray } from './vectorToArray'; + +interface CircularOptions { + buffer?: T[]; + append?: 'head' | 'tail'; + capacity?: number; +} + +/** + * Circular vector uses a single buffer to capture a stream of values + * overwriting the oldest value on add. + * + * This supports addting to the 'head' or 'tail' and will grow the buffer + * to match a configured capacity. + */ +export class CircularVector implements MutableVector { + private buffer: T[]; + private index: number; + private capacity: number; + private tail: boolean; + + constructor(options: CircularOptions) { + this.buffer = options.buffer || []; + this.capacity = this.buffer.length; + this.tail = 'head' !== options.append; + this.index = 0; + + this.add = this.getAddFunction(); + if (options.capacity) { + this.setCapacity(options.capacity); + } + } + + /** + * This gets the appropriate add function depending on the buffer state: + * * head vs tail + * * growing buffer vs overwriting values + */ + private getAddFunction() { + // When we are not at capacity, it should actually modify the buffer + if (this.capacity > this.buffer.length) { + if (this.tail) { + return (value: T) => { + this.buffer.push(value); + if (this.buffer.length >= this.capacity) { + this.add = this.getAddFunction(); + } + }; + } else { + return (value: T) => { + this.buffer.unshift(value); + if (this.buffer.length >= this.capacity) { + this.add = this.getAddFunction(); + } + }; + } + } + + if (this.tail) { + return (value: T) => { + this.buffer[this.index] = value; + this.index = (this.index + 1) % this.buffer.length; + }; + } + + // Append values to the head + return (value: T) => { + let idx = this.index - 1; + if (idx < 0) { + idx = this.buffer.length - 1; + } + this.buffer[idx] = value; + this.index = idx; + }; + } + + setCapacity(v: number) { + if (this.capacity === v) { + return; + } + // Make a copy so it is in order and new additions can be at the head or tail + const copy = this.toArray(); + if (v > this.length) { + this.buffer = copy; + } else if (v < this.capacity) { + // Shrink the buffer + const delta = this.length - v; + if (this.tail) { + this.buffer = copy.slice(delta, copy.length); // Keep last items + } else { + this.buffer = copy.slice(0, copy.length - delta); // Keep first items + } + } + this.capacity = v; + this.index = 0; + this.add = this.getAddFunction(); + } + + setAppendMode(mode: 'head' | 'tail') { + const tail = 'head' !== mode; + if (tail !== this.tail) { + this.buffer = this.toArray().reverse(); + this.index = 0; + this.tail = tail; + this.add = this.getAddFunction(); + } + } + + reverse() { + this.buffer.reverse(); + } + + /** + * Add the value to the buffer + */ + add: (value: T) => void; + + get(index: number) { + return this.buffer[(index + this.index) % this.buffer.length]; + } + + set(index: number, value: T) { + this.buffer[(index + this.index) % this.buffer.length] = value; + } + + get length() { + return this.buffer.length; + } + + toArray(): T[] { + return vectorToArray(this); + } + + toJSON(): T[] { + return vectorToArray(this); + } +} diff --git a/packages/grafana-data/src/vector/ConstantVector.test.ts b/packages/grafana-data/src/vector/ConstantVector.test.ts new file mode 100644 index 00000000000..b1b942209ee --- /dev/null +++ b/packages/grafana-data/src/vector/ConstantVector.test.ts @@ -0,0 +1,17 @@ +import { ConstantVector } from './ConstantVector'; + +describe('ConstantVector', () => { + it('should support constant values', () => { + const value = 3.5; + const v = new ConstantVector(value, 7); + expect(v.length).toEqual(7); + + expect(v.get(0)).toEqual(value); + expect(v.get(1)).toEqual(value); + + // Now check all of them + for (let i = 0; i < 10; i++) { + expect(v.get(i)).toEqual(value); + } + }); +}); diff --git a/packages/grafana-data/src/vector/ConstantVector.ts b/packages/grafana-data/src/vector/ConstantVector.ts new file mode 100644 index 00000000000..07b3beddc19 --- /dev/null +++ b/packages/grafana-data/src/vector/ConstantVector.ts @@ -0,0 +1,22 @@ +import { Vector } from '../types/vector'; + +export class ConstantVector implements Vector { + constructor(private value: T, private len: number) {} + + get length() { + return this.len; + } + + get(index: number): T { + return this.value; + } + + toArray(): T[] { + const arr = new Array(this.length); + return arr.fill(this.value); + } + + toJSON(): T[] { + return this.toArray(); + } +} diff --git a/packages/grafana-data/src/vector/ScaledVector.test.ts b/packages/grafana-data/src/vector/ScaledVector.test.ts new file mode 100644 index 00000000000..b3951135594 --- /dev/null +++ b/packages/grafana-data/src/vector/ScaledVector.test.ts @@ -0,0 +1,15 @@ +import { ArrayVector } from './ArrayVector'; +import { ScaledVector } from './ScaledVector'; + +describe('ScaledVector', () => { + it('should support multiply operations', () => { + const source = new ArrayVector([1, 2, 3, 4]); + const scale = 2.456; + const v = new ScaledVector(source, scale); + expect(v.length).toEqual(source.length); + // expect(v.push(10)).toEqual(source.length); // not implemented + for (let i = 0; i < 10; i++) { + expect(v.get(i)).toEqual(source.get(i) * scale); + } + }); +}); diff --git a/packages/grafana-data/src/vector/ScaledVector.ts b/packages/grafana-data/src/vector/ScaledVector.ts new file mode 100644 index 00000000000..0656291d7d0 --- /dev/null +++ b/packages/grafana-data/src/vector/ScaledVector.ts @@ -0,0 +1,22 @@ +import { Vector } from '../types/vector'; +import { vectorToArray } from './vectorToArray'; + +export class ScaledVector implements Vector { + constructor(private source: Vector, private scale: number) {} + + get length(): number { + return this.source.length; + } + + get(index: number): number { + return this.source.get(index) * this.scale; + } + + toArray(): number[] { + return vectorToArray(this); + } + + toJSON(): number[] { + return vectorToArray(this); + } +} diff --git a/packages/grafana-data/src/vector/SortedVector.ts b/packages/grafana-data/src/vector/SortedVector.ts new file mode 100644 index 00000000000..33b97f6778f --- /dev/null +++ b/packages/grafana-data/src/vector/SortedVector.ts @@ -0,0 +1,25 @@ +import { Vector } from '../types/vector'; +import { vectorToArray } from './vectorToArray'; + +/** + * Values are returned in the order defined by the input parameter + */ +export class SortedVector implements Vector { + constructor(private source: Vector, private order: number[]) {} + + get length(): number { + return this.source.length; + } + + get(index: number): T { + return this.source.get(this.order[index]); + } + + toArray(): T[] { + return vectorToArray(this); + } + + toJSON(): T[] { + return vectorToArray(this); + } +} diff --git a/packages/grafana-data/src/vector/index.ts b/packages/grafana-data/src/vector/index.ts new file mode 100644 index 00000000000..392cb379610 --- /dev/null +++ b/packages/grafana-data/src/vector/index.ts @@ -0,0 +1,6 @@ +export * from './AppendedVectors'; +export * from './ArrayVector'; +export * from './CircularVector'; +export * from './ConstantVector'; +export * from './ScaledVector'; +export * from './SortedVector'; diff --git a/packages/grafana-data/src/vector/vectorToArray.ts b/packages/grafana-data/src/vector/vectorToArray.ts new file mode 100644 index 00000000000..34212651ab8 --- /dev/null +++ b/packages/grafana-data/src/vector/vectorToArray.ts @@ -0,0 +1,9 @@ +import { Vector } from '../types/vector'; + +export function vectorToArray(v: Vector): T[] { + const arr: T[] = []; + for (let i = 0; i < v.length; i++) { + arr[i] = v.get(i); + } + return arr; +}