Skip to content

Commit 1de35bf

Browse files
leeoniyanmarrsryantxu
authored
TimeSeries / StateTimeline: Add support for rendering enum fields (grafana#64179)
Co-authored-by: nmarrs <nathanielmarrs@gmail.com> Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
1 parent 4e50115 commit 1de35bf

File tree

13 files changed

+182
-61
lines changed

13 files changed

+182
-61
lines changed

packages/grafana-data/src/field/displayProcessor.ts

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { getFieldTypeFromValue } from '../dataframe/processDataFrame';
66
import { toUtc, dateTimeParse } from '../datetime';
77
import { GrafanaTheme2 } from '../themes/types';
88
import { KeyValue, TimeZone } from '../types';
9-
import { EnumFieldConfig, Field, FieldType } from '../types/dataFrame';
9+
import { Field, FieldType } from '../types/dataFrame';
1010
import { DecimalCount, DisplayProcessor, DisplayValue } from '../types/displayValue';
1111
import { anyToNumber } from '../utils/anyToNumber';
1212
import { getValueMappingResult } from '../utils/valueMappings';
@@ -44,6 +44,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
4444

4545
const field = options.field as Field;
4646
const config = field.config ?? {};
47+
const { palette } = options.theme.visualization;
4748

4849
let unit = config.unit;
4950
let hasDateUnit = unit && (timeFormats[unit] || unit.startsWith('time:'));
@@ -70,8 +71,6 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
7071
}
7172
} else if (!unit && field.type === FieldType.string) {
7273
unit = 'string';
73-
} else if (field.type === FieldType.enum) {
74-
return getEnumDisplayProcessor(options.theme, config.type?.enum);
7574
}
7675

7776
const hasCurrencyUnit = unit?.startsWith('currency');
@@ -116,6 +115,28 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
116115
icon = mappingResult.icon;
117116
}
118117
}
118+
} else if (field.type === FieldType.enum) {
119+
// Apply enum display handling if field is enum type and no mappings are specified
120+
if (value == null) {
121+
return {
122+
text: '',
123+
numeric: NaN,
124+
};
125+
}
126+
127+
const enumIndex = +value;
128+
if (config && config.type && config.type.enum) {
129+
const { text: enumText, color: enumColor } = config.type.enum;
130+
131+
text = enumText ? enumText[enumIndex] : `${value}`;
132+
// If no color specified in enum field config we will fallback to iterating through the theme palette
133+
color = enumColor ? enumColor[enumIndex] : undefined;
134+
135+
if (color == null) {
136+
const namedColor = palette[enumIndex % palette.length];
137+
color = options.theme.visualization.getColorByName(namedColor);
138+
}
139+
}
119140
}
120141

