Transformations: Add unary operations to Add field from calculation (#75946)

* Add unary operations to add field from calc transform

* Refactor layout to use new UI components. fix 'as' type assertions

* rename exp

* more docs

* fix docs
This commit is contained in:
Victor Marin 2023-10-06 17:44:53 +03:00 committed by GitHub
parent 9067ca7d29
commit 8e576911b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 330 additions and 117 deletions

View File

@ -5263,8 +5263,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/transformers/editors/CalculateFieldTransformerEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]

View File

@ -108,11 +108,17 @@ Use this transformation to add a new field calculated from two other fields. Eac
- **Mode -** Select a mode:
- **Reduce row -** Apply selected calculation on each row of selected fields independently.
- **Binary option -** Apply basic math operation(sum, multiply, etc) on values in a single row from two selected fields.
- **Binary operation -** Apply basic binary operations (for example, sum or multiply) on values in a single row from two selected fields.
- **Unary operation -** Apply basic unary operations on values in a single row from a selected field. The available operations are:
- **Absolute value (abs)** - Returns the absolute value of a given expression. It represents its distance from zero as a positive number.
- **Natural exponential (exp)** - Returns _e_ raised to the power of a given expression.
- **Natural logarithm (ln)** - Returns the natural logarithm of a given expression.
- **Floor (floor)** - Returns the largest integer less than or equal to a given expression.
- **Ceiling (ceil)** - Returns the smallest integer greater than or equal to a given expression.
- **Row index -** Insert a field with the row index.
- **Field name -** Select the names of fields you want to use in the calculation for the new field.
- **Calculation -** If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][].
- **Operation -** If you select **Binary option** mode, then the **Operation** fields appear. These fields allow you to do basic math operations on values in a single row from two selected fields. You can also use numerical values for binary operations.
- **Operation -** If you select **Binary operation** or **Unary operation** mode, then the **Operation** fields appear. These fields allow you to apply basic math operations on values in a single row from selected fields. You can also use numerical values for binary operations.
- **As percentile -** If you select **Row index** mode, then the **As percentile** switch appears. This switch allows you to transform the row index as a percentage of the total number of rows.
- **Alias -** (Optional) Enter the name of your new field. If you leave this blank, then the field will be named to match the calculation.
- **Replace all fields -** (Optional) Select this option if you want to hide all other fields and display only your calculated field in the visualization.

View File

