ArrayDataFrame: Convert to a simple utility function rather than dynamically loaded values (#67427)

This commit is contained in:
Ryan McKinley
2023-04-27 12:38:19 -07:00
committed by GitHub
parent 63383ef545
commit 8352e716dd
9 changed files with 128 additions and 194 deletions

View File

@@ -1,6 +1,6 @@
import { FieldType, DataFrame } from '../types';
import { DataFrame } from '../types';
import { ArrayDataFrame } from './ArrayDataFrame';
import { ArrayDataFrame, arrayToDataFrame } from './ArrayDataFrame';
import { toDataFrameDTO } from './processDataFrame';
describe('Array DataFrame', () => {
@@ -15,30 +15,9 @@ describe('Array DataFrame', () => {
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);
expect(frame[0].name).toEqual(input[0].name);
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(`
{
@@ -84,19 +63,6 @@ describe('Array DataFrame', () => {
1100,
],
},
{
"config": {},
"labels": undefined,
"name": "phantom",
"type": "string",
"values": [
"🦥",
"🦥",
"🦥",
"🦥",
"🦥",
],
},
],
"meta": undefined,
"name": "Hello",
@@ -114,4 +80,25 @@ describe('Array DataFrame', () => {
expect(copy.length).toEqual(frame.length);
expect(copy.length).toEqual(input.length);
});
test('Handles any array input', () => {
const f = arrayToDataFrame([1, 2, 3]);
expect(f).toMatchInlineSnapshot(`
{
"fields": [
{
"config": {},
"name": "Value",
"type": "number",
"values": [
1,
2,
3,
],
},
],
"length": 3,
}
`);
});
});

View File

@@ -1,118 +1,69 @@
import { makeArrayIndexableVector, QueryResultMeta } from '../types';
import { Field, FieldType, DataFrame } from '../types/dataFrame';
import { FunctionalVector } from '../vector/FunctionalVector';
import { vectorToArray } from '../vector/vectorToArray';
import { QueryResultMeta } from '../types';
import { Field, FieldType, DataFrame, TIME_SERIES_VALUE_FIELD_NAME } from '../types/dataFrame';
import { guessFieldTypeFromNameAndValue, toDataFrameDTO } from './processDataFrame';
/** @public */
export type ValueConverter<T = any> = (val: unknown) => T;
const NOOP: ValueConverter = (v) => v;
class ArrayPropertyVector<T = any> extends FunctionalVector<T> {
converter = NOOP;
constructor(private source: any[], private prop: string) {
super();
return makeArrayIndexableVector(this);
}
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);
}
}
import { guessFieldTypeForField } from './processDataFrame';
/**
* The ArrayDataFrame takes an array of objects and presents it as a DataFrame
*
* @alpha
* @deprecated use arrayToDataFrame
*/
export class ArrayDataFrame<T = any> extends FunctionalVector<T> implements DataFrame {
export class ArrayDataFrame<T = any> implements DataFrame {
fields: Field[] = [];
length = 0;
name?: string;
refId?: string;
meta?: QueryResultMeta;
fields: Field[] = [];
length = 0;
constructor(private source: T[], names?: string[]) {
super();
this.length = source.length;
const first: any = source.length ? source[0] : {};
if (names) {
this.fields = names.map((name) => {
return {
name,
type: guessFieldTypeFromNameAndValue(name, first[name]),
config: {},
values: new ArrayPropertyVector(source, name),
};
});
} else {
this.setFieldsFromObject(first);
}
return makeArrayIndexableVector(this);
}
/**
* Add a field for each property in the object. This will guess the type
*/
setFieldsFromObject(obj: Record<string, unknown>) {
this.fields = 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 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);
constructor(source: T[], names?: string[]) {
return arrayToDataFrame(source, names) as ArrayDataFrame<T>; // returns a standard DataFrame
}
}
/**
* arrayToDataFrame will convert any array into a DataFrame
*
* @public
*/
export function arrayToDataFrame(source: any[], names?: string[]): DataFrame {
const df: DataFrame = {
fields: [],
length: source.length,
};
if (!source?.length) {
return df;
}
if (names) {
for (const name of names) {
df.fields.push(
makeFieldFromValues(
name,
source.map((v) => v[name])
)
);
}
return df;
}
const first = source.find((v) => v != null); // first not null|undefined
if (first != null) {
if (typeof first === 'object') {
df.fields = Object.keys(first).map((name) => {
return makeFieldFromValues(
name,
source.map((v) => v[name])
);
});
} else {
df.fields.push(makeFieldFromValues(TIME_SERIES_VALUE_FIELD_NAME, source));
}
}
return df;
}
function makeFieldFromValues(name: string, values: unknown[]): Field {
const f = { name, config: {}, values, type: FieldType.other };
f.type = guessFieldTypeForField(f) ?? FieldType.other;
return f;
}

View File

@@ -83,10 +83,9 @@ describe('toDataFrame', () => {
{ a: 1, b: 2 },
{ a: 3, b: 4 },
];
const array = new ArrayDataFrame(orig);
const array = new ArrayDataFrame(orig); // will return a simple DataFrame
const frame = toDataFrame(array);
expect(frame).toEqual(array);
expect(frame instanceof ArrayDataFrame).toEqual(true);
expect(frame.length).toEqual(orig.length);
expect(frame.fields.map((f) => f.name)).toEqual(['a', 'b']);
});

View File

@@ -25,7 +25,7 @@ import {
GraphSeriesValue,
} from '../types/index';
import { ArrayDataFrame } from './ArrayDataFrame';
import { arrayToDataFrame } from './ArrayDataFrame';
import { dataFrameFromJSON } from './DataFrameJSON';
import { MutableDataFrame } from './MutableDataFrame';
@@ -36,7 +36,7 @@ function convertTableToDataFrame(table: TableData): DataFrame {
return {
name: text?.length ? text : c, // rename 'text' to the 'name' field
config: (disp || {}) as FieldConfig,
values: [] as any[],
values: [] as unknown[],
type: type && Object.values(FieldType).includes(type as FieldType) ? (type as FieldType) : FieldType.other,
};
});
@@ -339,7 +339,7 @@ export function toDataFrame(data: any): DataFrame {
}
if (Array.isArray(data)) {
return new ArrayDataFrame(data);
return arrayToDataFrame(data);
}
console.warn('Can not convert', data);
@@ -350,7 +350,7 @@ export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData =
const { fields } = frame;
const rowCount = frame.length;
const rows: any[][] = [];
const rows: unknown[][] = [];
if (fields.length === 2) {
const { timeField, timeIndex } = getTimeField(frame);
@@ -379,7 +379,7 @@ export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData =
}
for (let i = 0; i < rowCount; i++) {
const row: any[] = [];
const row: unknown[] = [];
for (let j = 0; j < fields.length; j++) {
row.push(fields[j].values[i]);
}
@@ -460,8 +460,8 @@ export function reverseDataFrame(data: DataFrame): DataFrame {
/**
* Wrapper to get an array from each field value
*/
export function getDataFrameRow(data: DataFrame, row: number): any[] {
const values: any[] = [];
export function getDataFrameRow(data: DataFrame, row: number): unknown[] {
const values: unknown[] = [];
for (const field of data.fields) {
values.push(field.values[row]);
}

View File

@@ -146,6 +146,8 @@ export interface QueryResultBase {
export interface Labels {
[key: string]: string;
}
/** @deprecated this is a very old (pre Grafana 7 + DataFrame) representation for tabular data */
export interface Column {
text: string; // For a Column, the 'text' is the field name
filterable?: boolean;
@@ -153,6 +155,7 @@ export interface Column {
custom?: Record<string, any>;
}
/** @deprecated this is a very old (pre Grafana 7 + DataFrame) representation for tabular data */
export interface TableData extends QueryResultBase {
name?: string;
columns: Column[];
@@ -160,10 +163,13 @@ export interface TableData extends QueryResultBase {
type?: string;
}
/** @deprecated this is a very old (pre Grafana 7 + DataFrame) representation for tabular data */
export type TimeSeriesValue = number | null;
/** @deprecated this is a very old (pre Grafana 7 + DataFrame) representation for tabular data */
export type TimeSeriesPoints = TimeSeriesValue[][];
/** @deprecated this is a very old (pre Grafana 7 + DataFrame) representation for tabular data */
export interface TimeSeries extends QueryResultBase {
target: string;
/**

View File

@@ -85,9 +85,6 @@ export abstract class FunctionalVector<T = any> implements Vector<T> {
shift(): T | undefined {
throw new Error('Method not implemented.');
}
slice(start?: number | undefined, end?: number | undefined): T[] {
throw new Error('Method not implemented.');
}
sort(compareFn?: ((a: T, b: T) => number) | undefined): this {
throw new Error('Method not implemented.');
}
@@ -121,6 +118,9 @@ export abstract class FunctionalVector<T = any> implements Vector<T> {
// Delegated Array function -- these will not be efficient :grimmice:
//--------------------------------------------------------------------------------
slice(start?: number | undefined, end?: number | undefined): T[] {
return this.toArray().slice(start, end);
}
indexOf(searchElement: T, fromIndex?: number | undefined): number {
return this.toArray().indexOf(searchElement, fromIndex);
}