Transformations: Add empty values options to Transpose (#108421)

* Extract functions to util and utilize in both transforms

* Fix mistaken label and add better null logic to transpose

* Add a new row and add a blurb to docs about the new setting

* Update public/app/features/transformers/docs/content.ts

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* Update docs content, simplify null logic

* Add test, clarify empty logic

---------

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>
This commit is contained in:
Kristina
2025-08-04 07:53:26 -05:00
committed by GitHub
co-authored by Isabel Matwawana
parent 50ead8d463
commit 0dfcaf56d3
10 changed files with 139 additions and 87 deletions
@@ -1465,7 +1465,7 @@ For each generated **Trend** field value, a calculation function can be selected
### Transpose
Use this transformation to pivot the data frame, converting rows into columns and columns into rows. This transformation is particularly useful when you want to switch the orientation of your data to better suit your visualization needs.
If you have multiple types it will default to string type.
If you have multiple types, it will default to string type. You can select how empty cells should be represented.
**Before Transformation:**
@@ -12,6 +12,7 @@ import { fieldMatchers } from '../matchers';
import { FieldMatcherID } from '../matchers/ids';
import { DataTransformerID } from './ids';
import { getSpecialValue } from './utils';
export interface GroupingToMatrixTransformerOptions {
columnField?: string;
@@ -172,19 +173,3 @@ function findKeyField(frame: DataFrame, matchTitle: string): Field | null {
return null;
}
function getSpecialValue(specialValue: SpecialValue) {
switch (specialValue) {
case SpecialValue.False:
return false;
case SpecialValue.True:
return true;
case SpecialValue.Null:
return null;
case SpecialValue.Zero:
return 0;
case SpecialValue.Empty:
default:
return '';
}
}
@@ -2,6 +2,7 @@ import { DataTransformerConfig } from '@grafana/schema';
import { toDataFrame } from '../../dataframe/processDataFrame';
import { FieldType } from '../../types/dataFrame';
import { SpecialValue } from '../../types/transformations';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { transformDataFrame } from '../transformDataFrame';
@@ -246,4 +247,47 @@ describe('Transpose transformer', () => {
]);
});
});
it('should fill in empty values with the indicated option', async () => {
const cfgC: DataTransformerConfig<TransposeTransformerOptions> = {
id: DataTransformerID.transpose,
options: {
emptyValue: SpecialValue.Zero,
},
};
const seriesC = toDataFrame({
name: 'C',
fields: [
{ name: 'A', type: FieldType.string, values: ['apple', undefined] },
{ name: 'B', type: FieldType.string, values: [undefined, 'orange'] },
{ name: 'C', type: FieldType.string, values: ['banana', undefined] },
{ name: 'D', type: FieldType.string, values: ['strawberry', 'pear'] },
],
});
await expect(transformDataFrame([cfgC], [seriesC])).toEmitValuesWith((received) => {
const result = received[0];
expect(result[0].fields).toEqual([
{
name: 'Field',
type: FieldType.string,
values: ['B', 'C', 'D'],
config: {},
},
{
name: 'Value',
labels: { A: 'apple' },
type: FieldType.string,
values: [0, 'banana', 'strawberry'],
config: {},
},
{
name: 'Value',
labels: { A: 0 },
type: FieldType.string,
values: ['orange', 0, 'pear'],
config: {},
},
]);
});
});
});
@@ -2,13 +2,15 @@ import { map } from 'rxjs/operators';
import { cacheFieldDisplayNames } from '../../field/fieldState';
import { DataFrame, Field, FieldType } from '../../types/dataFrame';
import { DataTransformerInfo } from '../../types/transformations';
import { DataTransformerInfo, SpecialValue } from '../../types/transformations';
import { DataTransformerID } from './ids';
import { getSpecialValue } from './utils';
export interface TransposeTransformerOptions {
firstFieldName?: string;
restFieldsName?: string;
emptyValue?: SpecialValue;
}
export const transposeTransformer: DataTransformerInfo<TransposeTransformerOptions> = {
@@ -30,6 +32,7 @@ export const transposeTransformer: DataTransformerInfo<TransposeTransformerOptio
function transposeDataFrame(options: TransposeTransformerOptions, data: DataFrame[]): DataFrame[] {
cacheFieldDisplayNames(data);
const emptyValue = options.emptyValue ?? SpecialValue.Empty;
return data.map((frame) => {
const firstField = frame.fields[0];
@@ -38,7 +41,7 @@ function transposeDataFrame(options: TransposeTransformerOptions, data: DataFram
const useFirstFieldAsHeaders =
firstField.type === FieldType.string || firstField.type === FieldType.time || firstField.type === FieldType.enum;
const headers = useFirstFieldAsHeaders
? [firstName, ...fieldValuesAsStrings(firstField, firstField.values)]
? [firstName, ...fieldValuesAsStrings(firstField, firstField.values, emptyValue)]
: [firstName, ...firstField.values.map((_, i) => restName)];
const rows = useFirstFieldAsHeaders
? frame.fields
@@ -65,7 +68,7 @@ function transposeDataFrame(options: TransposeTransformerOptions, data: DataFram
const values = frame.fields.map((field) => {
if (fieldType === FieldType.string) {
return fieldValuesAsStrings(field, [field.values[index - 1]])[0];
return fieldValuesAsStrings(field, [field.values[index - 1]], emptyValue)[0];
}
return field.values[index - 1];
});
@@ -97,17 +100,17 @@ function determineFieldType(fieldTypes: FieldType[]): FieldType {
return uniqueFieldTypes.size === 1 ? [...uniqueFieldTypes][0] : FieldType.string;
}
function fieldValuesAsStrings(field: Field, values: unknown[]) {
function fieldValuesAsStrings(field: Field, values: unknown[], emptyValue: SpecialValue) {
switch (field.type) {
case FieldType.time:
case FieldType.number:
case FieldType.boolean:
case FieldType.string:
return values.map((v) => `${v}`);
return values.map((v) => (v != null ? `${v}` : getSpecialValue(emptyValue)));
case FieldType.enum:
// @ts-ignore
return values.map((v) => field.config.type!.enum!.text![v]);
return values.map((v) => field.config.type!.enum!.text![v] ?? getSpecialValue(emptyValue));
default:
return values.map((v) => JSON.stringify(v));
return values.map((v) => (v != null ? JSON.stringify(v) : getSpecialValue(emptyValue)));
}
}
@@ -1,5 +1,6 @@
import { BootData } from '../../types/config';
import { DataFrame } from '../../types/dataFrame';
import { SpecialValue } from '../../types/transformations';
declare global {
interface Window {
@@ -22,3 +23,19 @@ export function findMaxFields(data: DataFrame[]) {
return maxFields;
}
export function getSpecialValue(specialValue: SpecialValue) {
switch (specialValue) {
case SpecialValue.False:
return false;
case SpecialValue.True:
return true;
case SpecialValue.Null:
return null;
case SpecialValue.Zero:
return 0;
case SpecialValue.Empty:
default:
return '';
}
}
@@ -1559,7 +1559,7 @@ ${buildImageContent(
getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) {
return `
Use this transformation to pivot the data frame, converting rows into columns and columns into rows. This transformation is particularly useful when you want to switch the orientation of your data to better suit your visualization needs.
If you have multiple types it will default to string type.
If you have multiple types, it will default to string type. You can select how empty cells should be represented.
**Before Transformation:**
@@ -17,7 +17,7 @@ import { InlineField, InlineFieldRow, Select } from '@grafana/ui';
import { getTransformationContent } from '../docs/getTransformationContent';
import darkImage from '../images/dark/groupingToMatrix.svg';
import lightImage from '../images/light/groupingToMatrix.svg';
import { useAllFieldNamesFromDataFrames } from '../utils';
import { getEmptyOptions, useAllFieldNamesFromDataFrames } from '../utils';
export const GroupingToMatrixTransformerEditor = ({
input,
@@ -61,49 +61,6 @@ export const GroupingToMatrixTransformerEditor = ({
[onChange, options]
);
const specialValueOptions: Array<SelectableValue<SpecialValue>> = [
{
label: t('transformers.grouping-to-matrix-transformer-editor.special-value-options.label.null', 'Null'),
value: SpecialValue.Null,
description: t(
'transformers.grouping-to-matrix-transformer-editor.special-value-options.description.null-value',
'Null value'
),
},
{
label: t('transformers.grouping-to-matrix-transformer-editor.special-value-options.label.true', 'True'),
value: SpecialValue.True,
description: t(
'transformers.grouping-to-matrix-transformer-editor.special-value-options.description.boolean-true-value',
'Boolean true value'
),
},
{
label: t('transformers.grouping-to-matrix-transformer-editor.special-value-options.label.false', 'False'),
value: SpecialValue.False,
description: t(
'transformers.grouping-to-matrix-transformer-editor.special-value-options.description.boolean-false-value',
'Boolean false value'
),
},
{
label: t('transformers.grouping-to-matrix-transformer-editor.special-value-options.label.zero', 'Zero'),
value: SpecialValue.Zero,
description: t(
'transformers.grouping-to-matrix-transformer-editor.special-value-options.description.number-value',
'Number 0 value'
),
},
{
label: t('transformers.grouping-to-matrix-transformer-editor.special-value-options.label.empty', 'Empty'),
value: SpecialValue.Empty,
description: t(
'transformers.grouping-to-matrix-transformer-editor.special-value-options.description.empty-string',
'Empty string'
),
},
];
const onSelectEmptyValue = useCallback(
(value: SelectableValue<SpecialValue>) => {
onChange({
@@ -143,7 +100,7 @@ export const GroupingToMatrixTransformerEditor = ({
/>
</InlineField>
<InlineField label={t('transformers.grouping-to-matrix-transformer-editor.label-empty-value', 'Empty value')}>
<Select options={specialValueOptions} value={options.emptyValue} onChange={onSelectEmptyValue} isClearable />
<Select options={getEmptyOptions()} value={options.emptyValue} onChange={onSelectEmptyValue} isClearable />
</InlineField>
</InlineFieldRow>
</>
@@ -4,15 +4,25 @@ import {
TransformerRegistryItem,
TransformerUIProps,
TransformerCategory,
SpecialValue,
SelectableValue,
} from '@grafana/data';
import { TransposeTransformerOptions } from '@grafana/data/internal';
import { t } from '@grafana/i18n';
import { InlineField, InlineFieldRow, Input } from '@grafana/ui';
import { InlineField, InlineFieldRow, Input, Select } from '@grafana/ui';
import darkImage from '../images/dark/transpose.svg';
import lightImage from '../images/light/transpose.svg';
import { getEmptyOptions } from '../utils';
export const TransposeTransformerEditor = ({ options, onChange }: TransformerUIProps<TransposeTransformerOptions>) => {
const onSelectEmptyValue = (value?: SelectableValue<SpecialValue>) => {
onChange({
...options,
emptyValue: value?.value,
});
};
return (
<>
<InlineFieldRow>
@@ -42,6 +52,11 @@ export const TransposeTransformerEditor = ({ options, onChange }: TransformerUIP
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label={t('transformers.grouping-to-matrix-transformer-editor.label-empty-value', 'Empty value')}>
<Select options={getEmptyOptions()} value={options.emptyValue} onChange={onSelectEmptyValue} isClearable />
</InlineField>
</InlineFieldRow>
</>
);
};
+31
View File
@@ -8,6 +8,7 @@ import {
getTimeZones,
VariableOrigin,
VariableSuggestion,
SpecialValue,
} from '@grafana/data';
import { t } from '@grafana/i18n';
import { getTemplateSrv } from '@grafana/runtime';
@@ -118,3 +119,33 @@ export function getVariableSuggestions(): VariableSuggestion[] {
.getVariables()
.map((v) => ({ value: v.name, label: v.label || v.name, origin: VariableOrigin.Template }));
}
export function getEmptyOptions(): Array<SelectableValue<SpecialValue>> {
return [
{
label: t('transformers.utils.special-value-options.label.null-value', 'Null'),
description: t('transformers.utils.special-value-options.description.null-value', 'Null value'),
value: SpecialValue.Null,
},
{
label: t('transformers.utils.special-value-options.label.boolean-true', 'True'),
description: t('transformers.utils.special-value-options.description.boolean-true', 'Boolean true value'),
value: SpecialValue.True,
},
{
label: t('transformers.utils.special-value-options.label.boolean-false', 'False'),
description: t('transformers.utils.special-value-options.description.boolean-false', 'Boolean false value'),
value: SpecialValue.False,
},
{
label: t('transformers.utils.special-value-options.label.number-value', 'Zero'),
description: t('transformers.utils.special-value-options.description.number-value', 'Number 0 value'),
value: SpecialValue.Zero,
},
{
label: t('transformers.utils.special-value-options.label.empty-string', 'Empty'),
description: t('transformers.utils.special-value-options.description.empty-string', 'Empty String'),
value: SpecialValue.Empty,
},
];
}
+16 -16
View File
@@ -13308,22 +13308,6 @@
"label-row": "Row",
"name": {
"grouping-to-matrix": "Grouping to matrix"
},
"special-value-options": {
"description": {
"boolean-false-value": "Boolean false value",
"boolean-true-value": "Boolean true value",
"empty-string": "Empty string",
"null-value": "Null value",
"number-value": "Number 0 value"
},
"label": {
"empty": "Empty",
"false": "False",
"null": "Null",
"true": "True",
"zero": "Zero"
}
}
},
"histogram-transformer-editor": {
@@ -13676,6 +13660,22 @@
"perform-spatial-operations": "Perform spatial operations",
"reformat": "Reformat",
"reorder-and-rename": "Reorder and rename"
},
"special-value-options": {
"description": {
"boolean-false": "Boolean false value",
"boolean-true": "Boolean true value",
"empty-string": "Empty String",
"null-value": "Null value",
"number-value": "Number 0 value"
},
"label": {
"boolean-false": "False",
"boolean-true": "True",
"empty-string": "Empty",
"null-value": "Null",
"number-value": "Zero"
}
}
},
"wide-info": {