mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
NodeGraph: Show gradient fields in legend (#34078)
* Add gradient fields to legend * Fix test * Remove unnecessary mapping * Add tests
This commit is contained in:
parent
bbaa7a9b62
commit
2e7ccf0e42
@ -1,7 +1,7 @@
|
|||||||
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
|
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use theme.vizColors.getByName
|
* @deprecated use theme.visualization.getColorByName
|
||||||
*/
|
*/
|
||||||
export function getColorForTheme(color: string, theme: GrafanaTheme): string {
|
export function getColorForTheme(color: string, theme: GrafanaTheme): string {
|
||||||
return theme.visualization.getColorByName(color);
|
return theme.visualization.getColorByName(color);
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { SeriesIcon } from './SeriesIcon';
|
||||||
|
|
||||||
|
describe('SeriesIcon', () => {
|
||||||
|
it('renders gradient correctly', () => {
|
||||||
|
const { container } = render(<SeriesIcon gradient={'continuous-GrYlRd'} />);
|
||||||
|
const div = container.firstChild! as HTMLDivElement;
|
||||||
|
// There is issue in JSDOM which means we cannot actually get the gradient value. I guess if it's empty at least
|
||||||
|
// we know it is setting some gradient instead of a single color.
|
||||||
|
// https://github.com/jsdom/jsdom/issues/2166
|
||||||
|
expect(div.style.getPropertyValue('background')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders color correctly', () => {
|
||||||
|
const { container } = render(<SeriesIcon color={'red'} />);
|
||||||
|
const div = container.firstChild! as HTMLDivElement;
|
||||||
|
expect(div.style.getPropertyValue('background')).toBe('red');
|
||||||
|
});
|
||||||
|
});
|
@ -1,20 +1,40 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
|
import { useTheme2 } from '../../themes';
|
||||||
|
import { fieldColorModeRegistry } from '@grafana/data';
|
||||||
|
|
||||||
export interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
export interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
color: string;
|
color?: string;
|
||||||
|
gradient?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SeriesIcon = React.forwardRef<HTMLDivElement, Props>(({ color, className, ...restProps }, ref) => {
|
export const SeriesIcon = React.forwardRef<HTMLDivElement, Props>(
|
||||||
const styles: CSSProperties = {
|
({ color, className, gradient, ...restProps }, ref) => {
|
||||||
backgroundColor: color,
|
const theme = useTheme2();
|
||||||
width: '14px',
|
let cssColor: string;
|
||||||
height: '4px',
|
|
||||||
borderRadius: '1px',
|
|
||||||
display: 'inline-block',
|
|
||||||
marginRight: '8px',
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div ref={ref} className={className} style={styles} {...restProps} />;
|
if (gradient) {
|
||||||
});
|
const colors = fieldColorModeRegistry.get(gradient).getColors?.(theme);
|
||||||
|
if (colors?.length) {
|
||||||
|
cssColor = `linear-gradient(90deg, ${colors.join(', ')})`;
|
||||||
|
} else {
|
||||||
|
// Not sure what to default to, this will return gray, this should not happen though.
|
||||||
|
cssColor = theme.visualization.getColorByName('');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cssColor = color!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles: CSSProperties = {
|
||||||
|
background: cssColor,
|
||||||
|
width: '14px',
|
||||||
|
height: '4px',
|
||||||
|
borderRadius: '1px',
|
||||||
|
display: 'inline-block',
|
||||||
|
marginRight: '8px',
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div ref={ref} className={className} style={styles} {...restProps} />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
SeriesIcon.displayName = 'SeriesIcon';
|
SeriesIcon.displayName = 'SeriesIcon';
|
||||||
|
@ -59,7 +59,7 @@ export const VizLegendListItem = <T extends unknown = any>({
|
|||||||
className={cx(styles.itemWrapper, className)}
|
className={cx(styles.itemWrapper, className)}
|
||||||
aria-label={selectors.components.VizLegend.seriesName(item.label)}
|
aria-label={selectors.components.VizLegend.seriesName(item.label)}
|
||||||
>
|
>
|
||||||
<VizLegendSeriesIcon seriesName={item.label} color={item.color} />
|
<VizLegendSeriesIcon seriesName={item.label} color={item.color} gradient={item.gradient} />
|
||||||
<div
|
<div
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseOut={onMouseOut}
|
onMouseOut={onMouseOut}
|
||||||
|
@ -5,13 +5,14 @@ import { SeriesIcon } from './SeriesIcon';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
seriesName: string;
|
seriesName: string;
|
||||||
color: string;
|
color?: string;
|
||||||
|
gradient?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export const VizLegendSeriesIcon: React.FunctionComponent<Props> = ({ seriesName, color }) => {
|
export const VizLegendSeriesIcon: React.FunctionComponent<Props> = ({ seriesName, color, gradient }) => {
|
||||||
const { onSeriesColorChange } = usePanelContext();
|
const { onSeriesColorChange } = usePanelContext();
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(color: string) => {
|
(color: string) => {
|
||||||
@ -20,7 +21,7 @@ export const VizLegendSeriesIcon: React.FunctionComponent<Props> = ({ seriesName
|
|||||||
[seriesName, onSeriesColorChange]
|
[seriesName, onSeriesColorChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (seriesName && onSeriesColorChange) {
|
if (seriesName && onSeriesColorChange && color) {
|
||||||
return (
|
return (
|
||||||
<SeriesColorPicker color={color} onChange={onChange} enableNamedColors>
|
<SeriesColorPicker color={color} onChange={onChange} enableNamedColors>
|
||||||
{({ ref, showColorPicker, hideColorPicker }) => (
|
{({ ref, showColorPicker, hideColorPicker }) => (
|
||||||
@ -35,7 +36,7 @@ export const VizLegendSeriesIcon: React.FunctionComponent<Props> = ({ seriesName
|
|||||||
</SeriesColorPicker>
|
</SeriesColorPicker>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <SeriesIcon color={color} />;
|
return <SeriesIcon color={color} gradient={gradient} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
VizLegendSeriesIcon.displayName = 'VizLegendSeriesIcon';
|
VizLegendSeriesIcon.displayName = 'VizLegendSeriesIcon';
|
||||||
|
@ -31,7 +31,8 @@ export interface LegendProps<T = any> extends VizLegendBaseProps<T>, VizLegendTa
|
|||||||
export interface VizLegendItem<T = any> {
|
export interface VizLegendItem<T = any> {
|
||||||
getItemKey?: () => string;
|
getItemKey?: () => string;
|
||||||
label: string;
|
label: string;
|
||||||
color: string;
|
color?: string;
|
||||||
|
gradient?: string;
|
||||||
yAxis: number;
|
yAxis: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
// displayValues?: DisplayValue[];
|
// displayValues?: DisplayValue[];
|
||||||
|
@ -26,7 +26,11 @@ export function createGraphFrames(data: TraceResponse): DataFrame[] {
|
|||||||
{ name: Fields.subTitle, type: FieldType.string },
|
{ name: Fields.subTitle, type: FieldType.string },
|
||||||
{ name: Fields.mainStat, type: FieldType.string, config: { displayName: 'Total time (% of trace)' } },
|
{ name: Fields.mainStat, type: FieldType.string, config: { displayName: 'Total time (% of trace)' } },
|
||||||
{ name: Fields.secondaryStat, type: FieldType.string, config: { displayName: 'Self time (% of total)' } },
|
{ name: Fields.secondaryStat, type: FieldType.string, config: { displayName: 'Self time (% of total)' } },
|
||||||
{ name: Fields.color, type: FieldType.number, config: { color: { mode: 'continuous-GrYlRd' } } },
|
{
|
||||||
|
name: Fields.color,
|
||||||
|
type: FieldType.number,
|
||||||
|
config: { color: { mode: 'continuous-GrYlRd' }, displayName: 'Self time / Trace duration' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
meta: {
|
meta: {
|
||||||
preferredVisualisationType: 'nodeGraph',
|
preferredVisualisationType: 'nodeGraph',
|
||||||
|
@ -44,7 +44,11 @@ export function createGraphFrames(data: DataFrame): DataFrame[] {
|
|||||||
{ name: Fields.subTitle, type: FieldType.string },
|
{ name: Fields.subTitle, type: FieldType.string },
|
||||||
{ name: Fields.mainStat, type: FieldType.string, config: { displayName: 'Total time (% of trace)' } },
|
{ name: Fields.mainStat, type: FieldType.string, config: { displayName: 'Total time (% of trace)' } },
|
||||||
{ name: Fields.secondaryStat, type: FieldType.string, config: { displayName: 'Self time (% of total)' } },
|
{ name: Fields.secondaryStat, type: FieldType.string, config: { displayName: 'Self time (% of total)' } },
|
||||||
{ name: Fields.color, type: FieldType.number, config: { color: { mode: 'continuous-GrYlRd' } } },
|
{
|
||||||
|
name: Fields.color,
|
||||||
|
type: FieldType.number,
|
||||||
|
config: { color: { mode: 'continuous-GrYlRd' }, displayName: 'Self time / Trace duration' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
meta: {
|
meta: {
|
||||||
preferredVisualisationType: 'nodeGraph',
|
preferredVisualisationType: 'nodeGraph',
|
||||||
|
30
public/app/plugins/panel/nodeGraph/Legend.test.tsx
Normal file
30
public/app/plugins/panel/nodeGraph/Legend.test.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { FieldColorModeId } from '@grafana/data';
|
||||||
|
import { Legend } from './Legend';
|
||||||
|
import { NodeDatum } from './types';
|
||||||
|
|
||||||
|
describe('Legend', () => {
|
||||||
|
it('renders ok without nodes', () => {
|
||||||
|
render(<Legend nodes={[]} onSort={(sort) => {}} sortable={false} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders ok with color fields', () => {
|
||||||
|
const nodes: NodeDatum[] = [
|
||||||
|
{
|
||||||
|
id: 'nodeId',
|
||||||
|
mainStat: { config: { displayName: 'stat1' } } as any,
|
||||||
|
secondaryStat: { config: { displayName: 'stat2' } } as any,
|
||||||
|
arcSections: [
|
||||||
|
{ config: { displayName: 'error', color: { mode: FieldColorModeId.Fixed, fixedColor: 'red' } } } as any,
|
||||||
|
],
|
||||||
|
} as any,
|
||||||
|
];
|
||||||
|
render(<Legend nodes={nodes} onSort={(sort) => {}} sortable={false} />);
|
||||||
|
const items = screen.getAllByLabelText(/VizLegend series/);
|
||||||
|
expect(items.length).toBe(3);
|
||||||
|
|
||||||
|
const item = screen.getByLabelText(/VizLegend series error/);
|
||||||
|
expect((item.firstChild as HTMLDivElement).style.getPropertyValue('background')).toBe('rgb(242, 73, 92)');
|
||||||
|
});
|
||||||
|
});
|
@ -62,6 +62,9 @@ interface ItemData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getColorLegendItems(nodes: NodeDatum[], theme: GrafanaTheme): Array<VizLegendItem<ItemData>> {
|
function getColorLegendItems(nodes: NodeDatum[], theme: GrafanaTheme): Array<VizLegendItem<ItemData>> {
|
||||||
|
if (!nodes.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const fields = [nodes[0].mainStat, nodes[0].secondaryStat].filter(identity) as Field[];
|
const fields = [nodes[0].mainStat, nodes[0].secondaryStat].filter(identity) as Field[];
|
||||||
|
|
||||||
const node = nodes.find((n) => n.arcSections.length > 0);
|
const node = nodes.find((n) => n.arcSections.length > 0);
|
||||||
@ -71,18 +74,30 @@ function getColorLegendItems(nodes: NodeDatum[], theme: GrafanaTheme): Array<Viz
|
|||||||
|
|
||||||
// Lets collect and deduplicate as there isn't a requirement for 0 size arc section to be defined
|
// Lets collect and deduplicate as there isn't a requirement for 0 size arc section to be defined
|
||||||
fields.push(...new Set(nodes.map((n) => n.arcSections).flat()));
|
fields.push(...new Set(nodes.map((n) => n.arcSections).flat()));
|
||||||
} else {
|
|
||||||
// TODO: probably some sort of gradient which we will have to deal with later
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nodes[0].color) {
|
||||||
|
fields.push(nodes[0].color);
|
||||||
|
}
|
||||||
|
|
||||||
return fields.map((f) => {
|
return fields.map((f) => {
|
||||||
return {
|
const item: VizLegendItem = {
|
||||||
label: f.config.displayName || f.name,
|
label: f.config.displayName || f.name,
|
||||||
color: getColorForTheme(f.config.color?.fixedColor || '', theme),
|
|
||||||
yAxis: 0,
|
yAxis: 0,
|
||||||
data: { field: f },
|
data: { field: f },
|
||||||
};
|
};
|
||||||
|
if (f.config.color?.mode === FieldColorModeId.Fixed && f.config.color?.fixedColor) {
|
||||||
|
item.color = getColorForTheme(f.config.color?.fixedColor || '', theme);
|
||||||
|
} else if (f.config.color?.mode) {
|
||||||
|
item.gradient = f.config.color?.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(item.color || item.gradient)) {
|
||||||
|
// Defaults to gray color
|
||||||
|
item.color = getColorForTheme('', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { MouseEvent, memo } from 'react';
|
import React, { MouseEvent, memo } from 'react';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { getColorForTheme, GrafanaTheme2 } from '@grafana/data';
|
import { Field, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { useStyles2, useTheme } from '@grafana/ui';
|
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
import { NodeDatum } from './types';
|
import { NodeDatum } from './types';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
@ -117,14 +117,14 @@ export const Node = memo(function Node(props: {
|
|||||||
function ColorCircle(props: { node: NodeDatum }) {
|
function ColorCircle(props: { node: NodeDatum }) {
|
||||||
const { node } = props;
|
const { node } = props;
|
||||||
const fullStat = node.arcSections.find((s) => s.values.get(node.dataFrameRowIndex) === 1);
|
const fullStat = node.arcSections.find((s) => s.values.get(node.dataFrameRowIndex) === 1);
|
||||||
const theme = useTheme();
|
const theme = useTheme2();
|
||||||
|
|
||||||
if (fullStat) {
|
if (fullStat) {
|
||||||
// Doing arc with path does not work well so it's better to just do a circle in that case
|
// Doing arc with path does not work well so it's better to just do a circle in that case
|
||||||
return (
|
return (
|
||||||
<circle
|
<circle
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={getColorForTheme(fullStat.config.color?.fixedColor || '', theme)}
|
stroke={theme.visualization.getColorByName(fullStat.config.color?.fixedColor || '')}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
r={nodeR}
|
r={nodeR}
|
||||||
cx={node.x}
|
cx={node.x}
|
||||||
@ -136,7 +136,16 @@ function ColorCircle(props: { node: NodeDatum }) {
|
|||||||
const nonZero = node.arcSections.filter((s) => s.values.get(node.dataFrameRowIndex) !== 0);
|
const nonZero = node.arcSections.filter((s) => s.values.get(node.dataFrameRowIndex) !== 0);
|
||||||
if (nonZero.length === 0) {
|
if (nonZero.length === 0) {
|
||||||
// Fallback if no arc is defined
|
// Fallback if no arc is defined
|
||||||
return <circle fill="none" stroke={node.color} strokeWidth={2} r={nodeR} cx={node.x} cy={node.y} />;
|
return (
|
||||||
|
<circle
|
||||||
|
fill="none"
|
||||||
|
stroke={node.color ? getColor(node.color, node.dataFrameRowIndex, theme) : 'gray'}
|
||||||
|
strokeWidth={2}
|
||||||
|
r={nodeR}
|
||||||
|
cx={node.x}
|
||||||
|
cy={node.y}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { elements } = nonZero.reduce(
|
const { elements } = nonZero.reduce(
|
||||||
@ -151,7 +160,7 @@ function ColorCircle(props: { node: NodeDatum }) {
|
|||||||
y={node.y!}
|
y={node.y!}
|
||||||
startPercent={acc.percent}
|
startPercent={acc.percent}
|
||||||
percent={value}
|
percent={value}
|
||||||
color={getColorForTheme(color, theme)}
|
color={theme.visualization.getColorByName(color)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -197,3 +206,11 @@ function ArcSection({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getColor(field: Field, index: number, theme: GrafanaTheme2): string {
|
||||||
|
if (!field.config.color) {
|
||||||
|
return field.values.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFieldColorModeForField(field).getCalculator(field, theme)(0, field.values.get(index));
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ export type NodeDatum = SimulationNodeDatum & {
|
|||||||
mainStat?: Field;
|
mainStat?: Field;
|
||||||
secondaryStat?: Field;
|
secondaryStat?: Field;
|
||||||
arcSections: Field[];
|
arcSections: Field[];
|
||||||
color: string;
|
color?: Field;
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is the data we have before the graph is laid out with source and target being string IDs.
|
// This is the data we have before the graph is laid out with source and target being string IDs.
|
||||||
|
@ -19,6 +19,18 @@ describe('processNodes', () => {
|
|||||||
theme
|
theme
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const colorField = {
|
||||||
|
config: {
|
||||||
|
color: {
|
||||||
|
mode: 'continuous-GrYlRd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
index: 7,
|
||||||
|
name: 'color',
|
||||||
|
type: 'number',
|
||||||
|
values: new ArrayVector([0.5, 0.5, 0.5]),
|
||||||
|
};
|
||||||
|
|
||||||
expect(nodes).toEqual([
|
expect(nodes).toEqual([
|
||||||
{
|
{
|
||||||
arcSections: [
|
arcSections: [
|
||||||
@ -43,7 +55,7 @@ describe('processNodes', () => {
|
|||||||
values: new ArrayVector([0.5, 0.5, 0.5]),
|
values: new ArrayVector([0.5, 0.5, 0.5]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
color: 'rgb(226, 192, 61)',
|
color: colorField,
|
||||||
dataFrameRowIndex: 0,
|
dataFrameRowIndex: 0,
|
||||||
id: '0',
|
id: '0',
|
||||||
incoming: 0,
|
incoming: 0,
|
||||||
@ -87,7 +99,7 @@ describe('processNodes', () => {
|
|||||||
values: new ArrayVector([0.5, 0.5, 0.5]),
|
values: new ArrayVector([0.5, 0.5, 0.5]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
color: 'rgb(226, 192, 61)',
|
color: colorField,
|
||||||
dataFrameRowIndex: 1,
|
dataFrameRowIndex: 1,
|
||||||
id: '1',
|
id: '1',
|
||||||
incoming: 1,
|
incoming: 1,
|
||||||
@ -131,7 +143,7 @@ describe('processNodes', () => {
|
|||||||
values: new ArrayVector([0.5, 0.5, 0.5]),
|
values: new ArrayVector([0.5, 0.5, 0.5]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
color: 'rgb(226, 192, 61)',
|
color: colorField,
|
||||||
dataFrameRowIndex: 2,
|
dataFrameRowIndex: 2,
|
||||||
id: '2',
|
id: '2',
|
||||||
incoming: 2,
|
incoming: 2,
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
Field,
|
Field,
|
||||||
FieldCache,
|
FieldCache,
|
||||||
FieldType,
|
FieldType,
|
||||||
getFieldColorModeForField,
|
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
NodeGraphDataFrameFieldNames,
|
NodeGraphDataFrameFieldNames,
|
||||||
@ -100,7 +99,7 @@ export function processNodes(
|
|||||||
mainStat: nodeFields.mainStat,
|
mainStat: nodeFields.mainStat,
|
||||||
secondaryStat: nodeFields.secondaryStat,
|
secondaryStat: nodeFields.secondaryStat,
|
||||||
arcSections: nodeFields.arc,
|
arcSections: nodeFields.arc,
|
||||||
color: nodeFields.color ? getColor(nodeFields.color, index, theme) : '',
|
color: nodeFields.color,
|
||||||
};
|
};
|
||||||
return acc;
|
return acc;
|
||||||
}, {}) || {};
|
}, {}) || {};
|
||||||
@ -271,14 +270,6 @@ function edgesFrame() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColor(field: Field, index: number, theme: GrafanaTheme2): string {
|
|
||||||
if (!field.config.color) {
|
|
||||||
return field.values.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getFieldColorModeForField(field).getCalculator(field, theme)(0, field.values.get(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Bounds {
|
export interface Bounds {
|
||||||
top: number;
|
top: number;
|
||||||
right: number;
|
right: number;
|
||||||
|
Loading…
Reference in New Issue
Block a user