121142
if (!Number.isNaN(numeric)) {
@@ -192,41 +213,6 @@ function toStringProcessor(value: unknown): DisplayValue {
192213
return { text: toString(value), numeric: anyToNumber(value) };
193214
}
194215

195-
export function getEnumDisplayProcessor(theme: GrafanaTheme2, cfg?: EnumFieldConfig): DisplayProcessor {
196-
const config = {
197-
text: cfg?.text ?? [],
198-
color: cfg?.color ?? [],
199-
};
200-
// use the theme specific color values
201-
config.color = config.color.map((v) => theme.visualization.getColorByName(v));
202-
203-
return (value: unknown) => {
204-
if (value == null) {
205-
return {
206-
text: '',
207-
numeric: NaN,
208-
};
209-
}
210-
const idx = +value;
211-
let text = config.text[idx];
212-
if (text == null) {
213-
text = `${value}`; // the original value
214-
}
215-
let color = config.color[idx];
216-
if (color == null) {
217-
// constant color for index
218-
const { palette } = theme.visualization;
219-
color = palette[idx % palette.length];
220-
config.color[idx] = color;
221-
}
222-
return {
223-
text,
224-
numeric: idx,
225-
color,
226-
};
227-
};
228-
}
229-
230216
export function getRawDisplayProcessor(): DisplayProcessor {
231217
return (value: unknown) => ({
232218
text: getFieldTypeFromValue(value) === 'other' ? `${JSON.stringify(value, getCircularReplacer())}` : `${value}`,

packages/grafana-data/src/transformations/matchers/fieldTypeMatcher.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ const fieldTypeMatcher: FieldMatcherInfo<FieldType> = {
2121
},
2222
};
2323

24+
// General Field matcher (multiple types)
25+
const fieldTypesMatcher: FieldMatcherInfo<Set<FieldType>> = {
26+
id: FieldMatcherID.byTypes,
27+
name: 'Field Type',
28+
description: 'match based on the field types',
29+
defaultOptions: new Set(),
30+
31+
get: (types) => {
32+
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
33+
return types.has(field.type);
34+
};
35+
},
36+
37+
getOptionsDisplayText: (types) => {
38+
return `Field types: ${[...types].join(' | ')}`;
39+
},
40+
};
41+
2442
// Numeric Field matcher
2543
// This gets its own entry so it shows up in the dropdown
2644
const numericMatcher: FieldMatcherInfo = {
@@ -56,5 +74,5 @@ const timeMatcher: FieldMatcherInfo = {
5674
* Registry Initialization
5775
*/
5876
export function getFieldTypeMatchers(): FieldMatcherInfo[] {
59-
return [fieldTypeMatcher, numericMatcher, timeMatcher];
77+
return [fieldTypeMatcher, fieldTypesMatcher, numericMatcher, timeMatcher];
6078
}

packages/grafana-data/src/transformations/matchers/ids.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export enum FieldMatcherID {
1919

2020
// With arguments
2121
byType = 'byType',
22+
byTypes = 'byTypes',
2223
byName = 'byName',
2324
byNames = 'byNames',
2425
byRegexp = 'byRegexp',

packages/grafana-ui/src/components/GraphNG/GraphNG.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Field,
1111
FieldMatcherID,
1212
fieldMatchers,
13+
FieldType,
1314
LegacyGraphHoverEvent,
1415
TimeRange,
1516
TimeZone,
@@ -120,7 +121,7 @@ export class GraphNG extends Component<GraphNGProps, GraphNGState> {
120121
frames,
121122
fields || {
122123
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
123-
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
124+
y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])),
124125
},
125126
props.timeRange
126127
);

packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
4343
"incrs": undefined,
4444
"labelGap": 0,
4545
"rotate": undefined,
46-
"scale": "__fixed/na-na/na-na/auto/linear/na",
46+
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
4747
"show": true,
4848
"side": 3,
4949
"size": [Function],
@@ -81,7 +81,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
8181
"key": "__global_",
8282
"scales": [
8383
"x",
84-
"__fixed/na-na/na-na/auto/linear/na",
84+
"__fixed/na-na/na-na/auto/linear/na/number",
8585
],
8686
},
8787
},
@@ -101,7 +101,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
101101
[Function],
102102
],
103103
"scales": {
104-
"__fixed/na-na/na-na/auto/linear/na": {
104+
"__fixed/na-na/na-na/auto/linear/na/number": {
105105
"asinh": undefined,
106106
"auto": true,
107107
"dir": 1,
@@ -140,7 +140,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
140140
"stroke": "#ff0000",
141141
},
142142
"pxAlign": undefined,
143-
"scale": "__fixed/na-na/na-na/auto/linear/na",
143+
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
144144
"show": true,
145145
"spanGaps": false,
146146
"stroke": "#ff0000",
@@ -163,7 +163,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
163163
"stroke": "#ff0000",
164164
},
165165
"pxAlign": undefined,
166-
"scale": "__fixed/na-na/na-na/auto/linear/na",
166+
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
167167
"show": true,
168168
"spanGaps": false,
169169
"stroke": "#ff0000",
@@ -186,7 +186,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
186186
"stroke": "#ff0000",
187187
},
188188
"pxAlign": undefined,
189-
"scale": "__fixed/na-na/na-na/auto/linear/na",
189+
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
190190
"show": true,
191191
"spanGaps": false,
192192
"stroke": "#ff0000",
@@ -209,7 +209,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
209209
"stroke": "#ff0000",
210210
},
211211
"pxAlign": undefined,
212-
"scale": "__fixed/na-na/na-na/auto/linear/na",
212+
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
213213
"show": true,
214214
"spanGaps": false,
215215
"stroke": "#ff0000",
@@ -232,7 +232,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
232232
"stroke": "#ff0000",
233233
},
234234
"pxAlign": undefined,
235-
"scale": "__fixed/na-na/na-na/auto/linear/na",
235+
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
236236
"show": true,
237237
"spanGaps": false,
238238
"stroke": "#ff0000",

packages/grafana-ui/src/components/GraphNG/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers
146146
return null;
147147
}
148148

