mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
4e50115d95
commit
1de35bf3c3
@ -6,7 +6,7 @@ import { getFieldTypeFromValue } from '../dataframe/processDataFrame';
|
|||||||
import { toUtc, dateTimeParse } from '../datetime';
|
import { toUtc, dateTimeParse } from '../datetime';
|
||||||
import { GrafanaTheme2 } from '../themes/types';
|
import { GrafanaTheme2 } from '../themes/types';
|
||||||
import { KeyValue, TimeZone } from '../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 { DecimalCount, DisplayProcessor, DisplayValue } from '../types/displayValue';
|
||||||
import { anyToNumber } from '../utils/anyToNumber';
|
import { anyToNumber } from '../utils/anyToNumber';
|
||||||
import { getValueMappingResult } from '../utils/valueMappings';
|
import { getValueMappingResult } from '../utils/valueMappings';
|
||||||
@ -44,6 +44,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
|
|||||||
|
|
||||||
const field = options.field as Field;
|
const field = options.field as Field;
|
||||||
const config = field.config ?? {};
|
const config = field.config ?? {};
|
||||||
|
const { palette } = options.theme.visualization;
|
||||||
|
|
||||||
let unit = config.unit;
|
let unit = config.unit;
|
||||||
let hasDateUnit = unit && (timeFormats[unit] || unit.startsWith('time:'));
|
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) {
|
} else if (!unit && field.type === FieldType.string) {
|
||||||
unit = 'string';
|
unit = 'string';
|
||||||
} else if (field.type === FieldType.enum) {
|
|
||||||
return getEnumDisplayProcessor(options.theme, config.type?.enum);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasCurrencyUnit = unit?.startsWith('currency');
|
const hasCurrencyUnit = unit?.startsWith('currency');
|
||||||
@ -116,6 +115,28 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
|
|||||||
icon = mappingResult.icon;
|
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)) {
|
if (!Number.isNaN(numeric)) {
|
||||||
@ -192,41 +213,6 @@ function toStringProcessor(value: unknown): DisplayValue {
|
|||||||
return { text: toString(value), numeric: anyToNumber(value) };
|
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 {
|
export function getRawDisplayProcessor(): DisplayProcessor {
|
||||||
return (value: unknown) => ({
|
return (value: unknown) => ({
|
||||||
text: getFieldTypeFromValue(value) === 'other' ? `${JSON.stringify(value, getCircularReplacer())}` : `${value}`,
|
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
|
// Numeric Field matcher
|
||||||
// This gets its own entry so it shows up in the dropdown
|
// This gets its own entry so it shows up in the dropdown
|
||||||
const numericMatcher: FieldMatcherInfo = {
|
const numericMatcher: FieldMatcherInfo = {
|
||||||
@ -56,5 +74,5 @@ const timeMatcher: FieldMatcherInfo = {
|
|||||||
* Registry Initialization
|
* Registry Initialization
|
||||||
*/
|
*/
|
||||||
export function getFieldTypeMatchers(): FieldMatcherInfo[] {
|
export function getFieldTypeMatchers(): FieldMatcherInfo[] {
|
||||||
return [fieldTypeMatcher, numericMatcher, timeMatcher];
|
return [fieldTypeMatcher, fieldTypesMatcher, numericMatcher, timeMatcher];
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ export enum FieldMatcherID {
|
|||||||
|
|
||||||
// With arguments
|
// With arguments
|
||||||
byType = 'byType',
|
byType = 'byType',
|
||||||
|
byTypes = 'byTypes',
|
||||||
byName = 'byName',
|
byName = 'byName',
|
||||||
byNames = 'byNames',
|
byNames = 'byNames',
|
||||||
byRegexp = 'byRegexp',
|
byRegexp = 'byRegexp',
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
Field,
|
Field,
|
||||||
FieldMatcherID,
|
FieldMatcherID,
|
||||||
fieldMatchers,
|
fieldMatchers,
|
||||||
|
FieldType,
|
||||||
LegacyGraphHoverEvent,
|
LegacyGraphHoverEvent,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
@ -120,7 +121,7 @@ export class GraphNG extends Component<GraphNGProps, GraphNGState> {
|
|||||||
frames,
|
frames,
|
||||||
fields || {
|
fields || {
|
||||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
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
|
props.timeRange
|
||||||
);
|
);
|
||||||
|
@ -43,7 +43,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
|
|||||||
"incrs": undefined,
|
"incrs": undefined,
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
"rotate": undefined,
|
"rotate": undefined,
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na",
|
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 3,
|
"side": 3,
|
||||||
"size": [Function],
|
"size": [Function],
|
||||||
@ -81,7 +81,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
|
|||||||
"key": "__global_",
|
"key": "__global_",
|
||||||
"scales": [
|
"scales": [
|
||||||
"x",
|
"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],
|
[Function],
|
||||||
],
|
],
|
||||||
"scales": {
|
"scales": {
|
||||||
"__fixed/na-na/na-na/auto/linear/na": {
|
"__fixed/na-na/na-na/auto/linear/na/number": {
|
||||||
"asinh": undefined,
|
"asinh": undefined,
|
||||||
"auto": true,
|
"auto": true,
|
||||||
"dir": 1,
|
"dir": 1,
|
||||||
@ -140,7 +140,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
|
|||||||
"stroke": "#ff0000",
|
"stroke": "#ff0000",
|
||||||
},
|
},
|
||||||
"pxAlign": undefined,
|
"pxAlign": undefined,
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na",
|
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
||||||
"show": true,
|
"show": true,
|
||||||
"spanGaps": false,
|
"spanGaps": false,
|
||||||
"stroke": "#ff0000",
|
"stroke": "#ff0000",
|
||||||
@ -163,7 +163,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
|
|||||||
"stroke": "#ff0000",
|
"stroke": "#ff0000",
|
||||||
},
|
},
|
||||||
"pxAlign": undefined,
|
"pxAlign": undefined,
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na",
|
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
||||||
"show": true,
|
"show": true,
|
||||||
"spanGaps": false,
|
"spanGaps": false,
|
||||||
"stroke": "#ff0000",
|
"stroke": "#ff0000",
|
||||||
@ -186,7 +186,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
|
|||||||
"stroke": "#ff0000",
|
"stroke": "#ff0000",
|
||||||
},
|
},
|
||||||
"pxAlign": undefined,
|
"pxAlign": undefined,
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na",
|
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
||||||
"show": true,
|
"show": true,
|
||||||
"spanGaps": false,
|
"spanGaps": false,
|
||||||
"stroke": "#ff0000",
|
"stroke": "#ff0000",
|
||||||
@ -209,7 +209,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
|
|||||||
"stroke": "#ff0000",
|
"stroke": "#ff0000",
|
||||||
},
|
},
|
||||||
"pxAlign": undefined,
|
"pxAlign": undefined,
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na",
|
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
||||||
"show": true,
|
"show": true,
|
||||||
"spanGaps": false,
|
"spanGaps": false,
|
||||||
"stroke": "#ff0000",
|
"stroke": "#ff0000",
|
||||||
@ -232,7 +232,7 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
|
|||||||
"stroke": "#ff0000",
|
"stroke": "#ff0000",
|
||||||
},
|
},
|
||||||
"pxAlign": undefined,
|
"pxAlign": undefined,
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na",
|
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
||||||
"show": true,
|
"show": true,
|
||||||
"spanGaps": false,
|
"spanGaps": false,
|
||||||
"stroke": "#ff0000",
|
"stroke": "#ff0000",
|
||||||
|
@ -146,7 +146,7 @@ export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildScaleKey(config: FieldConfig<GraphFieldConfig>) {
|
export function buildScaleKey(config: FieldConfig<GraphFieldConfig>, fieldType: FieldType) {
|
||||||
const defaultPart = 'na';
|
const defaultPart = 'na';
|
||||||
|
|
||||||
const scaleRange = `${config.min !== undefined ? config.min : defaultPart}-${
|
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;
|
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) {
|
function getScaleDistributionPart(config: ScaleDistributionConfig) {
|
||||||
|
@ -214,7 +214,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
|||||||
|
|
||||||
const customConfig: GraphFieldConfig = config.custom!;
|
const customConfig: GraphFieldConfig = config.custom!;
|
||||||
|
|
||||||
if (field === xField || field.type !== FieldType.number) {
|
if (field === xField || (field.type !== FieldType.number && field.type !== FieldType.enum)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,7 +231,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
|||||||
theme,
|
theme,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const scaleKey = buildScaleKey(config);
|
const scaleKey = buildScaleKey(config, field.type);
|
||||||
const colorMode = getFieldColorModeForField(field);
|
const colorMode = getFieldColorModeForField(field);
|
||||||
const scaleColor = getFieldSeriesColor(field, theme);
|
const scaleColor = getFieldSeriesColor(field, theme);
|
||||||
const seriesColor = scaleColor.color;
|
const seriesColor = scaleColor.color;
|
||||||
@ -258,6 +258,16 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
|||||||
dataMax = dataMax > 0 ? 1 : 0;
|
dataMax = dataMax > 0 ? 1 : 0;
|
||||||
return [dataMin, dataMax];
|
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,
|
: undefined,
|
||||||
decimals: field.config.decimals,
|
decimals: field.config.decimals,
|
||||||
},
|
},
|
||||||
@ -302,8 +312,16 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
|||||||
|
|
||||||
let incrs: uPlot.Axis.Incrs | undefined;
|
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!)) {
|
if (IEC_UNITS.has(config.unit!)) {
|
||||||
incrs = BIN_INCRS;
|
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(
|
builder.addAxis(
|
||||||
@ -318,6 +336,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
|||||||
grid: { show: customConfig.axisGridShow },
|
grid: { show: customConfig.axisGridShow },
|
||||||
decimals: field.config.decimals,
|
decimals: field.config.decimals,
|
||||||
distr: customConfig.scaleDistribution?.type,
|
distr: customConfig.scaleDistribution?.type,
|
||||||
|
splits,
|
||||||
|
values,
|
||||||
incrs,
|
incrs,
|
||||||
...axisColorOpts,
|
...axisColorOpts,
|
||||||
},
|
},
|
||||||
|
@ -82,7 +82,7 @@ export function getStackingBands(group: StackingGroup) {
|
|||||||
export function getStackingGroups(frame: DataFrame) {
|
export function getStackingGroups(frame: DataFrame) {
|
||||||
let groups: Map<string, StackingGroup> = new Map();
|
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
|
// skip x or time field
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
return;
|
return;
|
||||||
@ -125,7 +125,10 @@ export function getStackingGroups(frame: DataFrame) {
|
|||||||
? (custom.lineInterpolation as LineInterpolation)
|
? (custom.lineInterpolation as LineInterpolation)
|
||||||
: null;
|
: 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);
|
let group = groups.get(stackKey);
|
||||||
|
|
||||||
|
@ -88,7 +88,11 @@ export class TimelineChart extends React.Component<TimelineProps> {
|
|||||||
{...this.props}
|
{...this.props}
|
||||||
fields={{
|
fields={{
|
||||||
x: (f) => f.type === FieldType.time,
|
x: (f) => f.type === FieldType.time,
|
||||||
y: (f) => f.type === FieldType.number || f.type === FieldType.boolean || f.type === FieldType.string,
|
y: (f) =>
|
||||||
|
f.type === FieldType.number ||
|
||||||
|
f.type === FieldType.boolean ||
|
||||||
|
f.type === FieldType.string ||
|
||||||
|
f.type === FieldType.enum,
|
||||||
}}
|
}}
|
||||||
prepConfig={this.prepConfig}
|
prepConfig={this.prepConfig}
|
||||||
propsToDiff={propsToDiff}
|
propsToDiff={propsToDiff}
|
||||||
|
@ -466,6 +466,7 @@ export function prepareTimelineFields(
|
|||||||
hasTimeseries = true;
|
hasTimeseries = true;
|
||||||
fields.push(field);
|
fields.push(field);
|
||||||
break;
|
break;
|
||||||
|
case FieldType.enum:
|
||||||
case FieldType.number:
|
case FieldType.number:
|
||||||
if (mergeValues && field.config.color?.mode === FieldColorModeId.Thresholds) {
|
if (mergeValues && field.config.color?.mode === FieldColorModeId.Thresholds) {
|
||||||
const f = mergeThresholdValues(field, theme);
|
const f = mergeThresholdValues(field, theme);
|
||||||
|
@ -3,8 +3,8 @@ import {
|
|||||||
DataFrame,
|
DataFrame,
|
||||||
DisplayProcessor,
|
DisplayProcessor,
|
||||||
Field,
|
Field,
|
||||||
|
FieldType,
|
||||||
getDisplayProcessor,
|
getDisplayProcessor,
|
||||||
getEnumDisplayProcessor,
|
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
@ -107,7 +107,9 @@ export class FlameGraphDataContainer {
|
|||||||
// both a backward compatibility but also to allow using a simple dataFrame without enum config. This would allow
|
// both a backward compatibility but also to allow using a simple dataFrame without enum config. This would allow
|
||||||
// users to use this panel with correct query from data sources that do not return profiles natively.
|
// users to use this panel with correct query from data sources that do not return profiles natively.
|
||||||
if (enumConfig) {
|
if (enumConfig) {
|
||||||
this.labelDisplayProcessor = getEnumDisplayProcessor(theme, enumConfig);
|
// TODO: Fix this from backend to set field type to enum correctly
|
||||||
|
this.labelField.type = FieldType.enum;
|
||||||
|
this.labelDisplayProcessor = getDisplayProcessor({ field: this.labelField, theme });
|
||||||
this.uniqueLabels = enumConfig.text || [];
|
this.uniqueLabels = enumConfig.text || [];
|
||||||
} else {
|
} else {
|
||||||
this.labelDisplayProcessor = (value) => ({
|
this.labelDisplayProcessor = (value) => ({
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useLayoutEffect, useMemo, useRef } from 'react';
|
import React, { useState, useLayoutEffect, useMemo, useRef } from 'react';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import { FieldConfigSource, ThresholdsConfig, getValueFormat } from '@grafana/data';
|
import { FieldConfigSource, ThresholdsConfig, getValueFormat, FieldType } from '@grafana/data';
|
||||||
import { UPlotConfigBuilder, buildScaleKey } from '@grafana/ui';
|
import { UPlotConfigBuilder, buildScaleKey } from '@grafana/ui';
|
||||||
|
|
||||||
import { ThresholdDragHandle } from './ThresholdDragHandle';
|
import { ThresholdDragHandle } from './ThresholdDragHandle';
|
||||||
@ -40,7 +40,7 @@ export const ThresholdControlsPlugin = ({ config, fieldConfig, onThresholdsChang
|
|||||||
if (!thresholds) {
|
if (!thresholds) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const scale = buildScaleKey(fieldConfig.defaults);
|
const scale = buildScaleKey(fieldConfig.defaults, FieldType.number);
|
||||||
|
|
||||||
const decimals = fieldConfig.defaults.decimals;
|
const decimals = fieldConfig.defaults.decimals;
|
||||||
const handles = [];
|
const handles = [];
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
ArrayVector,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
Field,
|
Field,
|
||||||
FieldType,
|
FieldType,
|
||||||
@ -14,6 +15,58 @@ import { convertFieldType } from '@grafana/data/src/transformations/transformers
|
|||||||
import { GraphFieldConfig, LineInterpolation } from '@grafana/schema';
|
import { GraphFieldConfig, LineInterpolation } from '@grafana/schema';
|
||||||
import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold';
|
import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold';
|
||||||
import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue';
|
import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue';
|
||||||
|
import { buildScaleKey } from '@grafana/ui/src/components/GraphNG/utils';
|
||||||
|
|
||||||
|
type ScaleKey = string;
|
||||||
|
|
||||||
|
// this will re-enumerate all enum fields on the same scale to create one ordinal progression
|
||||||
|
// e.g. ['a','b'][0,1,0] + ['c','d'][1,0,1] -> ['a','b'][0,1,0] + ['c','d'][3,2,3]
|
||||||
|
function reEnumFields(frames: DataFrame[]) {
|
||||||
|
let allTextsByKey: Map<ScaleKey, string[]> = new Map();
|
||||||
|
|
||||||
|
let frames2: DataFrame[] = frames.map((frame) => {
|
||||||
|
return {
|
||||||
|
...frame,
|
||||||
|
fields: frame.fields.map((field) => {
|
||||||
|
if (field.type === FieldType.enum) {
|
||||||
|
let scaleKey = buildScaleKey(field.config, field.type);
|
||||||
|
let allTexts = allTextsByKey.get(scaleKey);
|
||||||
|
|
||||||
|
if (!allTexts) {
|
||||||
|
allTexts = [];
|
||||||
|
allTextsByKey.set(scaleKey, allTexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
let idxs: number[] = field.values.toArray().slice();
|
||||||
|
let txts = field.config.type!.enum!.text!;
|
||||||
|
|
||||||
|
// by-reference incrementing
|
||||||
|
if (allTexts.length > 0) {
|
||||||
|
for (let i = 0; i < idxs.length; i++) {
|
||||||
|
idxs[i] += allTexts.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allTexts.push(...txts);
|
||||||
|
|
||||||
|
// shared among all enum fields on same scale
|
||||||
|
field.config.type!.enum!.text! = allTexts;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
values: new ArrayVector(idxs),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: update displayProcessor?
|
||||||
|
}
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return frames2;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns null if there are no graphable fields
|
* Returns null if there are no graphable fields
|
||||||
@ -52,6 +105,17 @@ export function prepareGraphableFields(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let enumFieldsCount = 0;
|
||||||
|
|
||||||
|
loopy: for (let frame of series) {
|
||||||
|
for (let field of frame.fields) {
|
||||||
|
if (field.type === FieldType.enum && ++enumFieldsCount > 1) {
|
||||||
|
series = reEnumFields(series);
|
||||||
|
break loopy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let copy: Field;
|
let copy: Field;
|
||||||
|
|
||||||
const frames: DataFrame[] = [];
|
const frames: DataFrame[] = [];
|
||||||
@ -94,6 +158,8 @@ export function prepareGraphableFields(
|
|||||||
|
|
||||||
fields.push(copy);
|
fields.push(copy);
|
||||||
break; // ok
|
break; // ok
|
||||||
|
case FieldType.enum:
|
||||||
|
hasValueField = true;
|
||||||
case FieldType.string:
|
case FieldType.string:
|
||||||
copy = {
|
copy = {
|
||||||
...field,
|
...field,
|
||||||
@ -150,18 +216,37 @@ export function prepareGraphableFields(
|
|||||||
|
|
||||||
if (frames.length) {
|
if (frames.length) {
|
||||||
setClassicPaletteIdxs(frames, theme, 0);
|
setClassicPaletteIdxs(frames, theme, 0);
|
||||||
|
matchEnumColorToSeriesColor(frames, theme);
|
||||||
return frames;
|
return frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const matchEnumColorToSeriesColor = (frames: DataFrame[], theme: GrafanaTheme2) => {
|
||||||
|
const { palette } = theme.visualization;
|
||||||
|
for (const frame of frames) {
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
if (field.type === FieldType.enum) {
|
||||||
|
const namedColor = palette[field.state?.seriesIndex! % palette.length];
|
||||||
|
const hexColor = theme.visualization.getColorByName(namedColor);
|
||||||
|
const enumConfig = field.config.type!.enum!;
|
||||||
|
|
||||||
|
enumConfig.color = Array(enumConfig.text!.length).fill(hexColor);
|
||||||
|
field.display = getDisplayProcessor({ field, theme });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setClassicPaletteIdxs = (frames: DataFrame[], theme: GrafanaTheme2, skipFieldIdx?: number) => {
|
const setClassicPaletteIdxs = (frames: DataFrame[], theme: GrafanaTheme2, skipFieldIdx?: number) => {
|
||||||
let seriesIndex = 0;
|
let seriesIndex = 0;
|
||||||
frames.forEach((frame) => {
|
frames.forEach((frame) => {
|
||||||
frame.fields.forEach((field, fieldIdx) => {
|
frame.fields.forEach((field, fieldIdx) => {
|
||||||
// TODO: also add FieldType.enum type here after https://github.com/grafana/grafana/pull/60491
|
if (
|
||||||
if (fieldIdx !== skipFieldIdx && (field.type === FieldType.number || field.type === FieldType.boolean)) {
|
fieldIdx !== skipFieldIdx &&
|
||||||
|
(field.type === FieldType.number || field.type === FieldType.boolean || field.type === FieldType.enum)
|
||||||
|
) {
|
||||||
field.state = {
|
field.state = {
|
||||||
...field.state,
|
...field.state,
|
||||||
seriesIndex: seriesIndex++, // TODO: skip this for fields with custom renderers (e.g. Candlestick)?
|
seriesIndex: seriesIndex++, // TODO: skip this for fields with custom renderers (e.g. Candlestick)?
|
||||||
|
Loading…
Reference in New Issue
Block a user