From 359ef074fabfc65ab7647f6310e8b117de6d903b Mon Sep 17 00:00:00 2001 From: Borja Garrido Date: Thu, 24 Feb 2022 18:01:04 +0100 Subject: [PATCH] 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 --- packages/grafana-data/src/index.ts | 1 + .../src/transformations/transformers.ts | 2 + .../transformers/groupingToMatrix.test.ts | 170 ++++++++++++++++++ .../transformers/groupingToMatrix.ts | 115 ++++++++++++ .../src/transformations/transformers/ids.ts | 1 + .../GroupingToMatrixTransformerEditor.tsx | 85 +++++++++ .../transformers/standardTransformers.ts | 2 + 7 files changed, 376 insertions(+) create mode 100644 packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts create mode 100644 packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts create mode 100644 public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 46477608f9b..2cefae622b7 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -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'; diff --git a/packages/grafana-data/src/transformations/transformers.ts b/packages/grafana-data/src/transformations/transformers.ts index 835be0d0228..063e9f8ddf4 100644 --- a/packages/grafana-data/src/transformations/transformers.ts +++ b/packages/grafana-data/src/transformations/transformers.ts @@ -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, }; diff --git a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts new file mode 100644 index 00000000000..103cf335a67 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts @@ -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 = { + 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 = { + 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 = { + 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, + "", + ], + }, + ] + `); + }); + }); +}); diff --git a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts new file mode 100644 index 00000000000..4d8a8729ff7 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts @@ -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 = { + 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; +} diff --git a/packages/grafana-data/src/transformations/transformers/ids.ts b/packages/grafana-data/src/transformations/transformers/ids.ts index e8254b1949a..1032e9c7d68 100644 --- a/packages/grafana-data/src/transformations/transformers/ids.ts +++ b/packages/grafana-data/src/transformations/transformers/ids.ts @@ -31,4 +31,5 @@ export enum DataTransformerID { heatmap = 'heatmap', spatial = 'spatial', extractFields = 'extractFields', + groupingToMatrix = 'groupingToMatrix', } diff --git a/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx b/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx new file mode 100644 index 00000000000..32cc311f61b --- /dev/null +++ b/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx @@ -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> = ({ + input, + options, + onChange, +}) => { + const fieldNames = useAllFieldNamesFromDataFrames(input).map((item: string) => ({ label: item, value: item })); + + const onSelectColumn = useCallback( + (value: SelectableValue) => { + onChange({ + ...options, + columnField: value.value, + }); + }, + [onChange, options] + ); + + const onSelectRow = useCallback( + (value: SelectableValue) => { + onChange({ + ...options, + rowField: value.value, + }); + }, + [onChange, options] + ); + + const onSelectValue = useCallback( + (value: SelectableValue) => { + onChange({ + ...options, + valueField: value.value, + }); + }, + [onChange, options] + ); + + return ( + <> + + + + + +