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:
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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)?
|
||||
|
||||
Reference in New Issue
Block a user