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 { 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);
|
||||
|
||||
|
@ -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)?
|
||||
|
Loading…
Reference in New Issue
Block a user