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
parent 4e50115d95
commit 1de35bf3c3
13 changed files with 182 additions and 61 deletions

View File

@@ -88,7 +88,11 @@ export class TimelineChart extends React.Component<TimelineProps> {
{...this.props}
fields={{
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}
propsToDiff={propsToDiff}

View File

@@ -466,6 +466,7 @@ export function prepareTimelineFields(
hasTimeseries = true;
fields.push(field);
break;
case FieldType.enum:
case FieldType.number:
if (mergeValues && field.config.color?.mode === FieldColorModeId.Thresholds) {
const f = mergeThresholdValues(field, theme);

View File

@@ -3,8 +3,8 @@ import {
DataFrame,
DisplayProcessor,
Field,
FieldType,
getDisplayProcessor,
getEnumDisplayProcessor,
GrafanaTheme2,
} 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
// users to use this panel with correct query from data sources that do not return profiles natively.
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 || [];
} else {
this.labelDisplayProcessor = (value) => ({

View File

@@ -1,7 +1,7 @@
import React, { useState, useLayoutEffect, useMemo, useRef } from 'react';
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 { ThresholdDragHandle } from './ThresholdDragHandle';
@@ -40,7 +40,7 @@ export const ThresholdControlsPlugin = ({ config, fieldConfig, onThresholdsChang
if (!thresholds) {
return null;
}
const scale = buildScaleKey(fieldConfig.defaults);
const scale = buildScaleKey(fieldConfig.defaults, FieldType.number);
const decimals = fieldConfig.defaults.decimals;
const handles = [];

View File

@@ -1,4 +1,5 @@
import {
ArrayVector,
DataFrame,
Field,
FieldType,
@@ -14,6 +15,58 @@ import { convertFieldType } from '@grafana/data/src/transformations/transformers
import { GraphFieldConfig, LineInterpolation } from '@grafana/schema';
import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold';
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
@@ -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;
const frames: DataFrame[] = [];
@@ -94,6 +158,8 @@ export function prepareGraphableFields(
fields.push(copy);
break; // ok
case FieldType.enum:
hasValueField = true;
case FieldType.string:
copy = {
...field,
@@ -150,18 +216,37 @@ export function prepareGraphableFields(
if (frames.length) {
setClassicPaletteIdxs(frames, theme, 0);
matchEnumColorToSeriesColor(frames, theme);
return frames;
}
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) => {
let seriesIndex = 0;
frames.forEach((frame) => {
frame.fields.forEach((field, fieldIdx) => {
// TODO: also add FieldType.enum type here after https://github.com/grafana/grafana/pull/60491
if (fieldIdx !== skipFieldIdx && (field.type === FieldType.number || field.type === FieldType.boolean)) {
if (
fieldIdx !== skipFieldIdx &&
(field.type === FieldType.number || field.type === FieldType.boolean || field.type === FieldType.enum)
) {
field.state = {
...field.state,
seriesIndex: seriesIndex++, // TODO: skip this for fields with custom renderers (e.g. Candlestick)?