@ -2,7 +2,7 @@ import { DataFrameView } from '../../dataframe';
import { toDataFrame } from '../../dataframe/processDataFrame';
import { DataTransformContext, ScopedVars } from '../../types';
import { FieldType } from '../../types/dataFrame';
import { BinaryOperationID } from '../../utils';
import { BinaryOperationID, UnaryOperationID } from '../../utils';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { ReducerID } from '../fieldReducer';
import { transformDataFrame } from '../transformDataFrame';
@ -189,6 +189,51 @@ describe('calculateField transformer w/ timeseries', () => {
});
});
it('unary math', async () => {
const unarySeries = toDataFrame({
fields: [
{ name: 'TheTime', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'A', type: FieldType.number, values: [1, -10, -200, 300] },
],
});
const cfg = {
id: DataTransformerID.calculateField,
options: {
mode: CalculateFieldMode.UnaryOperation,
unary: {
fieldName: 'A',
operator: UnaryOperationID.Abs,
},
replaceFields: true,
},
};
await expect(transformDataFrame([cfg], [unarySeries])).toEmitValuesWith((received) => {
const data = received[0];
const filtered = data[0];
const rows = new DataFrameView(filtered).toArray();
expect(rows).toEqual([
{
'abs(A)': 1,
TheTime: 1000,
},
{
'abs(A)': 10,
TheTime: 2000,
},
{
'abs(A)': 200,
TheTime: 3000,
},
{
'abs(A)': 300,
TheTime: 4000,
},
]);
});
});
it('boolean field', async () => {
const cfg = {
id: DataTransformerID.calculateField,

View File

@ -5,6 +5,7 @@ import { getTimeField } from '../../dataframe/processDataFrame';
import { getFieldDisplayName } from '../../field';
import { DataFrame, DataTransformerInfo, Field, FieldType, NullValueMode } from '../../types';
import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators';
import { UnaryOperationID, unaryOperators } from '../../utils/unaryOperators';
import { doStandardCalcs, fieldReducers, ReducerID } from '../fieldReducer';
import { getFieldMatcher } from '../matchers';
import { FieldMatcherID } from '../matchers/ids';
@ -16,6 +17,7 @@ import { noopTransformer } from './noop';
export enum CalculateFieldMode {
ReduceRow = 'reduceRow',
BinaryOperation = 'binary',
UnaryOperation = 'unary',
Index = 'index',
}
@ -25,6 +27,11 @@ export interface ReduceOptions {
nullValueMode?: NullValueMode;
}
export interface UnaryOptions {
operator: UnaryOperationID;
fieldName: string;
}
export interface BinaryOptions {
left: string;
operator: BinaryOperationID;
@ -45,6 +52,11 @@ const defaultBinaryOptions: BinaryOptions = {
right: '',
};
const defaultUnaryOptions: UnaryOptions = {
operator: UnaryOperationID.Abs,
fieldName: '',
};
export interface CalculateFieldTransformerOptions {
// True/False or auto
timeSeries?: boolean;
@ -53,6 +65,7 @@ export interface CalculateFieldTransformerOptions {
// Only one should be filled
reduce?: ReduceOptions;
binary?: BinaryOptions;
unary?: UnaryOptions;
index?: IndexOptions;
// Remove other fields
@ -93,6 +106,8 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
if (mode === CalculateFieldMode.ReduceRow) {
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions), data);
} else if (mode === CalculateFieldMode.UnaryOperation) {
creator = getUnaryCreator(defaults(options.unary, defaultUnaryOptions), data);
} else if (mode === CalculateFieldMode.BinaryOperation) {
const binaryOptions = {
...options.binary,
@ -263,12 +278,41 @@ function getBinaryCreator(options: BinaryOptions, allFrames: DataFrame[]): Value
};
}
function getUnaryCreator(options: UnaryOptions, allFrames: DataFrame[]): ValuesCreator {
const operator = unaryOperators.getIfExists(options.operator);
return (frame: DataFrame) => {
let value: number[] = [];
for (const f of frame.fields) {
if (options.fieldName === getFieldDisplayName(f, frame, allFrames) && f.type === FieldType.number) {
value = f.values;
}
}
if (!value.length || !operator) {
return undefined;
}
const arr = new Array(value.length);
for (let i = 0; i < arr.length; i++) {
arr[i] = operator.operation(value[i]);
}
return arr;
};
}
export function getNameFromOptions(options: CalculateFieldTransformerOptions) {
if (options.alias?.length) {
return options.alias;
}
switch (options.mode) {
case CalculateFieldMode.UnaryOperation: {
const { unary } = options;
return `${unary?.operator ?? ''}${unary?.fieldName ? `(${unary.fieldName})` : ''}`;
}
case CalculateFieldMode.BinaryOperation: {
const { binary } = options;
return `${binary?.left ?? ''} ${binary?.operator ?? ''} ${binary?.right ?? ''}`;

View File

@ -11,6 +11,7 @@ export type BinaryOperation = (left: number, right: number) => number;
interface BinaryOperatorInfo extends RegistryItem {
operation: BinaryOperation;
binaryOperationID: BinaryOperationID;
}
export const binaryOperators = new Registry<BinaryOperatorInfo>(() => {
@ -19,21 +20,25 @@ export const binaryOperators = new Registry<BinaryOperatorInfo>(() => {
id: BinaryOperationID.Add,
name: 'Add',
operation: (a: number, b: number) => a + b,
binaryOperationID: BinaryOperationID.Add,
},
{
id: BinaryOperationID.Subtract,
name: 'Subtract',
operation: (a: number, b: number) => a - b,
binaryOperationID: BinaryOperationID.Subtract,
},
{
id: BinaryOperationID.Multiply,
name: 'Multiply',
operation: (a: number, b: number) => a * b,
binaryOperationID: BinaryOperationID.Multiply,
},
{
id: BinaryOperationID.Divide,
name: 'Divide',
operation: (a: number, b: number) => a / b,
binaryOperationID: BinaryOperationID.Divide,
},
];
});

View File

@ -10,6 +10,7 @@ export * from './object';
export * from './namedColorsPalette';
export * from './series';
export * from './binaryOperators';
export * from './unaryOperators';
export * from './nodeGraph';
export * from './selectUtils';
export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUIBuilders';

View File

@ -0,0 +1,51 @@
import { Registry, RegistryItem } from './Registry';
export enum UnaryOperationID {
Abs = 'abs',
Exp = 'exp',
Ln = 'ln',
Floor = 'floor',
Ceil = 'ceil',
}
export type UnaryOperation = (value: number) => number;
interface UnaryOperatorInfo extends RegistryItem {
operation: UnaryOperation;
unaryOperationID: UnaryOperationID;
}
export const unaryOperators = new Registry<UnaryOperatorInfo>(() => {
return [
{
id: UnaryOperationID.Abs,
name: 'Absolute value',
operation: (value: number) => Math.abs(value),
unaryOperationID: UnaryOperationID.Abs,
},
{
id: UnaryOperationID.Exp,
name: 'Natural exponent',
operation: (value: number) => Math.exp(value),
unaryOperationID: UnaryOperationID.Exp,
},
{
id: UnaryOperationID.Ln,
name: 'Natural logarithm',
operation: (value: number) => Math.log(value),
unaryOperationID: UnaryOperationID.Ln,
},
{
id: UnaryOperationID.Floor,
name: 'Floor',
operation: (value: number) => Math.floor(value),
unaryOperationID: UnaryOperationID.Floor,
},
{
id: UnaryOperationID.Ceil,
name: 'Ceiling',
operation: (value: number) => Math.ceil(value),
unaryOperationID: UnaryOperationID.Ceil,
},
];
});

View File

@ -6,6 +6,7 @@ import { map } from 'rxjs/operators';
import {
BinaryOperationID,
binaryOperators,
unaryOperators,
DataFrame,
DataTransformerID,
FieldType,
@ -17,9 +18,11 @@ import {
TransformerRegistryItem,
TransformerUIProps,
TransformerCategory,
UnaryOperationID,
} from '@grafana/data';
import {
BinaryOptions,
UnaryOptions,
CalculateFieldMode,
CalculateFieldTransformerOptions,
getNameFromOptions,
@ -27,7 +30,17 @@ import {
ReduceOptions,
} from '@grafana/data/src/transformations/transformers/calculateField';
import { getTemplateSrv, config as cfg } from '@grafana/runtime';
import { FilterPill, HorizontalGroup, Input, LegacyForms, Select, StatsPicker } from '@grafana/ui';
import {
FilterPill,
HorizontalGroup,
InlineField,
InlineFieldRow,
InlineLabel,
InlineSwitch,
Input,
Select,
StatsPicker,
} from '@grafana/ui';
interface CalculateFieldTransformerEditorProps extends TransformerUIProps<CalculateFieldTransformerOptions> {}
@ -39,12 +52,15 @@ interface CalculateFieldTransformerEditorState {
const calculationModes = [
{ value: CalculateFieldMode.BinaryOperation, label: 'Binary operation' },
{ value: CalculateFieldMode.UnaryOperation, label: 'Unary operation' },
{ value: CalculateFieldMode.ReduceRow, label: 'Reduce row' },
{ value: CalculateFieldMode.Index, label: 'Row index' },
];
const okTypes = new Set<FieldType>([FieldType.time, FieldType.number, FieldType.string]);
const labelWidth = 16;
export class CalculateFieldTransformerEditor extends React.PureComponent<
CalculateFieldTransformerEditorProps,
CalculateFieldTransformerEditorState
@ -151,20 +167,20 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
);
}
onToggleReplaceFields = () => {
onToggleReplaceFields = (e: React.FormEvent<HTMLInputElement>) => {
const { options } = this.props;
this.props.onChange({
...options,
replaceFields: !options.replaceFields,
replaceFields: e.currentTarget.checked,
});
};
onToggleRowIndexAsPercentile = () => {
onToggleRowIndexAsPercentile = (e: React.FormEvent<HTMLInputElement>) => {
const { options } = this.props;
this.props.onChange({
...options,
index: {
asPercentile: !options.index?.asPercentile ?? false,
asPercentile: e.currentTarget.checked,
},
});
};
@ -227,15 +243,9 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
renderRowIndex(options?: IndexOptions) {
return (
<>
<div className="gf-form-inline">
<LegacyForms.Switch
label="As percentile"
tooltip="Transform the row index as a percentile."
labelClass="width-8"
checked={!!options?.asPercentile}
onChange={this.onToggleRowIndexAsPercentile}
/>
</div>
<InlineField labelWidth={labelWidth} label="As percentile" tooltip="Transform the row index as a percentile.">
<InlineSwitch value={!!options?.asPercentile} onChange={this.onToggleRowIndexAsPercentile} />
</InlineField>
</>
);
}
@ -246,37 +256,31 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
return (
<>
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Field name</div>
<HorizontalGroup spacing="xs" align="flex-start" wrap>
{names.map((o, i) => {
return (
<FilterPill
key={`${o}/${i}`}
onClick={() => {
this.onFieldToggle(o);
}}
label={o}
selected={selected.indexOf(o) > -1}
/>
);
})}
</HorizontalGroup>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-label width-8">Calculation</div>
<StatsPicker
allowMultiple={false}
className="width-18"
stats={[options.reducer]}
onChange={this.onStatsChange}
defaultStat={ReducerID.sum}
/>
</div>
</div>
<InlineField label="Operation" labelWidth={labelWidth} grow={true}>
<HorizontalGroup spacing="xs" align="flex-start" wrap>
{names.map((o, i) => {
return (
<FilterPill
key={`${o}/${i}`}
onClick={() => {
this.onFieldToggle(o);
}}
label={o}
selected={selected.indexOf(o) > -1}
/>
);
})}
</HorizontalGroup>
</InlineField>
<InlineField label="Calculation" labelWidth={labelWidth}>
<StatsPicker
allowMultiple={false}
className="width-18"
stats={[options.reducer]}
onChange={this.onStatsChange}
defaultStat={ReducerID.sum}
/>
</InlineField>
</>
);
}
@ -310,16 +314,16 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
});
};
onBinaryOperationChanged = (v: SelectableValue<string>) => {
onBinaryOperationChanged = (v: SelectableValue<BinaryOperationID>) => {
const { binary } = this.props.options;
this.updateBinaryOptions({
...binary!,
operator: v.value! as BinaryOperationID,
operator: v.value!,
});
};
renderBinaryOperation(options?: BinaryOptions) {
options = defaults(options, { reducer: ReducerID.sum });
options = defaults(options, { operator: BinaryOperationID.Add });
let foundLeft = !options?.left;
let foundRight = !options?.right;
@ -336,39 +340,109 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
const rightNames = foundRight ? names : [...names, { label: options?.right, value: options?.right }];
const ops = binaryOperators.list().map((v) => {
return { label: v.id, value: v.id };
return { label: v.binaryOperationID, value: v.binaryOperationID };
});
return (
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-label width-8">Operation</div>
</div>
<div className="gf-form">
<Select
allowCustomValue={true}
placeholder="Field or number"
options={leftNames}
className="min-width-18 gf-form-spacing"
value={options?.left}
onChange={this.onBinaryLeftChanged}
/>
<Select
className="width-8 gf-form-spacing"
options={ops}
value={options.operator ?? ops[0].value}
onChange={this.onBinaryOperationChanged}
/>
<Select
allowCustomValue={true}
placeholder="Field or number"
className="min-width-10"
options={rightNames}
value={options?.right}
onChange={this.onBinaryRightChanged}
/>
</div>
</div>
<>
<InlineFieldRow>
<InlineField label="Operation" labelWidth={labelWidth}>
<Select
allowCustomValue={true}
placeholder="Field or number"
options={leftNames}
className="min-width-18"
value={options?.left}
onChange={this.onBinaryLeftChanged}
/>
</InlineField>
<InlineField>
<Select
className="width-4"
options={ops}
value={options.operator ?? ops[0].value}
onChange={this.onBinaryOperationChanged}
/>
</InlineField>
<InlineField>
<Select
allowCustomValue={true}
placeholder="Field or number"
className="min-width-10"
options={rightNames}
value={options?.right}
onChange={this.onBinaryRightChanged}
/>
</InlineField>
</InlineFieldRow>
</>
);
}
//---------------------------------------------------------
// Unary Operator
//---------------------------------------------------------
updateUnaryOptions = (v: UnaryOptions) => {
const { options, onChange } = this.props;
onChange({
...options,
mode: CalculateFieldMode.UnaryOperation,
unary: v,
});
};
onUnaryOperationChanged = (v: SelectableValue<UnaryOperationID>) => {
const { unary } = this.props.options;
this.updateUnaryOptions({
...unary!,
operator: v.value!,
});
};
onUnaryValueChanged = (v: SelectableValue<string>) => {
const { unary } = this.props.options;
this.updateUnaryOptions({
...unary!,
fieldName: v.value!,
});
};
renderUnaryOperation(options?: UnaryOptions) {
options = defaults(options, { operator: UnaryOperationID.Abs });
let found = !options?.fieldName;
const names = this.state.names.map((v) => {
if (v === options?.fieldName) {
found = true;
}
return { label: v, value: v };
});
const ops = unaryOperators.list().map((v) => {
return { label: v.unaryOperationID, value: v.unaryOperationID };
});
const fieldName = found ? names : [...names, { label: options?.fieldName, value: options?.fieldName }];
return (
<>
<InlineFieldRow>
<InlineField label="Operation" labelWidth={labelWidth}>
<Select options={ops} value={options.operator ?? ops[0].value} onChange={this.onUnaryOperationChanged} />
</InlineField>
<InlineField label="(" labelWidth={2}>
<Select
placeholder="Field"
className="min-width-11"
options={fieldName}
value={options?.fieldName}
onChange={this.onUnaryValueChanged}
/>
</InlineField>
<InlineLabel width={2}>)</InlineLabel>
</InlineFieldRow>
</>
);
}
@ -382,43 +456,31 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
const mode = options.mode ?? CalculateFieldMode.BinaryOperation;
return (
<div>
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-label width-8">Mode</div>
<Select
className="width-18"
options={calculationModes}
value={calculationModes.find((v) => v.value === mode)}
onChange={this.onModeChanged}
/>
</div>
</div>
<>
<InlineField labelWidth={labelWidth} label="Mode">
<Select
className="width-18"
options={calculationModes}
value={calculationModes.find((v) => v.value === mode)}
onChange={this.onModeChanged}
/>
</InlineField>
{mode === CalculateFieldMode.BinaryOperation && this.renderBinaryOperation(options.binary)}
{mode === CalculateFieldMode.UnaryOperation && this.renderUnaryOperation(options.unary)}
{mode === CalculateFieldMode.ReduceRow && this.renderReduceRow(options.reduce)}
{mode === CalculateFieldMode.Index && this.renderRowIndex(options.index)}
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-label width-8">Alias</div>
<Input
className="width-18"
value={options.alias ?? ''}
placeholder={getNameFromOptions(options)}
onChange={this.onAliasChanged}
/>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<LegacyForms.Switch
label="Replace all fields"
labelClass="width-8"
checked={!!options.replaceFields}
onChange={this.onToggleReplaceFields}
/>
</div>
</div>
</div>
<InlineField labelWidth={labelWidth} label="Alias">
<Input
className="width-18"
value={options.alias ?? ''}
placeholder={getNameFromOptions(options)}
onChange={this.onAliasChanged}
/>
</InlineField>
<InlineField labelWidth={labelWidth} label="Replace all fields">
<InlineSwitch value={!!options.replaceFields} onChange={this.onToggleReplaceFields} />
</InlineField>
</>
);
}
}