149-
export function buildScaleKey(config: FieldConfig<GraphFieldConfig>) {
149+
export function buildScaleKey(config: FieldConfig<GraphFieldConfig>, fieldType: FieldType) {
150150
const defaultPart = 'na';
151151

152152
const scaleRange = `${config.min !== undefined ? config.min : defaultPart}-${
@@ -169,7 +169,7 @@ export function buildScaleKey(config: FieldConfig<GraphFieldConfig>) {
169169

170170
const scaleLabel = Boolean(config.custom?.axisLabel) ? config.custom!.axisLabel : defaultPart;
171171

172-
return `${scaleUnit}/${scaleRange}/${scaleSoftRange}/${scalePlacement}/${scaleDistribution}/${scaleLabel}`;
172+
return `${scaleUnit}/${scaleRange}/${scaleSoftRange}/${scalePlacement}/${scaleDistribution}/${scaleLabel}/${fieldType}`;
173173
}
174174

175175
function getScaleDistributionPart(config: ScaleDistributionConfig) {

packages/grafana-ui/src/components/TimeSeries/utils.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
214214

215215
const customConfig: GraphFieldConfig = config.custom!;
216216

217-
if (field === xField || field.type !== FieldType.number) {
217+
if (field === xField || (field.type !== FieldType.number && field.type !== FieldType.enum)) {
218218
continue;
219219
}
220220

@@ -231,7 +231,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
231231
theme,
232232
});
233233
}
234-
const scaleKey = buildScaleKey(config);
234+
const scaleKey = buildScaleKey(config, field.type);
235235
const colorMode = getFieldColorModeForField(field);
236236
const scaleColor = getFieldSeriesColor(field, theme);
237237
const seriesColor = scaleColor.color;
@@ -258,6 +258,16 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
258258
dataMax = dataMax > 0 ? 1 : 0;
259259
return [dataMin, dataMax];
260260
}
261+
: field.type === FieldType.enum
262+
? (u: uPlot, dataMin: number, dataMax: number) => {
263+
// this is the exhaustive enum (stable)
264+
let len = field.config.type!.enum!.text!.length;
265+
266+
return [-1, len];
267+
268+
// these are only values that are present
269+
// return [dataMin - 1, dataMax + 1]
270+
}
261271
: undefined,
262272
decimals: field.config.decimals,
263273
},
@@ -302,8 +312,16 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
302312

303313
let incrs: uPlot.Axis.Incrs | undefined;
304314

315+
// TODO: these will be dynamic with frame updates, so need to accept getYTickLabels()
316+
let values: uPlot.Axis.Values | undefined;
317+
let splits: uPlot.Axis.Splits | undefined;
318+
305319
if (IEC_UNITS.has(config.unit!)) {
306320
incrs = BIN_INCRS;
321+
} else if (field.type === FieldType.enum) {
322+
let text = field.config.type!.enum!.text!;
323+
splits = text.map((v: string, i: number) => i);
324+
values = text;
307325
}
308326

309327
builder.addAxis(
@@ -318,6 +336,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
318336
grid: { show: customConfig.axisGridShow },
319337
decimals: field.config.decimals,
320338
distr: customConfig.scaleDistribution?.type,
339+
splits,
340+
values,
321341
incrs,
322342
...axisColorOpts,
323343
},

packages/grafana-ui/src/components/uPlot/utils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export function getStackingBands(group: StackingGroup) {
8282
export function getStackingGroups(frame: DataFrame) {
8383
let groups: Map<string, StackingGroup> = new Map();
8484

85-
frame.fields.forEach(({ config, values }, i) => {
85+
frame.fields.forEach(({ config, values, type }, i) => {
8686
// skip x or time field
8787
if (i === 0) {
8888
return;
@@ -125,7 +125,10 @@ export function getStackingGroups(frame: DataFrame) {
125125
? (custom.lineInterpolation as LineInterpolation)
126126
: null;
127127

128-
let stackKey = `${stackDir}|${stackingMode}|${stackingGroup}|${buildScaleKey(config)}|${drawStyle}|${drawStyle2}`;
128+
let stackKey = `${stackDir}|${stackingMode}|${stackingGroup}|${buildScaleKey(
129+
config,
130+
type
131+
)}|${drawStyle}|${drawStyle2}`;
129132

130133
let group = groups.get(stackKey);
131134

public/app/core/components/TimelineChart/TimelineChart.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ export class TimelineChart extends React.Component<TimelineProps> {
8888
{...this.props}
8989
fields={{
9090
x: (f) => f.type === FieldType.time,
91-
y: (f) => f.type === FieldType.number || f.type === FieldType.boolean || f.type === FieldType.string,
91+
y: (f) =>
92+
f.type === FieldType.number ||
93+
f.type === FieldType.boolean ||
94+
f.type === FieldType.string ||
95+
f.type === FieldType.enum,
9296
}}
9397
prepConfig={this.prepConfig}
9498
propsToDiff={propsToDiff}

public/app/core/components/TimelineChart/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,7 @@ export function prepareTimelineFields(
466466
hasTimeseries = true;
467467
fields.push(field);
468468
break;
469+
case FieldType.enum:
469470
case FieldType.number:
470471
if (mergeValues && field.config.color?.mode === FieldColorModeId.Thresholds) {
471472
const f = mergeThresholdValues(field, theme);

0 commit comments

Comments
 (0)