mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
91af956eb7
commit
359ef074fa
@ -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';
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
"",
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
@ -31,4 +31,5 @@ export enum DataTransformerID {
|
||||
heatmap = 'heatmap',
|
||||
spatial = 'spatial',
|
||||
extractFields = 'extractFields',
|
||||
groupingToMatrix = 'groupingToMatrix',
|
||||
}
|
||||
|
@ -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`,
|
||||
};
|
@ -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,
|
||||
];
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user