Transforms: Labels to fields rewrite that uses merge transform inside it (#27125)
* testing things * POC: Simplify labels to fields by using merge transform inside it * removed old code * Fixed test ts issues * Added valueLabel field option * Updated merge transform tests to not expect a sort * fixed type * refactoring to minimize nesting
This commit is contained in:
@@ -1,17 +1,75 @@
|
|||||||
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
|
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
|
||||||
import { LabelsToFieldsOptions, labelsToFieldsTransformer } from './labelsToFields';
|
import { LabelsToFieldsOptions, labelsToFieldsTransformer } from './labelsToFields';
|
||||||
import { DataTransformerConfig, Field, FieldType } from '../../types';
|
import { DataTransformerConfig, FieldType, FieldDTO } from '../../types';
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
import { toDataFrame } from '../../dataframe';
|
import { toDataFrame, toDataFrameDTO } from '../../dataframe';
|
||||||
import { transformDataFrame } from '../transformDataFrame';
|
import { transformDataFrame } from '../transformDataFrame';
|
||||||
import { ArrayVector } from '../../vector';
|
|
||||||
|
|
||||||
describe('Labels as Columns', () => {
|
describe('Labels as Columns', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockTransformationsRegistry([labelsToFieldsTransformer]);
|
mockTransformationsRegistry([labelsToFieldsTransformer]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('data frame with 1 value and 1 label', () => {
|
it('data frame with two labels', () => {
|
||||||
|
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
||||||
|
id: DataTransformerID.labelsToFields,
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const source = toDataFrame({
|
||||||
|
name: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [1000, 2000] },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: [1, 2], labels: { location: 'inside', feelsLike: 'ok' } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = toDataFrameDTO(transformDataFrame([cfg], [source])[0]);
|
||||||
|
const expected: FieldDTO[] = [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [1000, 2000], config: {} },
|
||||||
|
{
|
||||||
|
name: 'location',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['inside', 'inside'],
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
{ name: 'feelsLike', type: FieldType.string, values: ['ok', 'ok'], config: {} },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: [1, 2], config: {} },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(result.fields).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('data frame with two labels and valueLabel option', () => {
|
||||||
|
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
||||||
|
id: DataTransformerID.labelsToFields,
|
||||||
|
options: { valueLabel: 'name' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const source = toDataFrame({
|
||||||
|
name: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [1000, 2000] },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: [1, 2], labels: { location: 'inside', name: 'Request' } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = toDataFrameDTO(transformDataFrame([cfg], [source])[0]);
|
||||||
|
const expected: FieldDTO[] = [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [1000, 2000], config: {} },
|
||||||
|
{
|
||||||
|
name: 'location',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['inside', 'inside'],
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
{ name: 'Request', type: FieldType.number, values: [1, 2], config: {} },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(result.fields).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('two data frames with 1 value and 1 label', () => {
|
||||||
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
||||||
id: DataTransformerID.labelsToFields,
|
id: DataTransformerID.labelsToFields,
|
||||||
options: {},
|
options: {},
|
||||||
@@ -33,200 +91,14 @@ describe('Labels as Columns', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = transformDataFrame([cfg], [oneValueOneLabelA, oneValueOneLabelB]);
|
const result = toDataFrameDTO(transformDataFrame([cfg], [oneValueOneLabelA, oneValueOneLabelB])[0]);
|
||||||
const expected: Field[] = [
|
|
||||||
{ name: 'time', type: FieldType.time, values: new ArrayVector([1000, 2000]), config: {} },
|
const expected: FieldDTO[] = [
|
||||||
{ name: 'location', type: FieldType.string, values: new ArrayVector(['inside', 'outside']), config: {} },
|
{ name: 'time', type: FieldType.time, values: [1000, 2000], config: {} },
|
||||||
{ name: 'temp', type: FieldType.number, values: new ArrayVector([1, -1]), config: {} },
|
{ name: 'location', type: FieldType.string, values: ['inside', 'outside'], config: {} },
|
||||||
|
{ name: 'temp', type: FieldType.number, values: [1, -1], config: {} },
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(result[0].fields).toEqual(expected);
|
expect(result.fields).toEqual(expected);
|
||||||
});
|
|
||||||
|
|
||||||
it('data frame with 2 values and 1 label', () => {
|
|
||||||
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
|
||||||
id: DataTransformerID.labelsToFields,
|
|
||||||
options: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const twoValuesOneLabelA = toDataFrame({
|
|
||||||
name: 'A',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [1000] },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: [1], labels: { location: 'inside' } },
|
|
||||||
{ name: 'humidity', type: FieldType.number, values: [10000], labels: { location: 'inside' } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const twoValuesOneLabelB = toDataFrame({
|
|
||||||
name: 'B',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [2000] },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: [-1], labels: { location: 'outside' } },
|
|
||||||
{ name: 'humidity', type: FieldType.number, values: [11000], labels: { location: 'outside' } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDataFrame([cfg], [twoValuesOneLabelA, twoValuesOneLabelB]);
|
|
||||||
const expected: Field[] = [
|
|
||||||
{ name: 'time', type: FieldType.time, values: new ArrayVector([1000, 2000]), config: {} },
|
|
||||||
{ name: 'location', type: FieldType.string, values: new ArrayVector(['inside', 'outside']), config: {} },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: new ArrayVector([1, -1]), config: {} },
|
|
||||||
{ name: 'humidity', type: FieldType.number, values: new ArrayVector([10000, 11000]), config: {} },
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(result[0].fields).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('data frame with 1 value and 2 labels', () => {
|
|
||||||
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
|
||||||
id: DataTransformerID.labelsToFields,
|
|
||||||
options: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const oneValueTwoLabelsA = toDataFrame({
|
|
||||||
name: 'A',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [1000] },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: [1], labels: { location: 'inside', area: 'living room' } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const oneValueTwoLabelsB = toDataFrame({
|
|
||||||
name: 'B',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [2000] },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: [-1], labels: { location: 'outside', area: 'backyard' } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDataFrame([cfg], [oneValueTwoLabelsA, oneValueTwoLabelsB]);
|
|
||||||
const expected: Field[] = [
|
|
||||||
{ name: 'time', type: FieldType.time, values: new ArrayVector([1000, 2000]), config: {} },
|
|
||||||
{ name: 'location', type: FieldType.string, values: new ArrayVector(['inside', 'outside']), config: {} },
|
|
||||||
{ name: 'area', type: FieldType.string, values: new ArrayVector(['living room', 'backyard']), config: {} },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: new ArrayVector([1, -1]), config: {} },
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(result[0].fields).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('data frame with 2 values and 2 labels', () => {
|
|
||||||
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
|
||||||
id: DataTransformerID.labelsToFields,
|
|
||||||
options: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const twoValuesTwoLabelsA = toDataFrame({
|
|
||||||
name: 'A',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [1000] },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: [1], labels: { location: 'inside', area: 'living room' } },
|
|
||||||
{
|
|
||||||
name: 'humidity',
|
|
||||||
type: FieldType.number,
|
|
||||||
values: [10000],
|
|
||||||
labels: { location: 'inside', area: 'living room' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const twoValuesTwoLabelsB = toDataFrame({
|
|
||||||
name: 'B',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [2000] },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: [-1], labels: { location: 'outside', area: 'backyard' } },
|
|
||||||
{
|
|
||||||
name: 'humidity',
|
|
||||||
type: FieldType.number,
|
|
||||||
values: [11000],
|
|
||||||
labels: { location: 'outside', area: 'backyard' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDataFrame([cfg], [twoValuesTwoLabelsA, twoValuesTwoLabelsB]);
|
|
||||||
const expected: Field[] = [
|
|
||||||
{ name: 'time', type: FieldType.time, values: new ArrayVector([1000, 2000]), config: {} },
|
|
||||||
{ name: 'location', type: FieldType.string, values: new ArrayVector(['inside', 'outside']), config: {} },
|
|
||||||
{ name: 'area', type: FieldType.string, values: new ArrayVector(['living room', 'backyard']), config: {} },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: new ArrayVector([1, -1]), config: {} },
|
|
||||||
{ name: 'humidity', type: FieldType.number, values: new ArrayVector([10000, 11000]), config: {} },
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(result[0].fields).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('data frames with different labels', () => {
|
|
||||||
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
|
||||||
id: DataTransformerID.labelsToFields,
|
|
||||||
options: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const oneValueDifferentLabelsA = toDataFrame({
|
|
||||||
name: 'A',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [1000] },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: [1], labels: { location: 'inside', feelsLike: 'ok' } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const oneValueDifferentLabelsB = toDataFrame({
|
|
||||||
name: 'B',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [2000] },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: [-1], labels: { location: 'outside', sky: 'cloudy' } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDataFrame([cfg], [oneValueDifferentLabelsA, oneValueDifferentLabelsB]);
|
|
||||||
const expected: Field[] = [
|
|
||||||
{ name: 'time', type: FieldType.time, values: new ArrayVector([1000, 2000]), config: {} },
|
|
||||||
{ name: 'location', type: FieldType.string, values: new ArrayVector(['inside', 'outside']), config: {} },
|
|
||||||
{ name: 'feelsLike', type: FieldType.string, values: new ArrayVector(['ok', null]), config: {} },
|
|
||||||
{ name: 'sky', type: FieldType.string, values: new ArrayVector([null, 'cloudy']), config: {} },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: new ArrayVector([1, -1]), config: {} },
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(result[0].fields).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('data frames with same timestamp and different labels', () => {
|
|
||||||
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
|
||||||
id: DataTransformerID.labelsToFields,
|
|
||||||
options: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const oneValueDifferentLabelsA = toDataFrame({
|
|
||||||
name: 'A',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [1000, 2000] },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: [1, 2], labels: { location: 'inside', feelsLike: 'ok' } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const oneValueDifferentLabelsB = toDataFrame({
|
|
||||||
name: 'B',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [1000, 2000] },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: [-1, -2], labels: { location: 'outside', sky: 'cloudy' } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDataFrame([cfg], [oneValueDifferentLabelsA, oneValueDifferentLabelsB]);
|
|
||||||
const expected: Field[] = [
|
|
||||||
{ name: 'time', type: FieldType.time, values: new ArrayVector([1000, 1000, 2000, 2000]), config: {} },
|
|
||||||
{
|
|
||||||
name: 'location',
|
|
||||||
type: FieldType.string,
|
|
||||||
values: new ArrayVector(['inside', 'outside', 'inside', 'outside']),
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
{ name: 'feelsLike', type: FieldType.string, values: new ArrayVector(['ok', null, 'ok', null]), config: {} },
|
|
||||||
{ name: 'sky', type: FieldType.string, values: new ArrayVector([null, 'cloudy', null, 'cloudy']), config: {} },
|
|
||||||
{ name: 'temp', type: FieldType.number, values: new ArrayVector([1, -1, 2, -2]), config: {} },
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(result[0].fields).toEqual(expected);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,173 +1,68 @@
|
|||||||
import { DataFrame, DataTransformerInfo, FieldType, Field } from '../../types';
|
import { DataFrame, DataTransformerInfo, FieldType, Field } from '../../types';
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
import { ArrayVector } from '../../vector';
|
import { ArrayVector } from '../../vector';
|
||||||
import { filterFieldsTransformer } from './filter';
|
import { mergeTransformer } from './merge';
|
||||||
import { FieldMatcherID } from '..';
|
|
||||||
import { MutableField } from '../../dataframe';
|
|
||||||
|
|
||||||
export interface LabelsToFieldsOptions {}
|
export interface LabelsToFieldsOptions {
|
||||||
type MapItem = { type: FieldType; values: Record<string, any>; isValue: boolean };
|
/*
|
||||||
type SeriesMapItem = Record<string, MapItem>;
|
* If set this will use this label's value as the value field name.
|
||||||
type Map = Record<string, SeriesMapItem>;
|
*/
|
||||||
|
valueLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const labelsToFieldsTransformer: DataTransformerInfo<LabelsToFieldsOptions> = {
|
export const labelsToFieldsTransformer: DataTransformerInfo<LabelsToFieldsOptions> = {
|
||||||
id: DataTransformerID.labelsToFields,
|
id: DataTransformerID.labelsToFields,
|
||||||
name: 'Labels to fields',
|
name: 'Labels to fields',
|
||||||
description: 'Groups series by time and return labels as columns',
|
description: 'Extract time series labels to fields (columns)',
|
||||||
defaultOptions: {},
|
defaultOptions: {},
|
||||||
transformer: options => (data: DataFrame[]) => {
|
transformer: options => (data: DataFrame[]) => {
|
||||||
const framesWithTimeField = filterFieldsTransformer.transformer({ include: { id: FieldMatcherID.time } })(data);
|
const result: DataFrame[] = [];
|
||||||
if (!framesWithTimeField.length || !framesWithTimeField[0].fields.length) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get frames with only value fields
|
for (const frame of data) {
|
||||||
const framesWithoutTimeField = getFramesWithOnlyValueFields(data);
|
const newFields: Field[] = [];
|
||||||
if (!framesWithoutTimeField.length || !framesWithoutTimeField[0].fields.length) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnsMap = createColumnsMap(framesWithTimeField, framesWithoutTimeField);
|
for (const field of frame.fields) {
|
||||||
const fields = createFields(columnsMap);
|
if (!field.labels) {
|
||||||
const values: Record<string, any[]> = {};
|
newFields.push(field);
|
||||||
|
continue;
|
||||||
const timeColumnItem = columnsMap[fields[0].name];
|
|
||||||
const seriesIndexStrings = Object.keys(timeColumnItem);
|
|
||||||
for (const seriesIndexString of seriesIndexStrings) {
|
|
||||||
const seriesItem = timeColumnItem[seriesIndexString];
|
|
||||||
const timeValueStrings = Object.keys(seriesItem.values);
|
|
||||||
|
|
||||||
for (const timeValueString of timeValueStrings) {
|
|
||||||
if (!values[timeValueString]) {
|
|
||||||
values[timeValueString] = [];
|
|
||||||
}
|
}
|
||||||
let row = new Array(fields.length);
|
|
||||||
for (let index = 0; index < fields.length; index++) {
|
let name = field.name;
|
||||||
const field = fields[index];
|
|
||||||
const valueItem = columnsMap[field.name][seriesIndexString];
|
for (const labelName of Object.keys(field.labels)) {
|
||||||
const value = valueItem ? valueItem.values[timeValueString] ?? null : null;
|
// if we should use this label as the value field name store it and skip adding this as a seperate field
|
||||||
row[index] = value;
|
if (options.valueLabel === labelName) {
|
||||||
|
name = field.labels[labelName];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = new Array(frame.length).fill(field.labels[labelName]);
|
||||||
|
newFields.push({
|
||||||
|
name: labelName,
|
||||||
|
type: FieldType.string,
|
||||||
|
values: new ArrayVector(values),
|
||||||
|
config: {},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
values[timeValueString].push(row);
|
|
||||||
|
// add the value field but clear out any labels or displayName
|
||||||
|
newFields.push({
|
||||||
|
...field,
|
||||||
|
name,
|
||||||
|
config: {
|
||||||
|
...field.config,
|
||||||
|
displayName: undefined,
|
||||||
|
},
|
||||||
|
labels: undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
fields: newFields,
|
||||||
|
length: frame.length,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamps = Object.values(values);
|
return mergeTransformer.transformer({})(result);
|
||||||
for (const timestamp of timestamps) {
|
|
||||||
for (const row of timestamp) {
|
|
||||||
for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
|
|
||||||
fields[fieldIndex].values.add(row[fieldIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
fields,
|
|
||||||
length: fields[0].values.length,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFramesWithOnlyValueFields(data: DataFrame[]): DataFrame[] {
|
|
||||||
const processed: DataFrame[] = [];
|
|
||||||
|
|
||||||
for (const series of data) {
|
|
||||||
const fields: Field[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < series.fields.length; i++) {
|
|
||||||
const field = series.fields[i];
|
|
||||||
|
|
||||||
if (field.type !== FieldType.number) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
fields.push(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fields.length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const copy = {
|
|
||||||
...series, // all the other properties
|
|
||||||
fields, // but a different set of fields
|
|
||||||
};
|
|
||||||
|
|
||||||
processed.push(copy);
|
|
||||||
}
|
|
||||||
|
|
||||||
return processed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addOrAppendMapItem(args: { map: Map; series: number; column: string; type: FieldType; isValue?: boolean }) {
|
|
||||||
const { map, column, type, series, isValue = false } = args;
|
|
||||||
// we're using the fact that the series (number) will automatically become a string prop on the object
|
|
||||||
const seriesMapItem: SeriesMapItem = { [series]: { type, values: {}, isValue } };
|
|
||||||
if (!map[column]) {
|
|
||||||
map[column] = seriesMapItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map[column][series]) {
|
|
||||||
map[column] = { ...map[column], ...seriesMapItem };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is a naive implementation that does the job, not optimized for performance or speed
|
|
||||||
function createColumnsMap(framesWithTimeField: DataFrame[], framesWithoutTimeField: DataFrame[]) {
|
|
||||||
const map: Map = {};
|
|
||||||
|
|
||||||
for (let frameIndex = 0; frameIndex < framesWithTimeField.length; frameIndex++) {
|
|
||||||
const timeFrame = framesWithTimeField[frameIndex];
|
|
||||||
const otherFrame = framesWithoutTimeField[frameIndex];
|
|
||||||
const timeField = timeFrame.fields[0];
|
|
||||||
|
|
||||||
addOrAppendMapItem({ map, column: timeField.name, series: frameIndex, type: timeField.type });
|
|
||||||
|
|
||||||
for (let valueIndex = 0; valueIndex < timeFrame.length; valueIndex++) {
|
|
||||||
const timeFieldValue = timeField.values.get(valueIndex);
|
|
||||||
map[timeField.name][frameIndex].values[timeFieldValue] = timeFieldValue;
|
|
||||||
|
|
||||||
for (const field of otherFrame.fields) {
|
|
||||||
if (field.labels) {
|
|
||||||
const labels = Object.keys(field.labels);
|
|
||||||
for (const label of labels) {
|
|
||||||
addOrAppendMapItem({ map, column: label, series: frameIndex, type: FieldType.string });
|
|
||||||
|
|
||||||
map[label][frameIndex].values[timeFieldValue] = field.labels[label];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const otherFieldValue = field.values.get(valueIndex);
|
|
||||||
addOrAppendMapItem({ map, column: field.name, series: frameIndex, type: field.type, isValue: true });
|
|
||||||
|
|
||||||
map[field.name][frameIndex].values[timeFieldValue] = otherFieldValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFields(columnsMap: Map): MutableField[] {
|
|
||||||
const columns = Object.keys(columnsMap);
|
|
||||||
const fields: MutableField[] = [];
|
|
||||||
const valueColumns: string[] = [];
|
|
||||||
|
|
||||||
for (const column of columns) {
|
|
||||||
const columnItem = Object.values<MapItem>(columnsMap[column])[0];
|
|
||||||
if (columnItem.isValue) {
|
|
||||||
valueColumns.push(column);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
fields.push({ type: columnItem.type, values: new ArrayVector(), name: column, config: {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const column of valueColumns) {
|
|
||||||
const columnItem = Object.values<MapItem>(columnsMap[column])[0];
|
|
||||||
fields.push({ type: columnItem.type, values: new ArrayVector(), name: column, config: {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ describe('Merge multipe to single', () => {
|
|||||||
|
|
||||||
const result = transformDataFrame([cfg], [seriesA, seriesB]);
|
const result = transformDataFrame([cfg], [seriesA, seriesB]);
|
||||||
const expected: Field[] = [
|
const expected: Field[] = [
|
||||||
createField('Time', FieldType.time, [2000, 1000]),
|
createField('Time', FieldType.time, [1000, 2000]),
|
||||||
createField('Temp', FieldType.number, [-1, 1]),
|
createField('Temp', FieldType.number, [1, -1]),
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(unwrap(result[0].fields)).toEqual(expected);
|
expect(unwrap(result[0].fields)).toEqual(expected);
|
||||||
@@ -66,8 +66,8 @@ describe('Merge multipe to single', () => {
|
|||||||
|
|
||||||
const result = transformDataFrame([cfg], [seriesA, seriesB]);
|
const result = transformDataFrame([cfg], [seriesA, seriesB]);
|
||||||
const expected: Field[] = [
|
const expected: Field[] = [
|
||||||
createField('Time', FieldType.time, [200, 150, 126, 125, 100, 100]),
|
createField('Time', FieldType.time, [100, 150, 200, 100, 125, 126]),
|
||||||
createField('Temp', FieldType.number, [5, 4, 3, 2, 1, -1]),
|
createField('Temp', FieldType.number, [1, 4, 5, -1, 2, 3]),
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(unwrap(result[0].fields)).toEqual(expected);
|
expect(unwrap(result[0].fields)).toEqual(expected);
|
||||||
@@ -105,8 +105,8 @@ describe('Merge multipe to single', () => {
|
|||||||
|
|
||||||
const result = transformDataFrame([cfg], [seriesA, seriesB, seriesC]);
|
const result = transformDataFrame([cfg], [seriesA, seriesB, seriesC]);
|
||||||
const expected: Field[] = [
|
const expected: Field[] = [
|
||||||
createField('Time', FieldType.time, [2000, 1000, 500]),
|
createField('Time', FieldType.time, [1000, 2000, 500]),
|
||||||
createField('Temp', FieldType.number, [-1, 1, 2]),
|
createField('Temp', FieldType.number, [1, -1, 2]),
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(unwrap(result[0].fields)).toEqual(expected);
|
expect(unwrap(result[0].fields)).toEqual(expected);
|
||||||
@@ -188,9 +188,9 @@ describe('Merge multipe to single', () => {
|
|||||||
|
|
||||||
const result = transformDataFrame([cfg], [tableA, seriesB, tableC]);
|
const result = transformDataFrame([cfg], [tableA, seriesB, tableC]);
|
||||||
const expected: Field[] = [
|
const expected: Field[] = [
|
||||||
createField('Time', FieldType.time, ['2019-11-01T11:10:23Z', '2019-10-01T11:10:23Z', '2019-09-01T11:10:23Z']),
|
createField('Time', FieldType.time, ['2019-10-01T11:10:23Z', '2019-09-01T11:10:23Z', '2019-11-01T11:10:23Z']),
|
||||||
createField('Temp', FieldType.number, [2, 1, -1]),
|
createField('Temp', FieldType.number, [1, -1, 2]),
|
||||||
createField('Humidity', FieldType.number, [5, 10, null]),
|
createField('Humidity', FieldType.number, [10, null, 5]),
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(unwrap(result[0].fields)).toEqual(expected);
|
expect(unwrap(result[0].fields)).toEqual(expected);
|
||||||
@@ -232,10 +232,10 @@ describe('Merge multipe to single', () => {
|
|||||||
const result = transformDataFrame([cfg], [tableA, tableB, tableC]);
|
const result = transformDataFrame([cfg], [tableA, tableB, tableC]);
|
||||||
|
|
||||||
const expected: Field[] = [
|
const expected: Field[] = [
|
||||||
createField('Time', FieldType.time, [200, 150, 149, 126, 125, 124, 100, 100, 100]),
|
createField('Time', FieldType.time, [100, 150, 200, 100, 125, 126, 100, 124, 149]),
|
||||||
createField('Temp', FieldType.number, [5, 4, 5, 3, 2, 4, 1, -1, 1]),
|
createField('Temp', FieldType.number, [1, 4, 5, -1, 2, 3, 1, 4, 5]),
|
||||||
createField('Humidity', FieldType.number, [55, 14, 30, null, null, 25, 10, null, 22]),
|
createField('Humidity', FieldType.number, [10, 14, 55, null, null, null, 22, 25, 30]),
|
||||||
createField('Enabled', FieldType.boolean, [null, null, null, true, false, null, null, true, null]),
|
createField('Enabled', FieldType.boolean, [null, null, null, true, false, true, null, null, null]),
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(unwrap(result[0].fields)).toEqual(expected);
|
expect(unwrap(result[0].fields)).toEqual(expected);
|
||||||
@@ -265,8 +265,8 @@ describe('Merge multipe to single', () => {
|
|||||||
|
|
||||||
const result = transformDataFrame([cfg], [serieA, serieB]);
|
const result = transformDataFrame([cfg], [serieA, serieB]);
|
||||||
const expected: Field[] = [
|
const expected: Field[] = [
|
||||||
createField('Time', FieldType.time, [200, 150, 126, 125, 100, 100]),
|
createField('Time', FieldType.time, [100, 150, 200, 100, 125, 126]),
|
||||||
createField('Temp', FieldType.number, [5, 4, 3, 2, 1, -1]),
|
createField('Temp', FieldType.number, [1, 4, 5, -1, 2, 3]),
|
||||||
];
|
];
|
||||||
|
|
||||||
const fields = unwrap(result[0].fields);
|
const fields = unwrap(result[0].fields);
|
||||||
@@ -299,8 +299,8 @@ describe('Merge multipe to single', () => {
|
|||||||
|
|
||||||
const result = transformDataFrame([cfg], [serieA, serieB]);
|
const result = transformDataFrame([cfg], [serieA, serieB]);
|
||||||
const expected: Field[] = [
|
const expected: Field[] = [
|
||||||
createField('Time', FieldType.time, [200, 150, 126, 125, 100, 100]),
|
createField('Time', FieldType.time, [100, 150, 200, 100, 125, 126]),
|
||||||
createField('Temp', FieldType.number, [5, 4, 3, 2, 1, -1], { units: 'celsius' }),
|
createField('Temp', FieldType.number, [1, 4, 5, -1, 2, 3], { units: 'celsius' }),
|
||||||
];
|
];
|
||||||
|
|
||||||
const fields = unwrap(result[0].fields);
|
const fields = unwrap(result[0].fields);
|
||||||
@@ -333,8 +333,8 @@ describe('Merge multipe to single', () => {
|
|||||||
|
|
||||||
const result = transformDataFrame([cfg], [serieA, serieB]);
|
const result = transformDataFrame([cfg], [serieA, serieB]);
|
||||||
const expected: Field[] = [
|
const expected: Field[] = [
|
||||||
createField('Time', FieldType.time, [200, 150, 126, 125, 100, 100]),
|
createField('Time', FieldType.time, [100, 150, 200, 100, 125, 126]),
|
||||||
createField('Temp', FieldType.number, [5, 4, 3, 2, 1, -1]),
|
createField('Temp', FieldType.number, [1, 4, 5, -1, 2, 3]),
|
||||||
];
|
];
|
||||||
|
|
||||||
const fields = unwrap(result[0].fields);
|
const fields = unwrap(result[0].fields);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
import { DataTransformerInfo } from '../../types/transformations';
|
import { DataTransformerInfo } from '../../types/transformations';
|
||||||
import { DataFrame, Field, FieldType } from '../../types/dataFrame';
|
import { DataFrame, Field } from '../../types/dataFrame';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { ArrayVector } from '../../vector/ArrayVector';
|
import { ArrayVector } from '../../vector/ArrayVector';
|
||||||
import { MutableDataFrame, sortDataFrame } from '../../dataframe';
|
import { MutableDataFrame } from '../../dataframe';
|
||||||
|
|
||||||
type MergeDetailsKeyFactory = (existing: Record<string, any>, value: Record<string, any>) => string;
|
type MergeDetailsKeyFactory = (existing: Record<string, any>, value: Record<string, any>) => string;
|
||||||
|
|
||||||
@@ -68,10 +68,10 @@ export const mergeTransformer: DataTransformerInfo<MergeTransformerOptions> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeIndex = dataFrame.fields.findIndex(field => field.type === FieldType.time);
|
// const timeIndex = dataFrame.fields.findIndex(field => field.type === FieldType.time);
|
||||||
if (typeof timeIndex === 'number') {
|
// if (typeof timeIndex === 'number') {
|
||||||
return [sortDataFrame(dataFrame, timeIndex, true)];
|
// return [sortDataFrame(dataFrame, timeIndex, true)];
|
||||||
}
|
// }
|
||||||
return [dataFrame];
|
return [dataFrame];
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DataTransformerID, standardTransformers, TransformerRegistyItem, TransformerUIProps } from '@grafana/data';
|
import {
|
||||||
|
DataTransformerID,
|
||||||
|
standardTransformers,
|
||||||
|
TransformerRegistyItem,
|
||||||
|
TransformerUIProps,
|
||||||
|
SelectableValue,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { Select } from '@grafana/ui';
|
||||||
|
|
||||||
import { LabelsToFieldsOptions } from '@grafana/data/src/transformations/transformers/labelsToFields';
|
import { LabelsToFieldsOptions } from '@grafana/data/src/transformations/transformers/labelsToFields';
|
||||||
|
|
||||||
@@ -8,7 +15,45 @@ export const LabelsAsFieldsTransformerEditor: React.FC<TransformerUIProps<Labels
|
|||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
return null;
|
let labelNames: Array<SelectableValue<string>> = [];
|
||||||
|
let uniqueLabels: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
for (const frame of input) {
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
if (!field.labels) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const labelName of Object.keys(field.labels)) {
|
||||||
|
if (!uniqueLabels[labelName]) {
|
||||||
|
labelNames.push({ value: labelName, label: labelName });
|
||||||
|
uniqueLabels[labelName] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onValueLabelChange = (value: SelectableValue<string> | null) => {
|
||||||
|
onChange({ valueLabel: value?.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<div className="gf-form">
|
||||||
|
<div className="gf-form-label width-8">Value field name</div>
|
||||||
|
<Select
|
||||||
|
isClearable={true}
|
||||||
|
allowCustomValue={false}
|
||||||
|
placeholder="(Optional) Select label"
|
||||||
|
options={labelNames}
|
||||||
|
className="min-width-18 gf-form-spacing"
|
||||||
|
value={options?.valueLabel}
|
||||||
|
onChange={onValueLabelChange}
|
||||||
|
menuPlacement="bottom"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const labelsToFieldsTransformerRegistryItem: TransformerRegistyItem<LabelsToFieldsOptions> = {
|
export const labelsToFieldsTransformerRegistryItem: TransformerRegistyItem<LabelsToFieldsOptions> = {
|
||||||
@@ -16,6 +61,6 @@ export const labelsToFieldsTransformerRegistryItem: TransformerRegistyItem<Label
|
|||||||
editor: LabelsAsFieldsTransformerEditor,
|
editor: LabelsAsFieldsTransformerEditor,
|
||||||
transformation: standardTransformers.labelsToFieldsTransformer,
|
transformation: standardTransformers.labelsToFieldsTransformer,
|
||||||
name: 'Labels to fields',
|
name: 'Labels to fields',
|
||||||
description: `Groups series by time and return labels or tags as fields.
|
description: `Groups series by time and return labels or tags as fields.
|
||||||
Useful for showing time series with labels in a table where each label key becomes a seperate column`,
|
Useful for showing time series with labels in a table where each label key becomes a seperate column`,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user