Transformers: allow label fields in the reduce processor (#37373)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
An 2021-07-29 16:56:07 -04:00 committed by GitHub
parent c4c28a5b63
commit c564736c68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 171 additions and 35 deletions

View File

@ -2,9 +2,9 @@ import { ReducerID } from '../fieldReducer';
import { DataTransformerID } from './ids'; import { DataTransformerID } from './ids';
import { toDataFrame } from '../../dataframe/processDataFrame'; import { toDataFrame } from '../../dataframe/processDataFrame';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { reduceFields, reduceTransformer } from './reduce'; import { reduceFields, reduceTransformer, ReduceTransformerOptions } from './reduce';
import { transformDataFrame } from '../transformDataFrame'; import { transformDataFrame } from '../transformDataFrame';
import { Field, FieldType } from '../../types'; import { DataTransformerConfig, Field, FieldType } from '../../types';
import { ArrayVector } from '../../vector'; import { ArrayVector } from '../../vector';
import { notTimeFieldMatcher } from '../matchers/predicates'; import { notTimeFieldMatcher } from '../matchers/predicates';
import { DataFrameView } from '../../dataframe'; import { DataFrameView } from '../../dataframe';
@ -366,4 +366,84 @@ describe('Reducer Transformer', () => {
expect(processed[0].fields).toEqual(expected); expect(processed[0].fields).toEqual(expected);
}); });
}); });
it('reduces keeping label field', async () => {
const cfg: DataTransformerConfig<ReduceTransformerOptions> = {
id: DataTransformerID.reduce,
options: {
reducers: [ReducerID.max],
labelsToFields: true,
},
};
const seriesA = toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
{ name: 'value', labels: { state: 'CA' }, type: FieldType.number, values: [3, 4, 5, 6] },
{ name: 'value', labels: { state: 'NY' }, type: FieldType.number, values: [3, 4, 5, 6] },
],
});
const seriesB = toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
{ name: 'value', labels: { state: 'CA', country: 'USA' }, type: FieldType.number, values: [3, 4, 5, 6] },
{ name: 'value', labels: { country: 'USA' }, type: FieldType.number, values: [3, 4, 5, 6] },
],
});
await expect(transformDataFrame([cfg], [seriesA, seriesB])).toEmitValuesWith((received) => {
const processed = received[0];
expect(processed.length).toEqual(1);
expect(processed[0].fields).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {},
"name": "Field",
"type": "string",
"values": Array [
"value",
"value",
"value",
"value",
],
},
Object {
"config": Object {},
"name": "state",
"type": "string",
"values": Array [
"CA",
"NY",
"CA",
undefined,
],
},
Object {
"config": Object {},
"name": "country",
"type": "string",
"values": Array [
undefined,
undefined,
"USA",
"USA",
],
},
Object {
"config": Object {},
"name": "Max",
"type": "number",
"values": Array [
6,
6,
6,
6,
],
},
]
`);
});
});
}); });

View File

@ -20,6 +20,7 @@ export interface ReduceTransformerOptions {
fields?: MatcherConfig; // Assume all fields fields?: MatcherConfig; // Assume all fields
mode?: ReduceTransformerMode; mode?: ReduceTransformerMode;
includeTimeField?: boolean; includeTimeField?: boolean;
labelsToFields?: boolean;
} }
export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> = { export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> = {
@ -53,7 +54,7 @@ export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> =
} }
// Add a row for each series // Add a row for each series
const res = reduceSeriesToRows(data, matcher, options.reducers); const res = reduceSeriesToRows(data, matcher, options.reducers, options.labelsToFields);
return res ? [res] : []; return res ? [res] : [];
}) })
), ),
@ -65,77 +66,113 @@ export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> =
export function reduceSeriesToRows( export function reduceSeriesToRows(
data: DataFrame[], data: DataFrame[],
matcher: FieldMatcher, matcher: FieldMatcher,
reducerId: ReducerID[] reducerId: ReducerID[],
labelsToFields?: boolean
): DataFrame | undefined { ): DataFrame | undefined {
const calculators = fieldReducers.list(reducerId); const calculators = fieldReducers.list(reducerId);
const reducers = calculators.map((c) => c.id); const reducers = calculators.map((c) => c.id);
const processed: DataFrame[] = []; const processed: DataFrame[] = [];
const distinctLabels = labelsToFields ? getDistinctLabelKeys(data) : [];
for (const series of data) { for (const series of data) {
const values: ArrayVector[] = []; const source = series.fields.filter((f) => matcher(f, series, data));
const fields: Field[] = [];
const byId: KeyValue<ArrayVector> = {};
values.push(new ArrayVector()); // The name const size = source.length;
const fields: Field[] = [];
const names = new ArrayVector<string>(new Array(size));
fields.push({ fields.push({
name: 'Field', name: 'Field',
type: FieldType.string, type: FieldType.string,
values: values[0], values: names,
config: {}, config: {},
}); });
for (const info of calculators) { const labels: KeyValue<ArrayVector> = {};
const vals = new ArrayVector(); if (labelsToFields) {
byId[info.id] = vals; for (const key of distinctLabels) {
values.push(vals); labels[key] = new ArrayVector<string>(new Array(size));
fields.push({
name: key,
type: FieldType.string,
values: labels[key],
config: {},
});
}
}
const calcs: KeyValue<ArrayVector> = {};
for (const info of calculators) {
calcs[info.id] = new ArrayVector(new Array(size));
fields.push({ fields.push({
name: info.name, name: info.name,
type: FieldType.other, // UNKNOWN until after we call the functions type: FieldType.other, // UNKNOWN until after we call the functions
values: values[values.length - 1], values: calcs[info.id],
config: {}, config: {},
}); });
} }
for (let i = 0; i < series.fields.length; i++) { for (let i = 0; i < source.length; i++) {
const field = series.fields[i]; const field = source[i];
const results = reduceField({
field,
reducers,
});
if (matcher(field, series, data)) { if (labelsToFields) {
const results = reduceField({ names.buffer[i] = field.name;
field, if (field.labels) {
reducers, for (const key of Object.keys(field.labels)) {
}); labels[key].set(i, field.labels[key]);
}
// Update the name list
const fieldName = getFieldDisplayName(field, series, data);
values[0].buffer.push(fieldName);
for (const info of calculators) {
const v = results[info.id];
byId[info.id].buffer.push(v);
} }
} else {
names.buffer[i] = getFieldDisplayName(field, series, data);
}
for (const info of calculators) {
const v = results[info.id];
calcs[info.id].buffer[i] = v;
} }
} }
// For reduced fields, we don't know the type until we see the value
for (const f of fields) { for (const f of fields) {
const t = guessFieldTypeForField(f); if (f.type === FieldType.other) {
const t = guessFieldTypeForField(f);
if (t) { if (t) {
f.type = t; f.type = t;
}
} }
} }
processed.push({ processed.push({
...series, // Same properties, different fields ...series, // Same properties, different fields
fields, fields,
length: values[0].length, length: size,
}); });
} }
return mergeResults(processed); return mergeResults(processed);
} }
export function getDistinctLabelKeys(frames: DataFrame[]): string[] {
const ordered: string[] = [];
const keys = new Set<string>();
for (const frame of frames) {
for (const field of frame.fields) {
if (field.labels) {
for (const k of Object.keys(field.labels)) {
if (!keys.has(k)) {
ordered.push(k);
keys.add(k);
}
}
}
}
}
return ordered;
}
/** /**
* @internal only exported for testing * @internal only exported for testing
*/ */

View File

@ -49,6 +49,13 @@ export const ReduceTransformerEditor: React.FC<TransformerUIProps<ReduceTransfor
}); });
}, [onChange, options]); }, [onChange, options]);
const onToggleLabels = useCallback(() => {
onChange({
...options,
labelsToFields: !options.labelsToFields,
});
}, [onChange, options]);
return ( return (
<> <>
<div> <div>
@ -95,6 +102,18 @@ export const ReduceTransformerEditor: React.FC<TransformerUIProps<ReduceTransfor
</div> </div>
</div> </div>
)} )}
{options.mode !== ReduceTransformerMode.ReduceFields && (
<div className="gf-form-inline">
<div className="gf-form">
<LegacyForms.Switch
label="Labels to fields"
labelClass="width-8"
checked={!!options.labelsToFields}
onChange={onToggleLabels}
/>
</div>
</div>
)}
</> </>
); );
}; };