TimeSeries / StateTimeline: Add support for rendering enum fields (#64179)

Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Leon Sorokin
2023-07-21 11:38:11 -05:00
committed by GitHub
co-authored by nmarrs Ryan McKinley
parent 4e50115d95
commit 1de35bf3c3
13 changed files with 182 additions and 61 deletions
@@ -6,7 +6,7 @@ import { getFieldTypeFromValue } from '../dataframe/processDataFrame';
import { toUtc, dateTimeParse } from '../datetime';
import { GrafanaTheme2 } from '../themes/types';
import { KeyValue, TimeZone } from '../types';
import { EnumFieldConfig, Field, FieldType } from '../types/dataFrame';
import { Field, FieldType } from '../types/dataFrame';
import { DecimalCount, DisplayProcessor, DisplayValue } from '../types/displayValue';
import { anyToNumber } from '../utils/anyToNumber';
import { getValueMappingResult } from '../utils/valueMappings';
@@ -44,6 +44,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
const field = options.field as Field;
const config = field.config ?? {};
const { palette } = options.theme.visualization;
let unit = config.unit;
let hasDateUnit = unit && (timeFormats[unit] || unit.startsWith('time:'));
@@ -70,8 +71,6 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
}
} else if (!unit && field.type === FieldType.string) {
unit = 'string';
} else if (field.type === FieldType.enum) {
return getEnumDisplayProcessor(options.theme, config.type?.enum);
}
const hasCurrencyUnit = unit?.startsWith('currency');
@@ -116,6 +115,28 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
icon = mappingResult.icon;
}
}
} else if (field.type === FieldType.enum) {
// Apply enum display handling if field is enum type and no mappings are specified
if (value == null) {
return {
text: '',
numeric: NaN,
};
}
const enumIndex = +value;
if (config && config.type && config.type.enum) {
const { text: enumText, color: enumColor } = config.type.enum;
text = enumText ? enumText[enumIndex] : `${value}`;
// If no color specified in enum field config we will fallback to iterating through the theme palette
color = enumColor ? enumColor[enumIndex] : undefined;
if (color == null) {
const namedColor = palette[enumIndex % palette.length];
color = options.theme.visualization.getColorByName(namedColor);
}
}
}
if (!Number.isNaN(numeric)) {
@@ -192,41 +213,6 @@ function toStringProcessor(value: unknown): DisplayValue {
return { text: toString(value), numeric: anyToNumber(value) };
}
export function getEnumDisplayProcessor(theme: GrafanaTheme2, cfg?: EnumFieldConfig): DisplayProcessor {
const config = {
text: cfg?.text ?? [],
color: cfg?.color ?? [],
};
// use the theme specific color values
config.color = config.color.map((v) => theme.visualization.getColorByName(v));
return (value: unknown) => {
if (value == null) {
return {
text: '',
numeric: NaN,
};
}
const idx = +value;
let text = config.text[idx];
if (text == null) {
text = `${value}`; // the original value
}
let color = config.color[idx];
if (color == null) {
// constant color for index
const { palette } = theme.visualization;
color = palette[idx % palette.length];
config.color[idx] = color;
}
return {
text,
numeric: idx,
color,
};
};
}
export function getRawDisplayProcessor(): DisplayProcessor {
return (value: unknown) => ({
text: getFieldTypeFromValue(value) === 'other' ? `${JSON.stringify(value, getCircularReplacer())}` : `${value}`,
@@ -21,6 +21,24 @@ const fieldTypeMatcher: FieldMatcherInfo<FieldType> = {
},
};
// General Field matcher (multiple types)
const fieldTypesMatcher: FieldMatcherInfo<Set<FieldType>> = {
id: FieldMatcherID.byTypes,
name: 'Field Type',
description: 'match based on the field types',
defaultOptions: new Set(),
get: (types) => {
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
return types.has(field.type);
};
},
getOptionsDisplayText: (types) => {
return `Field types: ${[...types].join(' | ')}`;
},
};
// Numeric Field matcher
// This gets its own entry so it shows up in the dropdown
const numericMatcher: FieldMatcherInfo = {
@@ -56,5 +74,5 @@ const timeMatcher: FieldMatcherInfo = {
* Registry Initialization
*/
export function getFieldTypeMatchers(): FieldMatcherInfo[] {
return [fieldTypeMatcher, numericMatcher, timeMatcher];
return [fieldTypeMatcher, fieldTypesMatcher, numericMatcher, timeMatcher];
}
@@ -19,6 +19,7 @@ export enum FieldMatcherID {
// With arguments
byType = 'byType',
byTypes = 'byTypes',
byName = 'byName',
byNames = 'byNames',
byRegexp = 'byRegexp',
@@ -10,6 +10,7 @@ import {
Field,
FieldMatcherID,
fieldMatchers,
FieldType,
LegacyGraphHoverEvent,
TimeRange,
TimeZone,
@@ -120,7 +121,7 @@ export class GraphNG extends Component<GraphNGProps, GraphNGState> {
frames,
fields || {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])),
},
props.timeRange
);
@@ -43,7 +43,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
"incrs": undefined,
"labelGap": 0,
"rotate": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na",
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"side": 3,
"size": [Function],
@@ -81,7 +81,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
"key": "__global_",
"scales": [
"x",
"__fixed/na-na/na-na/auto/linear/na",
"__fixed/na-na/na-na/auto/linear/na/number",
],
},
},
@@ -101,7 +101,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
[Function],
],
"scales": {
"__fixed/na-na/na-na/auto/linear/na": {
"__fixed/na-na/na-na/auto/linear/na/number": {
"asinh": undefined,
"auto": true,
"dir": 1,
@@ -140,7 +140,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
"stroke": "#ff0000",
},
"pxAlign": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na",
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"spanGaps": false,
"stroke": "#ff0000",
@@ -163,7 +163,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
"stroke": "#ff0000",
},
"pxAlign": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na",
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"spanGaps": false,
"stroke": "#ff0000",
@@ -186,7 +186,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
"stroke": "#ff0000",
},
"pxAlign": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na",
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"spanGaps": false,
"stroke": "#ff0000",
@@ -209,7 +209,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
"stroke": "#ff0000",
},
"pxAlign": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na",
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"spanGaps": false,
"stroke": "#ff0000",
@@ -232,7 +232,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
"stroke": "#ff0000",
},
"pxAlign": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na",
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"spanGaps": false,
"stroke": "#ff0000",
@@ -146,7 +146,7 @@ export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers
return null;
}
export function buildScaleKey(config: FieldConfig<GraphFieldConfig>) {
export function buildScaleKey(config: FieldConfig<GraphFieldConfig>, fieldType: FieldType) {
const defaultPart = 'na';
const scaleRange = `${config.min !== undefined ? config.min : defaultPart}-${
@@ -169,7 +169,7 @@ export function buildScaleKey(config: FieldConfig<GraphFieldConfig>) {
const scaleLabel = Boolean(config.custom?.axisLabel) ? config.custom!.axisLabel : defaultPart;
return `${scaleUnit}/${scaleRange}/${scaleSoftRange}/${scalePlacement}/${scaleDistribution}/${scaleLabel}`;
return `${scaleUnit}/${scaleRange}/${scaleSoftRange}/${scalePlacement}/${scaleDistribution}/${scaleLabel}/${fieldType}`;
}
function getScaleDistributionPart(config: ScaleDistributionConfig) {
@@ -214,7 +214,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
const customConfig: GraphFieldConfig = config.custom!;
if (field === xField || field.type !== FieldType.number) {
if (field === xField || (field.type !== FieldType.number && field.type !== FieldType.enum)) {
continue;
}
@@ -231,7 +231,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
theme,
});
}
const scaleKey = buildScaleKey(config);
const scaleKey = buildScaleKey(config, field.type);
const colorMode = getFieldColorModeForField(field);
const scaleColor = getFieldSeriesColor(field, theme);
const seriesColor = scaleColor.color;
@@ -258,6 +258,16 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
dataMax = dataMax > 0 ? 1 : 0;
return [dataMin, dataMax];
}
: field.type === FieldType.enum
? (u: uPlot, dataMin: number, dataMax: number) => {
// this is the exhaustive enum (stable)
let len = field.config.type!.enum!.text!.length;
return [-1, len];
// these are only values that are present
// return [dataMin - 1, dataMax + 1]
}
: undefined,
decimals: field.config.decimals,
},
@@ -302,8 +312,16 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
let incrs: uPlot.Axis.Incrs | undefined;
// TODO: these will be dynamic with frame updates, so need to accept getYTickLabels()
let values: uPlot.Axis.Values | undefined;
let splits: uPlot.Axis.Splits | undefined;
if (IEC_UNITS.has(config.unit!)) {
incrs = BIN_INCRS;
} else if (field.type === FieldType.enum) {
let text = field.config.type!.enum!.text!;
splits = text.map((v: string, i: number) => i);
values = text;
}
builder.addAxis(
@@ -318,6 +336,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
grid: { show: customConfig.axisGridShow },
decimals: field.config.decimals,
distr: customConfig.scaleDistribution?.type,
splits,
values,
incrs,
...axisColorOpts,
},
@@ -82,7 +82,7 @@ export function getStackingBands(group: StackingGroup) {
export function getStackingGroups(frame: DataFrame) {
let groups: Map<string, StackingGroup> = new Map();
frame.fields.forEach(({ config, values }, i) => {
frame.fields.forEach(({ config, values, type }, i) => {
// skip x or time field
if (i === 0) {
return;
@@ -125,7 +125,10 @@ export function getStackingGroups(frame: DataFrame) {
? (custom.lineInterpolation as LineInterpolation)
: null;
let stackKey = `${stackDir}|${stackingMode}|${stackingGroup}|${buildScaleKey(config)}|${drawStyle}|${drawStyle2}`;
let stackKey = `${stackDir}|${stackingMode}|${stackingGroup}|${buildScaleKey(
config,
type
)}|${drawStyle}|${drawStyle2}`;
let group = groups.get(stackKey);