Compare commits

...

14 Commits

Author SHA1 Message Date
Leon Sorokin
b104c045db cremate graveyard legend 2024-11-25 14:07:17 -06:00
Leon Sorokin
26025c1049 fix candlestick suggestions 2024-11-25 13:57:55 -06:00
Leon Sorokin
fed7ccc69b fix candlestick 2024-11-25 13:53:07 -06:00
Leon Sorokin
663614f3c6 fix test 2024-11-25 13:40:57 -06:00
Leon Sorokin
af6be4cbf3 clarify comment 2024-11-25 13:29:18 -06:00
Leon Sorokin
b79ba4dc07 fix Histogram 2024-11-25 13:26:47 -06:00
Leon Sorokin
72c8e13bf6 fixes 2024-11-25 12:52:50 -06:00
Leon Sorokin
08f18b0cae wip 2024-11-25 11:46:18 -06:00
Leon Sorokin
87100e72d6 better 2024-11-25 09:34:41 -06:00
Leon Sorokin
74badbf831 progress 2024-11-25 07:34:33 -06:00
Leon Sorokin
5b7af0a9ea Merge branch 'main' into kristina/hidden-fields-links 2024-11-23 00:49:13 -06:00
Kristina Durivage
2513594a95 remove unneeded decoupling, filter fields when there are no links 2024-10-21 20:26:01 -05:00
Kristina Durivage
3408ad436e make hidefrom logic more robust 2024-10-21 17:00:36 -05:00
Kristina Durivage
bcabff77df Use hideFrom in GraphNG to hide fields if value is defined. Remove other filtering from timeline chart 2024-10-21 17:00:36 -05:00
20 changed files with 155 additions and 73 deletions

View File

