Skip to content

Commit

Permalink
Transform: adding missing "table"-transform and "series to rows"-tran…
Browse files Browse the repository at this point in the history
…sform to Grafana v7-transforms. (grafana#26042)

* Fixed so the merge for table values works as it did before.

* wip

* fixed tests.

* merge tests are green.

* removed unused code and simplify the seriesToRows.

* added time series to rows editor.

* using getFrameDisplayName for the metric value.

* updated description of transforms.

* updated docs.

* fixed according to feedback.

* changed from images to markdown tables for the examples.

* forgot to save :P
  • Loading branch information
mckn committed Jul 8, 2020
1 parent fd44c01 commit 17d8707
Show file tree
Hide file tree
Showing 16 changed files with 708 additions and 359 deletions.
68 changes: 57 additions & 11 deletions docs/sources/panels/transformations.md
Expand Up @@ -11,7 +11,7 @@ weight = 300

This page explains what transformations in Grafana are and how to use them.

> **Note:** This documentation refers to a Grafana 7.0 beta feature. This documentation will be frequently updated to reflect updates to the feature, and it will probably be broken into smaller sections when the feature moves out of beta.
> **Note:** This documentation refers to a Grafana 7.0 feature. This documentation will be frequently updated to reflect updates to the feature, and it will probably be broken into smaller sections when the feature moves out of beta.
Transformations process the result set before it’s passed to the visualization. You access transformations in the Transform tab of the Grafana panel editor.

Expand Down Expand Up @@ -74,6 +74,7 @@ Grafana comes with the following transformations:
- [Join by field (outer join)](#join-by-field-outer-join)
- [Add field from calculation](#add-field-from-calculation)
- [Labels to fields](#labels-to-fields)
- [Series to rows](#series-to-rows)
- [Debug transformations](#debug-transformations)

Keep reading for detailed descriptions of each type of transformation and the options available for each, as well as suggestions on how to use them.
Expand All @@ -96,25 +97,33 @@ After I apply the transformation, there is no time value and each column has bee

### Merge

Use this transformation to combine the result from multiple queries into one single result based on the time field. This is helpful when using the table panel visualization.
> **Note:** This documentation refers to a Grafana 7.1 feature.
In the example below, we are visualizing multiple queries returning table data before applying the transformation.
Use this transformation to combine the result from multiple queries into one single result. This is helpful when using the table panel visualization. Values that can be merged are combined into the same row. Values are mergeable if the shared fields contains the same data.

{{< docs-imagebox img="/img/docs/transformations/table-data-before-merge-7-1.png" class="docs-image--no-shadow" max-width= "1100px" >}}
In the example below, we have two queries returning table data. It is visualized as two separate tables before applying the transformation.

Here is the same example after applying the merge transformation.
Query A:

{{< docs-imagebox img="/img/docs/transformations/table-data-after-merge-7-1.png" class="docs-image--no-shadow" max-width= "1100px" >}}
| Time | Job | Uptime |
|---------------------|---------|-----------|
| 2020-07-07 11:34:20 | node | 25260122 |
| 2020-07-07 11:24:20 | postgre | 123001233 |

If any of the queries return time series data, then a `Metric` column containing the name of the query is added. You can be customized this value by defining `Label` on the source query.
Query B:

In the example below, we are visualizing multiple queries returning time series data before applying the transformation.
| Time | Job | Errors |
|---------------------|---------|--------|
| 2020-07-07 11:34:20 | node | 15 |
| 2020-07-07 11:24:20 | postgre | 5 |

{{< docs-imagebox img="/img/docs/transformations/time-series-before-merge-7-1.png" class="docs-image--no-shadow" max-width= "1100px" >}}
Here is the result after applying the `Merge` transformation.

Here is the same example after applying the merge transformation.
| Time | Job | Errors | Uptime |
|---------------------|---------|--------|-----------|
| 2020-07-07 11:34:20 | node | 15 | 25260122 |
| 2020-07-07 11:24:20 | postgre | 5 | 123001233 |

{{< docs-imagebox img="/img/docs/transformations/time-series-after-merge-7-1.png" class="docs-image--no-shadow" max-width= "1100px" >}}

### Filter by name

Expand Down Expand Up @@ -213,6 +222,43 @@ After I apply the transformation, my labels appear in the table as fields.

{{< docs-imagebox img="/img/docs/transformations/labels-to-fields-after-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}}

## Series to rows

> **Note:** This documentation refers to a Grafana 7.1 feature.
Use this transformation to combine the result from multiple time series data queries into one single result. This is helpful when using the table panel visualization.

The result from this transformation will contain three columns: `Time`, `Metric`, and `Value`. The `Metric` column is added so you easily can see from which query the metric originates from. Customize this value by defining `Label` on the source query.

In the example below, we have two queries returning time series data. It is visualized as two separate tables before applying the transformation.

Query A:

| Time | Temperature |
|---------------------|-------------|
| 2020-07-07 11:34:20 | 25 |
| 2020-07-07 10:31:22 | 22 |
| 2020-07-07 09:30:05 | 19 |

Query B:

| Time | Humidity |
|---------------------|----------|
| 2020-07-07 11:34:20 | 24 |
| 2020-07-07 10:32:20 | 29 |
| 2020-07-07 09:30:57 | 33 |

Here is the result after applying the `Series to rows` transformation.

| Time | Metric | Value |
|---------------------|-------------|-------|
| 2020-07-07 11:34:20 | Temperature | 25 |
| 2020-07-07 11:34:20 | Humidity | 22 |
| 2020-07-07 10:32:20 | Humidity | 29 |
| 2020-07-07 10:31:22 | Temperature | 22 |
| 2020-07-07 09:30:57 | Humidity | 33 |
| 2020-07-07 09:30:05 | Temperature | 19 |

## Debug transformations

To see the input and the output result sets of the transformation, click the bug icon on the right side of the transformation row.
Expand Down
12 changes: 12 additions & 0 deletions packages/grafana-data/src/dataframe/utils.ts
@@ -0,0 +1,12 @@
import { DataFrame, FieldType } from '../types/dataFrame';

export const isTimeSerie = (frame: DataFrame): boolean => {
if (frame.fields.length > 2) {
return false;
}
return !!frame.fields.find(field => field.type === FieldType.time);
};

export const isTimeSeries = (data: DataFrame[]): boolean => {
return !data.find(frame => !isTimeSerie(frame));
};
4 changes: 3 additions & 1 deletion packages/grafana-data/src/transformations/transformers.ts
Expand Up @@ -8,10 +8,11 @@ import { filterFramesByRefIdTransformer } from './transformers/filterByRefId';
import { orderFieldsTransformer } from './transformers/order';
import { organizeFieldsTransformer } from './transformers/organize';
import { seriesToColumnsTransformer } from './transformers/seriesToColumns';
import { seriesToRowsTransformer } from './transformers/seriesToRows';
import { renameFieldsTransformer } from './transformers/rename';
import { labelsToFieldsTransformer } from './transformers/labelsToFields';
import { ensureColumnsTransformer } from './transformers/ensureColumns';
import { mergeTransformer } from './transformers/merge/merge';
import { mergeTransformer } from './transformers/merge';

export const standardTransformers = {
noopTransformer,
Expand All @@ -25,6 +26,7 @@ export const standardTransformers = {
reduceTransformer,
calculateFieldTransformer,
seriesToColumnsTransformer,
seriesToRowsTransformer,
renameFieldsTransformer,
labelsToFieldsTransformer,
ensureColumnsTransformer,
Expand Down
Expand Up @@ -8,6 +8,7 @@ export enum DataTransformerID {
rename = 'rename',
calculateField = 'calculateField',
seriesToColumns = 'seriesToColumns',
seriesToRows = 'seriesToRows',
merge = 'merge',
labelsToFields = 'labelsToFields',
filterFields = 'filterFields',
Expand Down
@@ -1,9 +1,9 @@
import { mockTransformationsRegistry } from '../../../utils/tests/mockTransformationsRegistry';
import { DataTransformerConfig, Field, FieldType } from '../../../types';
import { DataTransformerID } from '../ids';
import { toDataFrame } from '../../../dataframe';
import { transformDataFrame } from '../../transformDataFrame';
import { ArrayVector } from '../../../vector';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { DataTransformerConfig, Field, FieldType } from '../../types';
import { DataTransformerID } from './ids';
import { toDataFrame } from '../../dataframe';
import { transformDataFrame } from '../transformDataFrame';
import { ArrayVector } from '../../vector';
import { mergeTransformer, MergeTransformerOptions } from './merge';

describe('Merge multipe to single', () => {
Expand Down Expand Up @@ -35,12 +35,11 @@ describe('Merge multipe to single', () => {

const result = transformDataFrame([cfg], [seriesA, seriesB]);
const expected: Field[] = [
createField('Time', FieldType.time, [1000, 2000]),
createField('Metric', FieldType.string, ['A', 'B']),
createField('Value', FieldType.number, [1, -1]),
createField('Time', FieldType.time, [2000, 1000]),
createField('Temp', FieldType.number, [-1, 1]),
];

expect(result[0].fields).toMatchObject(expected);
expect(unwrap(result[0].fields)).toEqual(expected);
});

it('combine two series with multiple values into one', () => {
Expand All @@ -67,12 +66,11 @@ describe('Merge multipe to single', () => {

const result = transformDataFrame([cfg], [seriesA, seriesB]);
const expected: Field[] = [
createField('Time', FieldType.time, [100, 100, 125, 126, 150, 200]),
createField('Metric', FieldType.string, ['A', 'B', 'B', 'B', 'A', 'A']),
createField('Value', FieldType.number, [1, -1, 2, 3, 4, 5]),
createField('Time', FieldType.time, [200, 150, 126, 125, 100, 100]),
createField('Temp', FieldType.number, [5, 4, 3, 2, 1, -1]),
];

expect(result[0].fields).toMatchObject(expected);
expect(unwrap(result[0].fields)).toEqual(expected);
});

it('combine three series into one', () => {
Expand Down Expand Up @@ -107,12 +105,11 @@ describe('Merge multipe to single', () => {

const result = transformDataFrame([cfg], [seriesA, seriesB, seriesC]);
const expected: Field[] = [
createField('Time', FieldType.time, [500, 1000, 2000]),
createField('Metric', FieldType.string, ['C', 'A', 'B']),
createField('Value', FieldType.number, [2, 1, -1]),
createField('Time', FieldType.time, [2000, 1000, 500]),
createField('Temp', FieldType.number, [-1, 1, 2]),
];

expect(result[0].fields).toMatchObject(expected);
expect(unwrap(result[0].fields)).toEqual(expected);
});

it('combine one serie and two tables into one table', () => {
Expand Down Expand Up @@ -149,13 +146,12 @@ describe('Merge multipe to single', () => {

const result = transformDataFrame([cfg], [tableA, seriesB, tableB]);
const expected: Field[] = [
createField('Time', FieldType.time, [500, 1000, 1000]),
createField('Metric', FieldType.string, ['C', 'A', 'B']),
createField('Temp', FieldType.number, [2, 1, -1]),
createField('Humidity', FieldType.number, [5, 10, null]),
createField('Time', FieldType.time, [1000, 1000, 500]),
createField('Temp', FieldType.number, [1, -1, 2]),
createField('Humidity', FieldType.number, [10, null, 5]),
];

expect(result[0].fields).toMatchObject(expected);
expect(unwrap(result[0].fields)).toEqual(expected);
});

it('combine one serie and two tables with ISO dates into one table', () => {
Expand Down Expand Up @@ -192,13 +188,12 @@ describe('Merge multipe to single', () => {

const result = transformDataFrame([cfg], [tableA, seriesB, tableC]);
const expected: Field[] = [
createField('Time', FieldType.time, ['2019-09-01T11:10:23Z', '2019-10-01T11:10:23Z', '2019-11-01T11:10:23Z']),
createField('Metric', FieldType.string, ['B', 'A', 'C']),
createField('Temp', FieldType.number, [-1, 1, 2]),
createField('Humidity', FieldType.number, [null, 10, 5]),
createField('Time', FieldType.time, ['2019-11-01T11:10:23Z', '2019-10-01T11:10:23Z', '2019-09-01T11:10:23Z']),
createField('Temp', FieldType.number, [2, 1, -1]),
createField('Humidity', FieldType.number, [5, 10, null]),
];

expect(result[0].fields).toMatchObject(expected);
expect(unwrap(result[0].fields)).toEqual(expected);
});

it('combine three tables with multiple values into one', () => {
Expand Down Expand Up @@ -235,14 +230,15 @@ describe('Merge multipe to single', () => {
});

const result = transformDataFrame([cfg], [tableA, tableB, tableC]);

const expected: Field[] = [
createField('Time', FieldType.time, [100, 100, 100, 124, 125, 126, 149, 150, 200]),
createField('Temp', FieldType.number, [1, -1, 1, 4, 2, 3, 5, 4, 5]),
createField('Humidity', FieldType.number, [10, null, 22, 25, null, null, 30, 14, 55]),
createField('Enabled', FieldType.boolean, [null, true, null, null, false, true, null, null, null]),
createField('Time', FieldType.time, [200, 150, 149, 126, 125, 124, 100, 100, 100]),
createField('Temp', FieldType.number, [5, 4, 5, 3, 2, 4, 1, -1, 1]),
createField('Humidity', FieldType.number, [55, 14, 30, null, null, 25, 10, null, 22]),
createField('Enabled', FieldType.boolean, [null, null, null, true, false, null, null, true, null]),
];

expect(result[0].fields).toMatchObject(expected);
expect(unwrap(result[0].fields)).toEqual(expected);
});

it('combine two time series, where first serie fields has displayName, into one', () => {
Expand All @@ -269,13 +265,14 @@ describe('Merge multipe to single', () => {

const result = transformDataFrame([cfg], [serieA, serieB]);
const expected: Field[] = [
createField('Time', FieldType.time, [100, 100, 125, 126, 150, 200]),
createField('Metric', FieldType.string, ['A', 'B', 'B', 'B', 'A', 'A']),
createField('Value', FieldType.number, [1, -1, 2, 3, 4, 5]),
createField('Time', FieldType.time, [200, 150, 126, 125, 100, 100]),
createField('Temp', FieldType.number, [5, 4, 3, 2, 1, -1]),
];

expect(result[0].fields[2].config).toEqual({});
expect(result[0].fields).toMatchObject(expected);
const fields = unwrap(result[0].fields);

expect(fields[1].config).toEqual({});
expect(fields).toEqual(expected);
});

it('combine two time series, where first serie fields has units, into one', () => {
Expand All @@ -302,13 +299,14 @@ describe('Merge multipe to single', () => {

const result = transformDataFrame([cfg], [serieA, serieB]);
const expected: Field[] = [
createField('Time', FieldType.time, [100, 100, 125, 126, 150, 200]),
createField('Metric', FieldType.string, ['A', 'B', 'B', 'B', 'A', 'A']),
createField('Value', FieldType.number, [1, -1, 2, 3, 4, 5], { units: 'celsius' }),
createField('Time', FieldType.time, [200, 150, 126, 125, 100, 100]),
createField('Temp', FieldType.number, [5, 4, 3, 2, 1, -1], { units: 'celsius' }),
];

expect(result[0].fields[2].config).toEqual({ units: 'celsius' });
expect(result[0].fields).toMatchObject(expected);
const fields = unwrap(result[0].fields);

expect(fields[1].config).toEqual({ units: 'celsius' });
expect(fields).toEqual(expected);
});

it('combine two time series, where second serie fields has units, into one', () => {
Expand All @@ -335,16 +333,28 @@ describe('Merge multipe to single', () => {

const result = transformDataFrame([cfg], [serieA, serieB]);
const expected: Field[] = [
createField('Time', FieldType.time, [100, 100, 125, 126, 150, 200]),
createField('Metric', FieldType.string, ['A', 'B', 'B', 'B', 'A', 'A']),
createField('Value', FieldType.number, [1, -1, 2, 3, 4, 5]),
createField('Time', FieldType.time, [200, 150, 126, 125, 100, 100]),
createField('Temp', FieldType.number, [5, 4, 3, 2, 1, -1]),
];

expect(result[0].fields[2].config).toEqual({});
expect(result[0].fields).toMatchObject(expected);
const fields = unwrap(result[0].fields);

expect(fields[1].config).toEqual({});
expect(fields).toEqual(expected);
});
});

const createField = (name: string, type: FieldType, values: any[], config = {}): Field => {
return { name, type, values: new ArrayVector(values), config, labels: undefined };
};

const unwrap = (fields: Field[]): Field[] => {
return fields.map(field =>
createField(
field.name,
field.type,
field.values.toArray().map((value: any) => value),
field.config
)
);
};

0 comments on commit 17d8707

Please sign in to comment.