Transformations: Add new grouping to matrix transformer (#28739)

* Add new transformer grouping to matrix

* Add new transformer grouping to matrix tests

* Add new transformer grouping to matrix UI

* Fix tests for grouping to matrix transformer

* Update transformer to latest interfaces

* Add field selector to form

* Make linter happier

* Replace Fields with InlineSnapshot as it was to taking units properly

* Rearrange for new transformers structure

* Expose GroupingToMatrix options as part of data package

* Increase labelWidth as suggested

* Add uniqueValues helper function and use it to extract Column and Row Values
This commit is contained in:
Borja Garrido 2022-02-24 18:01:04 +01:00 committed by GitHub
parent 91af956eb7
commit 359ef074fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 376 additions and 0 deletions

View File

@ -25,6 +25,7 @@ export { LayoutModes, LayoutMode } from './types/layout';
export { PanelPlugin, SetFieldConfigOptionsArgs, StandardOptionConfig } from './panel/PanelPlugin';
export { createFieldConfigRegistry } from './panel/registryFactories';
export { QueryRunner, QueryRunnerOptions } from './types/queryRunner';
export { GroupingToMatrixTransformerOptions } from './transformations/transformers/groupingToMatrix';
// Moved to `@grafana/schema`, in Grafana 9, this will be removed
export * from './schema';

View File

@ -19,6 +19,7 @@ import { renameByRegexTransformer } from './transformers/renameByRegex';
import { filterByValueTransformer } from './transformers/filterByValue';
import { histogramTransformer } from './transformers/histogram';
import { convertFieldTypeTransformer } from './transformers/convertFieldType';
import { groupingToMatrixTransformer } from './transformers/groupingToMatrix';
export const standardTransformers = {
noopTransformer,
@ -43,4 +44,5 @@ export const standardTransformers = {
renameByRegexTransformer,
histogramTransformer,
convertFieldTypeTransformer,
groupingToMatrixTransformer,
};

View File

@ -0,0 +1,170 @@
import {
ArrayVector,
DataTransformerConfig,
DataTransformerID,
Field,
FieldType,
toDataFrame,
transformDataFrame,
} from '@grafana/data';
import { GroupingToMatrixTransformerOptions, groupingToMatrixTransformer } from './groupingToMatrix';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
describe('Grouping to Matrix', () => {
beforeAll(() => {
mockTransformationsRegistry([groupingToMatrixTransformer]);
});
it('generates Matrix with default fields', async () => {
const cfg: DataTransformerConfig<GroupingToMatrixTransformerOptions> = {
id: DataTransformerID.groupingToMatrix,
options: {},
};
const seriesA = toDataFrame({
name: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [1000, 1001, 1002] },
{ name: 'Value', type: FieldType.number, values: [1, 2, 3] },
],
});
await expect(transformDataFrame([cfg], [seriesA])).toEmitValuesWith((received) => {
const processed = received[0];
const expected: Field[] = [
{
name: 'Time\\Time',
type: FieldType.string,
values: new ArrayVector([1000, 1001, 1002]),
config: {},
},
{
name: '1000',
type: FieldType.number,
values: new ArrayVector([1, '', '']),
config: {},
},
{
name: '1001',
type: FieldType.number,
values: new ArrayVector(['', 2, '']),
config: {},
},
{
name: '1002',
type: FieldType.number,
values: new ArrayVector(['', '', 3]),
config: {},
},
];
expect(processed[0].fields).toEqual(expected);
});
});
it('generates Matrix with multiple fields', async () => {
const cfg: DataTransformerConfig<GroupingToMatrixTransformerOptions> = {
id: DataTransformerID.groupingToMatrix,
options: {
columnField: 'Column',
rowField: 'Row',
valueField: 'Temp',
},
};
const seriesA = toDataFrame({
name: 'A',
fields: [
{ name: 'Column', type: FieldType.string, values: ['C1', 'C1', 'C2'] },
{ name: 'Row', type: FieldType.string, values: ['R1', 'R2', 'R1'] },
{ name: 'Temp', type: FieldType.number, values: [1, 4, 5] },
],
});
await expect(transformDataFrame([cfg], [seriesA])).toEmitValuesWith((received) => {
const processed = received[0];
const expected: Field[] = [
{
name: 'Row\\Column',
type: FieldType.string,
values: new ArrayVector(['R1', 'R2']),
config: {},
},
{
name: 'C1',
type: FieldType.number,
values: new ArrayVector([1, 4]),
config: {},
},
{
name: 'C2',
type: FieldType.number,
values: new ArrayVector([5, '']),
config: {},
},
];
expect(processed[0].fields).toEqual(expected);
});
});
it('generates Matrix with multiple fields and value type', async () => {
const cfg: DataTransformerConfig<GroupingToMatrixTransformerOptions> = {
id: DataTransformerID.groupingToMatrix,
options: {
columnField: 'Column',
rowField: 'Row',
valueField: 'Temp',
},
};
const seriesA = toDataFrame({
name: 'C',
fields: [
{ name: 'Column', type: FieldType.string, values: ['C1', 'C1', 'C2'] },
{ name: 'Row', type: FieldType.string, values: ['R1', 'R2', 'R1'] },
{ name: 'Temp', type: FieldType.number, values: [1, 4, 5], config: { units: 'celsius' } },
],
});
await expect(transformDataFrame([cfg], [seriesA])).toEmitValuesWith((received) => {
const processed = received[0];
expect(processed[0].fields).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {},
"name": "Row\\\\Column",
"type": "string",
"values": Array [
"R1",
"R2",
],
},
Object {
"config": Object {
"units": "celsius",
},
"name": "C1",
"type": "number",
"values": Array [
1,
4,
],
},
Object {
"config": Object {
"units": "celsius",
},
"name": "C2",
"type": "number",
"values": Array [
5,
"",
],
},
]
`);
});
});
});

View File

@ -0,0 +1,115 @@
import { map } from 'rxjs/operators';
import { DataFrame, DataTransformerInfo, Field, FieldType, Vector } from '../../types';
import { DataTransformerID } from './ids';
import { MutableDataFrame } from '../../dataframe';
import { getFieldDisplayName } from '../../field/fieldState';
export interface GroupingToMatrixTransformerOptions {
columnField?: string;
rowField?: string;
valueField?: string;
}
const DEFAULT_COLUMN_FIELD = 'Time';
const DEFAULT_ROW_FIELD = 'Time';
const DEFAULT_VALUE_FIELD = 'Value';
export const groupingToMatrixTransformer: DataTransformerInfo<GroupingToMatrixTransformerOptions> = {
id: DataTransformerID.groupingToMatrix,
name: 'Grouping to Matrix',
description: 'Groups series by field and return a matrix visualisation',
defaultOptions: {
columnField: DEFAULT_COLUMN_FIELD,
rowField: DEFAULT_ROW_FIELD,
valueField: DEFAULT_VALUE_FIELD,
},
operator: (options) => (source) =>
source.pipe(
map((data) => {
const columnFieldMatch = options.columnField || DEFAULT_COLUMN_FIELD;
const rowFieldMatch = options.rowField || DEFAULT_ROW_FIELD;
const valueFieldMatch = options.valueField || DEFAULT_VALUE_FIELD;
// Accept only single queries
if (data.length !== 1) {
return data;
}
const frame = data[0];
const keyColumnField = findKeyField(frame, columnFieldMatch);
const keyRowField = findKeyField(frame, rowFieldMatch);
const valueField = findKeyField(frame, valueFieldMatch);
const rowColumnField = `${rowFieldMatch}\\${columnFieldMatch}`;
if (!keyColumnField || !keyRowField || !valueField) {
return data;
}
const columnValues = uniqueValues(keyColumnField.values);
const rowValues = uniqueValues(keyRowField.values);
const matrixValues: { [key: string]: { [key: string]: any } } = {};
for (let index = 0; index < valueField.values.length; index++) {
const columnName = keyColumnField.values.get(index);
const rowName = keyRowField.values.get(index);
const value = valueField.values.get(index);
if (!matrixValues[columnName]) {
matrixValues[columnName] = {};
}
matrixValues[columnName][rowName] = value;
}
const resultFrame = new MutableDataFrame();
resultFrame.addField({
name: rowColumnField,
values: rowValues,
type: FieldType.string,
});
for (const columnName of columnValues) {
let values = [];
for (const rowName of rowValues) {
const value = matrixValues[columnName][rowName] ?? '';
values.push(value);
}
resultFrame.addField({
name: columnName.toString(),
values: values,
config: valueField.config,
type: valueField.type,
});
}
return [resultFrame];
})
),
};
function uniqueValues(values: Vector): any[] {
const unique = new Set();
for (let index = 0; index < values.length; index++) {
unique.add(values.get(index));
}
return Array.from(unique);
}
function findKeyField(frame: DataFrame, matchTitle: string): Field | null {
for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) {
const field = frame.fields[fieldIndex];
if (matchTitle === getFieldDisplayName(field)) {
return field;
}
}
return null;
}

View File

@ -31,4 +31,5 @@ export enum DataTransformerID {
heatmap = 'heatmap',
spatial = 'spatial',
extractFields = 'extractFields',
groupingToMatrix = 'groupingToMatrix',
}

View File

@ -0,0 +1,85 @@
import React, { useCallback } from 'react';
import {
DataTransformerID,
SelectableValue,
standardTransformers,
TransformerRegistryItem,
TransformerUIProps,
GroupingToMatrixTransformerOptions,
} from '@grafana/data';
import { InlineField, InlineFieldRow, Select } from '@grafana/ui';
import { useAllFieldNamesFromDataFrames } from '../utils';
export const GroupingToMatrixTransformerEditor: React.FC<TransformerUIProps<GroupingToMatrixTransformerOptions>> = ({
input,
options,
onChange,
}) => {
const fieldNames = useAllFieldNamesFromDataFrames(input).map((item: string) => ({ label: item, value: item }));
const onSelectColumn = useCallback(
(value: SelectableValue<string>) => {
onChange({
...options,
columnField: value.value,
});
},
[onChange, options]
);
const onSelectRow = useCallback(
(value: SelectableValue<string>) => {
onChange({
...options,
rowField: value.value,
});
},
[onChange, options]
);
const onSelectValue = useCallback(
(value: SelectableValue<string>) => {
onChange({
...options,
valueField: value.value,
});
},
[onChange, options]
);
return (
<>
<InlineFieldRow>
<InlineField label="Column" labelWidth={8}>
<Select
menuShouldPortal
options={fieldNames}
value={options.columnField}
onChange={onSelectColumn}
isClearable
/>
</InlineField>
<InlineField label="Row" labelWidth={8}>
<Select menuShouldPortal options={fieldNames} value={options.rowField} onChange={onSelectRow} isClearable />
</InlineField>
<InlineField label="Cell Value" labelWidth={10}>
<Select
menuShouldPortal
options={fieldNames}
value={options.valueField}
onChange={onSelectValue}
isClearable
/>
</InlineField>
</InlineFieldRow>
</>
);
};
export const groupingToMatrixTransformRegistryItem: TransformerRegistryItem<GroupingToMatrixTransformerOptions> = {
id: DataTransformerID.groupingToMatrix,
editor: GroupingToMatrixTransformerEditor,
transformation: standardTransformers.groupingToMatrixTransformer,
name: 'Grouping to matrix',
description: `Takes a three fields combination and produces a Matrix`,
};

View File

@ -22,6 +22,7 @@ import { fieldLookupTransformRegistryItem } from './lookupGazetteer/FieldLookupT
import { extractFieldsTransformRegistryItem } from './extractFields/ExtractFieldsTransformerEditor';
import { heatmapTransformRegistryItem } from './calculateHeatmap/HeatmapTransformerEditor';
import { spatialTransformRegistryItem } from './spatial/SpatialTransformerEditor';
import { groupingToMatrixTransformRegistryItem } from './editors/GroupingToMatrixTransformerEditor';
export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> => {
return [
@ -48,5 +49,6 @@ export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> =
fieldLookupTransformRegistryItem,
extractFieldsTransformRegistryItem,
heatmapTransformRegistryItem,
groupingToMatrixTransformRegistryItem,
];
};