@@ -62,11 +62,32 @@ export function cacheFieldDisplayNames(frames: DataFrame[]) {
/**
*
* moves each field's config.custom.hideFrom to field.state.hideFrom
* and mutates orgiginal field.config.custom.hideFrom to one with explicit overrides only, (without the ad-hoc stateful __system override from legend toggle)
* and sets field.config.custom.hideFrom to one with explicit overrides only, (without the ad-hoc stateful __system override from legend toggle)
*/
export function decoupleHideFromState(frames: DataFrame[], fieldConfig: FieldConfigSource) {
frames.forEach((frame) => {
frame.fields.forEach((field) => {
return frames.map((frame) => {
const frameCopy: DataFrame = { ...frame };
frameCopy.fields = frame.fields.map((field) => {
const fieldCopy: Field = {
...field,
state: {
...field.state,
hideFrom: {
...(field.state?.hideFrom ?? { legend: false, tooltip: false, viz: false }),
},
},
config: {
...field.config,
custom: {
...field.config.custom,
hideFrom: {
...field.config.custom?.hideFrom,
},
},
},
};
const hideFrom = {
legend: false,
tooltip: false,
@@ -75,7 +96,7 @@ export function decoupleHideFromState(frames: DataFrame[], fieldConfig: FieldCon
};
// with ad hoc __system override applied
const hideFromState = field.config.custom?.hideFrom;
const hideFromState = fieldCopy.config.custom?.hideFrom;
fieldConfig.overrides.forEach((o) => {
if ('__systemRef' in o) {
@@ -93,16 +114,20 @@ export function decoupleHideFromState(frames: DataFrame[], fieldConfig: FieldCon
}
});
field.state = {
...field.state,
fieldCopy.state = {
...fieldCopy.state,
hideFrom: {
...hideFromState,
},
};
// original with perm overrides
field.config.custom.hideFrom = hideFrom;
fieldCopy.config.custom.hideFrom = hideFrom;
return fieldCopy;
});
return frameCopy;
});
}

View File

@@ -1,6 +1,6 @@
import { memo } from 'react';
import { DataFrame, getFieldDisplayName, getFieldSeriesColor } from '@grafana/data';
import { DataFrame, getFieldSeriesColor } from '@grafana/data';
import { VizLegendOptions, AxisPlacement } from '@grafana/schema';
import { useTheme2 } from '../../themes';
@@ -12,7 +12,7 @@ import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
import { getDisplayValuesForCalcs } from './utils';
interface PlotLegendProps extends VizLegendOptions, Omit<VizLayoutLegendProps, 'children'> {
data: DataFrame[];
frame: DataFrame;
config: UPlotConfigBuilder;
}
@@ -40,41 +40,60 @@ export function hasVisibleLegendSeries(config: UPlotConfigBuilder, data: DataFra
}
export const PlotLegend = memo(
({ data, config, placement, calcs, displayMode, ...vizLayoutLegendProps }: PlotLegendProps) => {
({ frame, config, placement, calcs, displayMode, ...vizLayoutLegendProps }: PlotLegendProps) => {
const theme = useTheme2();
const legendItems = config
.getSeries()
.map<VizLegendItem | undefined>((s) => {
const seriesConfig = s.props;
const fieldIndex = seriesConfig.dataFrameFieldIndex;
const axisPlacement = config.getAxisPlacement(s.props.scaleKey);
if (!fieldIndex) {
const cfgSeries = config.getSeries();
const legendItems: VizLegendItem[] = frame.fields
.map((field, i) => {
if (i === 0 || field.config.custom?.hideFrom.legend) {
return undefined;
}
const field = data[fieldIndex.frameIndex]?.fields[fieldIndex.fieldIndex];
const dataFrameFieldIndex = field.state?.origin!;
if (!field || field.config.custom?.hideFrom?.legend) {
return undefined;
const seriesConfig = cfgSeries.find(({ props }) => {
const { dataFrameFieldIndex: dataFrameFieldIndexCfg } = props;
return (
dataFrameFieldIndexCfg?.frameIndex === dataFrameFieldIndex.frameIndex &&
dataFrameFieldIndexCfg?.fieldIndex === dataFrameFieldIndex.fieldIndex
);
});
let axisPlacement = AxisPlacement.Left;
// there is a bit of a bug here. since we no longer add hidden fields to the uplot config
// we cannot determine "auto" axis placement of hidden series
// we can fix this in future by decoupling some things
if (seriesConfig != null) {
axisPlacement = config.getAxisPlacement(seriesConfig.props.scaleKey);
} else {
let fieldAxisPlacement = field.config.custom?.axisPlacement;
// respect explicit non-auto placement
if (fieldAxisPlacement !== AxisPlacement.Auto) {
fieldAxisPlacement = fieldAxisPlacement;
}
}
const label = getFieldDisplayName(field, data[fieldIndex.frameIndex]!, data);
const label = field.state?.displayName ?? field.name;
const scaleColor = getFieldSeriesColor(field, theme);
const seriesColor = scaleColor.color;
return {
disabled: !(seriesConfig.show ?? true),
fieldIndex,
disabled: field.state?.hideFrom?.viz,
fieldIndex: dataFrameFieldIndex,
color: seriesColor,
label,
yAxis: axisPlacement === AxisPlacement.Left || axisPlacement === AxisPlacement.Bottom ? 1 : 2,
getDisplayValues: () => getDisplayValuesForCalcs(calcs, field, theme),
getItemKey: () => `${label}-${fieldIndex.frameIndex}-${fieldIndex.fieldIndex}`,
lineStyle: seriesConfig.lineStyle,
getItemKey: () => `${label}-${dataFrameFieldIndex.frameIndex}-${dataFrameFieldIndex.fieldIndex}`,
lineStyle: field.config.custom.lineStyle,
};
})
.filter((i): i is VizLegendItem => i !== undefined);
.filter((item) => item !== undefined);
return (
<VizLayout.Legend placement={placement} {...vizLayoutLegendProps}>

View File

@@ -4,7 +4,6 @@ import * as React from 'react';
import { DataFrame, TimeRange } from '@grafana/data';
import { PanelContextRoot } from '../../components/PanelChrome/PanelContext';
import { hasVisibleLegendSeries, PlotLegend } from '../../components/uPlot/PlotLegend';
import { UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder';
import { withTheme2 } from '../../themes/ThemeContext';
import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG';
@@ -37,13 +36,7 @@ export class UnthemedTimeSeries extends Component<TimeSeriesProps> {
};
renderLegend = (config: UPlotConfigBuilder) => {
const { legend, frames } = this.props;
if (!config || (legend && !legend.showLegend) || !hasVisibleLegendSeries(config, frames)) {
return null;
}
return <PlotLegend data={frames} config={config} {...legend} />;
return null;
};
render() {

View File

@@ -47,7 +47,7 @@ export interface GraphNGProps extends Themeable2 {
prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder;
propsToDiff?: Array<string | PropDiffFn>;
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null;
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null;
renderLegend: (config: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactElement | null;
replaceVariables: InterpolateFunction;
dataLinkPostProcessor?: DataLinkPostProcessor;
cursorSync?: DashboardCursorSync;
@@ -83,6 +83,9 @@ function sameProps<T extends Record<string, unknown>>(
* @internal -- not a public API
*/
export interface GraphNGState {
// includes fields hidden from viz
alignedFrameLegend: DataFrame;
// excludes fields hidden from viz
alignedFrame: DataFrame;
alignedData?: AlignedData;
config?: UPlotConfigBuilder;
@@ -175,6 +178,17 @@ export class GraphNG extends Component<GraphNGProps, GraphNGState> {
};
}
const alignedFrameLegend = alignedFrameFinal;
const nonHiddenFields = alignedFrameFinal.fields.filter(
(field, i) => i === 0 || (!field.config.custom?.hideFrom?.viz && !field.state?.hideFrom?.viz)
);
alignedFrameFinal = {
...alignedFrameFinal,
fields: nonHiddenFields,
length: nonHiddenFields.length,
};
let config = this.state?.config;
if (withConfig) {
@@ -184,6 +198,7 @@ export class GraphNG extends Component<GraphNGProps, GraphNGState> {
state = {
alignedFrame: alignedFrameFinal,
alignedFrameLegend,
config,
};
@@ -229,14 +244,14 @@ export class GraphNG extends Component<GraphNGProps, GraphNGState> {
render() {
const { width, height, children, renderLegend } = this.props;
const { config, alignedFrame, alignedData } = this.state;
const { config, alignedFrame, alignedFrameLegend, alignedData } = this.state;
if (!config) {
return null;
}
return (
<VizLayout width={width} height={height} legend={renderLegend(config)}>
<VizLayout width={width} height={height} legend={renderLegend(config, alignedFrameLegend)}>
{(vizWidth: number, vizHeight: number) => (
<UPlotChart
config={config}

View File

@@ -31,14 +31,14 @@ export class UnthemedTimeSeries extends Component<TimeSeriesProps> {
});
};
renderLegend = (config: UPlotConfigBuilder) => {
renderLegend = (config: UPlotConfigBuilder, alignedFrame: DataFrame) => {
const { legend, frames } = this.props;
if (!config || (legend && !legend.showLegend) || !hasVisibleLegendSeries(config, frames)) {
return null;
}
return <PlotLegend data={frames} config={config} {...legend} />;
return <PlotLegend frame={alignedFrame} config={config} {...legend} />;
};
render() {

View File

@@ -210,7 +210,9 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
const customConfig: GraphFieldConfig = config.custom!;
if (field === xField || (field.type !== FieldType.number && field.type !== FieldType.enum)) {
const isHidden = config.custom?.hideFrom?.viz || field.state?.hideFrom?.viz;
if (field === xField || isHidden || (field.type !== FieldType.number && field.type !== FieldType.enum)) {
continue;
}
@@ -497,7 +499,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
barMaxWidth: customConfig.barMaxWidth,
pointSize: customConfig.pointSize,
spanNulls: customConfig.spanNulls || false,
show: !customConfig.hideFrom?.viz,
gradientMode: customConfig.gradientMode,
thresholds: config.thresholds,
hardMin: field.config.min,

View File

@@ -375,9 +375,6 @@ export function prepareTimelineFields(
const fields: Field[] = [];
for (let field of frame.fields) {
if (field.config.custom?.hideFrom?.viz) {
continue;
}
switch (field.type) {
case FieldType.time:
isTimeseries = true;

View File

@@ -62,7 +62,7 @@ export function prepSeries(
}
cacheFieldDisplayNames(frames);
decoupleHideFromState(frames, fieldConfig);
frames = decoupleHideFromState(frames, fieldConfig);
let frame: DataFrame | undefined = { ...frames[0] };

View File

@@ -59,8 +59,8 @@ export const CandlestickPanel = ({
const theme = useTheme2();
const info = useMemo(() => {
return prepareCandlestickFields(data.series, options, theme, timeRange);
}, [data.series, options, theme, timeRange]);
return prepareCandlestickFields(data.series, fieldConfig, options, theme, timeRange);
}, [data.series, fieldConfig, options, theme, timeRange]);
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);

View File

@@ -1,9 +1,10 @@
import { createTheme, toDataFrame } from '@grafana/data';
import { createTheme, FieldConfigSource, toDataFrame } from '@grafana/data';
import { prepareCandlestickFields } from './fields';
import { Options, VizDisplayMode } from './types';
const theme = createTheme();
const fieldConfig: FieldConfigSource = { defaults: {}, overrides: [] };
describe('Candlestick data', () => {
const options = {} as Options;
@@ -21,6 +22,7 @@ describe('Candlestick data', () => {
],
}),
],
fieldConfig,
options,
theme
);
@@ -40,6 +42,7 @@ describe('Candlestick data', () => {
],
}),
],
fieldConfig,
options,
theme
);
@@ -70,6 +73,7 @@ describe('Candlestick data', () => {
],
}),
],
fieldConfig,
options,
theme
)!;
@@ -113,6 +117,7 @@ describe('Candlestick data', () => {
],
}),
],
fieldConfig,
options,
theme
)!;
@@ -162,6 +167,7 @@ describe('Candlestick data', () => {
],
}),
],
fieldConfig,
options,
theme
)!;
@@ -224,6 +230,7 @@ describe('Candlestick data', () => {
],
}),
],
fieldConfig,
options,
theme
)!;
@@ -278,6 +285,7 @@ describe('Candlestick data', () => {
],
}),
],
fieldConfig,
options,
theme
)!;

View File

@@ -1,6 +1,7 @@
import {
DataFrame,
Field,
FieldConfigSource,
FieldType,
getFieldDisplayName,
GrafanaTheme2,
@@ -96,6 +97,7 @@ function findFieldOrAuto(frame: DataFrame, info: FieldPickerInfo, options: Candl
export function prepareCandlestickFields(
series: DataFrame[] | undefined,
fieldConfig: FieldConfigSource,
options: Partial<Options>,
theme: GrafanaTheme2,
timeRange?: TimeRange
@@ -120,7 +122,7 @@ export function prepareCandlestickFields(
const data: CandlestickData = { aligned, frame: aligned, names: {} };
// Apply same filter as everything else in timeseries
const timeSeriesFrames = prepareGraphableFields([aligned], theme, timeRange);
const timeSeriesFrames = prepareGraphableFields([aligned], fieldConfig, theme, timeRange);
if (!timeSeriesFrames) {
return null;
}

View File

@@ -71,7 +71,7 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(CandlestickPane
.useFieldConfig(getGraphFieldConfig(defaultGraphConfig))
.setPanelOptions((builder, context) => {
const opts = context.options ?? defaultOptions;
const info = prepareCandlestickFields(context.data, opts, config.theme2);
const info = prepareCandlestickFields(context.data, { defaults: {}, overrides: [] }, opts, config.theme2);
builder
.addRadio({

View File

@@ -19,7 +19,12 @@ export class CandlestickSuggestionsSupplier {
return;
}
const info = prepareCandlestickFields(builder.data.series, defaultOptions, config.theme2);
const info = prepareCandlestickFields(
builder.data.series,
{ defaults: {}, overrides: [] },
defaultOptions,
config.theme2
);
if (!info) {
return;
}

View File

@@ -45,7 +45,6 @@ export interface HistogramProps extends Themeable2 {
height: number;
structureRev?: number; // a number that will change when the frames[] structure changes
legend: VizLegendOptions;
rawSeries?: DataFrame[];
children?: (builder: UPlotConfigBuilder, frame: DataFrame, xMinOnlyFrame: DataFrame) => React.ReactNode;
}
@@ -280,8 +279,11 @@ const preparePlotData = (builder: UPlotConfigBuilder, xMinOnlyFrame: DataFrame)
};
interface State {
alignedData: AlignedData;
// includes fields hidden from viz, but excludes xMin/xMax
alignedFrameLegend: DataFrame;
// excludes fields hidden from viz
alignedFrame: DataFrame;
alignedData: AlignedData;
config?: UPlotConfigBuilder;
xMinOnlyFrame: DataFrame;
}
@@ -299,24 +301,30 @@ export class Histogram extends React.Component<HistogramProps, State> {
const xMinOnly = xMinOnlyFrame(alignedFrame);
const alignedData = preparePlotData(config, xMinOnly);
let alignedFrameLegend = {
...alignedFrame,
fields: alignedFrame.fields.filter((field) => field.name !== 'xMin' && field.name !== 'xMax'),
};
// console.log(alignedFrame.fields);
return {
alignedFrame,
alignedFrameLegend,
alignedData,
config,
xMinOnlyFrame: xMinOnly,
};
}
renderLegend(config: UPlotConfigBuilder) {
renderLegend(config: UPlotConfigBuilder, alignedFrame: DataFrame) {
const { legend } = this.props;
if (!config || legend.showLegend === false) {
return null;
}
const frames = this.props.options.combine ? [this.props.alignedFrame] : this.props.rawSeries!;
return <PlotLegend data={frames} config={config} maxHeight="35%" maxWidth="60%" {...legend} />;
return <PlotLegend frame={alignedFrame} config={config} maxHeight="35%" maxWidth="60%" {...legend} />;
}
componentDidUpdate(prevProps: HistogramProps) {
@@ -339,15 +347,15 @@ export class Histogram extends React.Component<HistogramProps, State> {
}
render() {
const { width, height, children, alignedFrame } = this.props;
const { config } = this.state;
const { width, height, children } = this.props;
const { config, alignedFrame, alignedFrameLegend } = this.state;
if (!config) {
return null;
}
return (
<VizLayout width={width} height={height} legend={this.renderLegend(config)}>
<VizLayout width={width} height={height} legend={this.renderLegend(config, alignedFrameLegend)}>
{(vizWidth: number, vizHeight: number) => (
<UPlotChart config={this.state.config!} data={this.state.alignedData} width={vizWidth} height={vizHeight}>
{children ? children(config, alignedFrame, this.state.xMinOnlyFrame) : null}

View File

@@ -63,7 +63,6 @@ export const HistogramPanel = ({ data, options, width, height }: Props) => {
options={options}
theme={theme}
legend={options.legend}
rawSeries={data.series}
structureRev={data.structureRev}
width={width}
height={height}

View File

@@ -44,7 +44,11 @@ export const TimeSeriesPanel = ({
// Vertical orientation is not available for users through config.
// It is simplified version of horizontal time series panel and it does not support all plugins.
const isVerticallyOriented = options.orientation === VizOrientation.Vertical;
const frames = useMemo(() => prepareGraphableFields(data.series, config.theme2, timeRange), [data.series, timeRange]);
const frames = useMemo(
() => prepareGraphableFields(data.series, fieldConfig, config.theme2, timeRange),
[data.series, fieldConfig, timeRange]
);
const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]);
const suggestions = useMemo(() => {
if (frames?.length && frames.every((df) => df.meta?.type === DataFrameType.TimeSeriesLong)) {

View File

@@ -1,7 +1,9 @@
import { createTheme, FieldType, createDataFrame, toDataFrame } from '@grafana/data';
import { createTheme, FieldType, createDataFrame, toDataFrame, FieldConfigSource } from '@grafana/data';
import { prepareGraphableFields } from './utils';
const fieldConfig: FieldConfigSource = { defaults: {}, overrides: [] };
describe('prepare timeseries graph', () => {
it('errors with no time fields', () => {
const input = [
@@ -12,7 +14,7 @@ describe('prepare timeseries graph', () => {
],
}),
];
const frames = prepareGraphableFields(input, createTheme());
const frames = prepareGraphableFields(input, fieldConfig, createTheme());
expect(frames).toBeNull();
});
@@ -25,7 +27,7 @@ describe('prepare timeseries graph', () => {
],
}),
];
const frames = prepareGraphableFields(input, createTheme());
const frames = prepareGraphableFields(input, fieldConfig, createTheme());
expect(frames).toBeNull();
});
@@ -41,7 +43,7 @@ describe('prepare timeseries graph', () => {
],
}),
];
const frames = prepareGraphableFields(input, createTheme());
const frames = prepareGraphableFields(input, fieldConfig, createTheme());
expect(frames![0].fields.map((f) => f.state?.seriesIndex)).toEqual([undefined, undefined, 0, undefined, 1]);
});
@@ -56,7 +58,7 @@ describe('prepare timeseries graph', () => {
],
}),
];
const frames = prepareGraphableFields(input, createTheme());
const frames = prepareGraphableFields(input, fieldConfig, createTheme());
const out = frames![0];
expect(out.fields.map((f) => f.name)).toEqual(['a', 'b', 'c', 'd']);
@@ -82,7 +84,7 @@ describe('prepare timeseries graph', () => {
{ name: 'a', values: [-10, NaN, 10, -Infinity, +Infinity] },
],
});
const frames = prepareGraphableFields([df], createTheme());
const frames = prepareGraphableFields([df], fieldConfig, createTheme());
const field = frames![0].fields.find((f) => f.name === 'a');
expect(field!.values).toMatchInlineSnapshot(`
@@ -103,7 +105,7 @@ describe('prepare timeseries graph', () => {
{ name: 'a', values: [1, 2, 3] },
],
});
const frames = prepareGraphableFields([df], createTheme());
const frames = prepareGraphableFields([df], fieldConfig, createTheme());
const field = frames![0].fields.find((f) => f.name === 'a');
expect(field!.values).toMatchInlineSnapshot(`
@@ -127,7 +129,7 @@ describe('prepare timeseries graph', () => {
{ name: 'a', config: { noValue: '20' }, values: [1, 2, 3] },
],
});
const frames = prepareGraphableFields([df], createTheme());
const frames = prepareGraphableFields([df], fieldConfig, createTheme());
const field = frames![0].fields.find((f) => f.name === 'a');
expect(field!.values).toMatchInlineSnapshot(`

View File

@@ -7,7 +7,9 @@ import {
isBooleanUnit,
TimeRange,
cacheFieldDisplayNames,
FieldConfigSource,
} from '@grafana/data';
import { decoupleHideFromState } from '@grafana/data/src/field/fieldState';
import { convertFieldType } from '@grafana/data/src/transformations/transformers/convertFieldType';
import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold';
import { nullToValue } from '@grafana/data/src/transformations/transformers/nulls/nullToValue';
@@ -72,6 +74,7 @@ function reEnumFields(frames: DataFrame[]): DataFrame[] {
*/
export function prepareGraphableFields(
series: DataFrame[],
fieldConfig: FieldConfigSource,
theme: GrafanaTheme2,
timeRange?: TimeRange,
// numeric X requires a single frame where the first field is numeric
@@ -82,6 +85,7 @@ export function prepareGraphableFields(
}
cacheFieldDisplayNames(series);
series = decoupleHideFromState(series, fieldConfig);
let useNumericX = xNumFieldIdx != null;

View File

@@ -80,8 +80,8 @@ export const TrendPanel = ({
}
}
return { frames: prepareGraphableFields(frames, config.theme2, undefined, xFieldIdx) };
}, [data.series, options.xField]);
return { frames: prepareGraphableFields(frames, fieldConfig, config.theme2, undefined, xFieldIdx) };
}, [data.series, fieldConfig, options.xField]);
if (info.warning || !info.frames) {
return (

View File

@@ -43,7 +43,7 @@ export function prepSeries(
fieldConfig: FieldConfigSource
) {
cacheFieldDisplayNames(frames);
decoupleHideFromState(frames, fieldConfig);
frames = decoupleHideFromState(frames, fieldConfig);
let series: XYSeries[] = [];