MarketTrend: aggressive default field matching (#41574)

This commit is contained in:
Ryan McKinley 2021-11-11 22:04:37 -08:00 committed by GitHub
parent 7a92faf398
commit 7ffdff9e1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 458 additions and 144 deletions

View File

@ -149,13 +149,7 @@
"down": "red",
"up": "green"
},
"fieldMap": {
"close": "close",
"high": "high",
"low": "low",
"open": "open",
"volume": "volume"
},
"fields": {},
"legend": {
"calcs": [],
"displayMode": "list",
@ -238,12 +232,7 @@
"down": "red",
"up": "green"
},
"fieldMap": {
"close": "close",
"high": "high",
"low": "low",
"open": "open"
},
"fields": {},
"legend": {
"calcs": [],
"displayMode": "list",
@ -342,12 +331,7 @@
"down": "red",
"up": "blue"
},
"fieldMap": {
"close": "close",
"high": "high",
"low": "low",
"open": "open"
},
"fields": { },
"legend": {
"calcs": [],
"displayMode": "list",
@ -479,11 +463,7 @@
"down": "red",
"up": "yellow"
},
"fieldMap": {
"close": "close",
"open": "open",
"volume": "volume"
},
"fields": {},
"legend": {
"calcs": [],
"displayMode": "list",

View File

@ -1,4 +1,3 @@
import { ComponentType } from 'react';
import {
DataLink,
Field,
@ -171,11 +170,6 @@ export interface StatsPickerConfigSettings {
defaultStat?: string;
}
interface FieldNamePickerInfoProps {
name?: string;
field?: Field;
}
export interface FieldNamePickerConfigSettings {
/**
* Function is a predicate, to test each element of the array.
@ -188,13 +182,7 @@ export interface FieldNamePickerConfigSettings {
*/
noFieldsMessage?: string;
/**
* When a field is selected, this component can show aditional
* information, including validation etc
*/
info?: ComponentType<FieldNamePickerInfoProps> | null;
/**
/**addFieldNamePicker
* Sets the width to a pixel value.
*/
width?: number;

View File

@ -15,11 +15,11 @@ export const FieldNamePicker: React.FC<StandardEditorProps<string, FieldNamePick
const selectOptions = useSelectOptions(names, value);
const onSelectChange = useCallback(
(selection: SelectableValue<string>) => {
if (!frameHasName(selection.value, names)) {
return;
(selection?: SelectableValue<string>) => {
if (selection && !frameHasName(selection.value, names)) {
return; // can not select name that does not exist?
}
return onChange(selection.value!);
return onChange(selection?.value);
},
[names, onChange]
);
@ -35,8 +35,8 @@ export const FieldNamePicker: React.FC<StandardEditorProps<string, FieldNamePick
onChange={onSelectChange}
noOptionsMessage={settings.noFieldsMessage}
width={settings.width}
isClearable={true}
/>
{settings.info && <settings.info name={value} field={names.fields.get(value)} />}
</>
);
};

View File

@ -2,14 +2,13 @@
// with some extra renderers passed to the <TimeSeries> component
import React, { useMemo } from 'react';
import { DataFrame, Field, getDisplayProcessor, PanelProps } from '@grafana/data';
import { Field, getDisplayProcessor, PanelProps } from '@grafana/data';
import { TooltipDisplayMode } from '@grafana/schema';
import { usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin, UPlotConfigBuilder } from '@grafana/ui';
import { usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin, UPlotConfigBuilder, useTheme2 } from '@grafana/ui';
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin';
import { ExemplarsPlugin } from '../timeseries/plugins/ExemplarsPlugin';
import { prepareGraphableFields } from '../timeseries/utils';
import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin';
import { ThresholdControlsPlugin } from '../timeseries/plugins/ThresholdControlsPlugin';
import { config } from 'app/core/config';
@ -17,22 +16,10 @@ import { drawMarkers, FieldIndices } from './utils';
import { defaultColors, MarketOptions, MarketTrendMode } from './models.gen';
import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder';
import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
import { findField } from 'app/features/dimensions';
import { prepareCandlestickFields } from './fields';
interface MarketPanelProps extends PanelProps<MarketOptions> {}
function findFieldInFrames(frames?: DataFrame[], name?: string): Field | undefined {
if (frames?.length) {
for (const frame of frames) {
const f = findField(frame, name);
if (f) {
return f;
}
}
}
return undefined;
}
export const MarketTrendPanel: React.FC<MarketPanelProps> = ({
data,
timeRange,
@ -50,11 +37,9 @@ export const MarketTrendPanel: React.FC<MarketPanelProps> = ({
return getFieldLinksForExplore({ field, rowIndex, splitOpenFn: onSplitOpen, range: timeRange });
};
const { frames, warn } = useMemo(
() => prepareGraphableFields(data?.series, config.theme2),
// eslint-disable-next-line react-hooks/exhaustive-deps
[data, options]
);
const theme = useTheme2();
const info = useMemo(() => prepareCandlestickFields(data?.series, options, theme), [data, options, theme]);
const { renderers, tweakScale, tweakAxis } = useMemo(() => {
let tweakScale = (opts: ScaleProps) => opts;
@ -66,20 +51,20 @@ export const MarketTrendPanel: React.FC<MarketPanelProps> = ({
tweakAxis,
};
if (options.fieldMap == null) {
// Un-encoding the already parsed special fields
// This takes currently matched fields and saves the name so they can be looked up by name later
// ¯\_(ツ)_/¯ someday this can make more sense!
const fieldMap = info.names;
if (!Object.keys(fieldMap).length) {
return doNothing;
}
const { mode, priceStyle, fieldMap, colorStrategy } = options;
const { mode, priceStyle, colorStrategy } = options;
const colors = { ...defaultColors, ...options.colors };
let { open, high, low, close, volume } = fieldMap;
let { open, high, low, close, volume } = fieldMap; // names from matched fields
if (
open == null ||
close == null ||
findFieldInFrames(frames, open) == null ||
findFieldInFrames(frames, close) == null
) {
if (open == null || close == null) {
return doNothing;
}
@ -91,7 +76,7 @@ export const MarketTrendPanel: React.FC<MarketPanelProps> = ({
// find volume field and set overrides
if (volume != null && mode !== MarketTrendMode.Price) {
let volumeField = findFieldInFrames(frames, volume);
let volumeField = info.volume!;
if (volumeField != null) {
shouldRenderVolume = true;
@ -147,12 +132,7 @@ export const MarketTrendPanel: React.FC<MarketPanelProps> = ({
}
}
let shouldRenderPrice =
mode !== MarketTrendMode.Volume &&
high != null &&
low != null &&
findFieldInFrames(frames, high) != null &&
findFieldInFrames(frames, low) != null;
let shouldRenderPrice = mode !== MarketTrendMode.Volume && high != null && low != null;
if (!shouldRenderPrice && !shouldRenderVolume) {
return doNothing;
@ -162,11 +142,11 @@ export const MarketTrendPanel: React.FC<MarketPanelProps> = ({
let indicesOnly = [];
if (shouldRenderPrice) {
fields = { open, high, low, close };
fields = { open, high: high!, low: low!, close };
// hide series from legend that are rendered as composite markers
for (let key in fields) {
let field = findFieldInFrames(frames, fields[key])!;
let field = (info as any)[key] as Field;
field.config = {
...field.config,
custom: {
@ -183,7 +163,7 @@ export const MarketTrendPanel: React.FC<MarketPanelProps> = ({
}
if (shouldRenderVolume) {
fields.volume = volume;
fields.volume = volume!;
fields.open = open;
fields.close = close;
}
@ -219,10 +199,10 @@ export const MarketTrendPanel: React.FC<MarketPanelProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options, data.structureRev]);
if (!frames || warn) {
if (!info.frame || info.warn) {
return (
<div className="panel-empty">
<p>{warn ?? 'No data found in response'}</p>
<p>{info.warn ?? 'No data found in response'}</p>
</div>
);
}
@ -231,7 +211,7 @@ export const MarketTrendPanel: React.FC<MarketPanelProps> = ({
return (
<TimeSeries
frames={frames}
frames={[info.frame]}
structureRev={data.structureRev}
timeRange={timeRange}
timeZone={timeZone}

View File

@ -0,0 +1,121 @@
import { createTheme, toDataFrame } from '@grafana/data';
import { prepareCandlestickFields } from './fields';
import { MarketOptions } from './models.gen';
const theme = createTheme();
describe('Candlestick data', () => {
const options: MarketOptions = {} as MarketOptions;
it('require a time field', () => {
const info = prepareCandlestickFields(
[
toDataFrame({
name: 'hello',
columns: ['a', 'b', 'c'],
rows: [
['A', 2, 3],
['B', 4, 5],
['C', 6, 7],
],
}),
],
options,
theme
);
expect(info.warn).toMatchInlineSnapshot(`"Data does not have a time field"`);
});
it('will match common names by default', () => {
const info = prepareCandlestickFields(
[
toDataFrame({
fields: [
{ name: 'time', values: [1] },
{ name: 'a', values: [1] },
{ name: 'min', values: [1] },
{ name: 'MAX', values: [1] },
{ name: 'v', values: [1] },
],
}),
],
options,
theme
);
expect(info.warn).toBeUndefined();
expect(info.names).toMatchInlineSnapshot(`
Object {
"close": "Next open",
"high": "MAX",
"low": "min",
"open": "a",
"volume": "v",
}
`);
});
it('will support simple timeseries (poorly)', () => {
const info = prepareCandlestickFields(
[
toDataFrame({
fields: [
{
name: 'time',
values: [1, 2, 3],
},
{
name: 'value',
values: [4, 5, 6],
},
],
}),
],
options,
theme
);
expect(info.open).toBeDefined();
expect(info.open).toEqual(info.high);
expect(info.open).toEqual(info.low);
expect(info.open).not.toEqual(info.close);
expect(info.names.close).toMatchInlineSnapshot(`"Next open"`);
// Close should be offset by one and dupliate last point
expect({ open: info.open!.values.toArray(), close: info.close!.values.toArray() }).toMatchInlineSnapshot(`
Object {
"close": Array [
5,
6,
6,
],
"open": Array [
4,
5,
6,
],
}
`);
});
it('will create open from previous close', () => {
const info = prepareCandlestickFields(
[
toDataFrame({
fields: [
{
name: 'time',
values: [1, 2, 3, 4, 5],
},
{
name: 'close',
values: [1, 2, 3, 4, 5],
},
],
}),
],
options,
theme
);
expect(info.open!.values.toArray()).toEqual([1, 1, 2, 3, 4]);
expect(info.close!.values.toArray()).toEqual([1, 2, 3, 4, 5]);
});
});

View File

@ -0,0 +1,181 @@
import {
ArrayVector,
DataFrame,
Field,
FieldType,
getFieldDisplayName,
GrafanaTheme2,
outerJoinDataFrames,
} from '@grafana/data';
import { findField } from 'app/features/dimensions';
import { prepareGraphableFields } from '../timeseries/utils';
import { MarketOptions, CandlestickFieldMap } from './models.gen';
export interface FieldPickerInfo {
/** property name */
key: keyof CandlestickFieldMap;
/** The display name */
name: string;
/** by default pick these fields */
defaults: string[];
/** How is the field used */
description: string;
}
export const candlestickFieldsInfo: Record<keyof CandlestickFieldMap, FieldPickerInfo> = {
open: {
key: 'open',
name: 'Open',
defaults: ['open', 'o'],
description: 'The value at the beginning of the period',
},
high: {
key: 'high',
name: 'High',
defaults: ['high', 'h', 'max'],
description: 'The maximum value within the period',
},
low: {
key: 'low',
name: 'Low',
defaults: ['low', 'l', 'min'],
description: 'The minimum value within the period',
},
close: {
key: 'close',
name: 'Close',
defaults: ['close', 'c'],
description: 'The value at the end of the measured period',
},
volume: {
key: 'volume',
name: 'Volume',
defaults: ['volume', 'v'],
description: 'Activity within the measured period',
},
};
export interface CandlestickData {
warn?: string;
noTimeField?: boolean;
// Special fields
open?: Field;
high?: Field;
low?: Field;
close?: Field;
volume?: Field;
// All incoming values
aligned: DataFrame;
// The stuff passed to GraphNG
frame: DataFrame;
// The real names used
names: CandlestickFieldMap;
}
function findFieldOrAuto(frame: DataFrame, info: FieldPickerInfo, options: CandlestickFieldMap): Field | undefined {
const field = findField(frame, options[info.key]);
if (!field) {
for (const field of frame.fields) {
const name = getFieldDisplayName(field, frame).toLowerCase();
if (info.defaults.includes(name) || info.defaults.includes(field.name)) {
return field;
}
}
}
return field;
}
export function prepareCandlestickFields(
series: DataFrame[] | undefined,
options: MarketOptions,
theme: GrafanaTheme2
): CandlestickData {
if (!series?.length) {
return { warn: 'No data' } as CandlestickData;
}
// All fields
const fieldMap = options.fields ?? {};
const aligned = series.length === 1 ? series[0] : outerJoinDataFrames({ frames: series, enforceSort: true });
if (!aligned?.length) {
return { warn: 'No data found' } as CandlestickData;
}
const data: CandlestickData = { aligned, frame: aligned, names: {} };
// Apply same filter as everythign else in timeseries
const norm = prepareGraphableFields([aligned], theme);
if (norm.warn || norm.noTimeField || !norm.frames?.length) {
return norm as CandlestickData;
}
data.frame = norm.frames[0];
// Find the known fields
const used = new Set<Field>();
for (const info of Object.values(candlestickFieldsInfo)) {
const field = findFieldOrAuto(data.frame, info, fieldMap);
if (field) {
data[info.key] = field;
used.add(field);
}
}
// Use first numeric value as open
if (!data.open && !data.close) {
data.open = data.frame.fields.find((f) => f.type === FieldType.number);
if (data.open) {
used.add(data.open);
}
}
// Use next open as 'close' value
if (data.open && !data.close && !fieldMap.close) {
const values = data.open.values.toArray().slice(1);
values.push(values[values.length - 1]); // duplicate last value
data.close = {
...data.open,
values: new ArrayVector(values),
name: 'Next open',
state: undefined,
};
data.frame.fields.push(data.close);
}
// Use previous close as 'open' value
if (data.close && !data.open && !fieldMap.open) {
const values = data.close.values.toArray().slice();
values.unshift(values[0]); // duplicate first value
values.length = data.frame.length;
data.open = {
...data.close,
values: new ArrayVector(values),
name: 'Previous close',
state: undefined,
};
data.frame.fields.push(data.open);
}
// Use the open field for min/max if nothing is set
if (!data.high && !fieldMap.high) {
data.high = data.open;
}
if (!data.low && !fieldMap.low) {
data.low = data.open;
}
// Register the name of each mapped field
for (const info of Object.values(candlestickFieldsInfo)) {
const f = data[info.key];
if (f) {
data.names[info.key] = getFieldDisplayName(f, data.frame);
}
}
return data;
}

View File

@ -1,30 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
<path style="fill:#4788C7;" d="M35.5,23L35.5,23c-0.275,0-0.5-0.225-0.5-0.5v-21C35,1.225,35.225,1,35.5,1l0,0 C35.775,1,36,1.225,36,1.5v21C36,22.775,35.775,23,35.5,23z"/>
<path style="fill:#4788C7;" d="M14.5,30L14.5,30c-0.275,0-0.5-0.225-0.5-0.5v-23C14,6.225,14.225,6,14.5,6h0 C14.775,6,15,6.225,15,6.5v23C15,29.775,14.775,30,14.5,30z"/>
<path style="fill:#4788C7;" d="M25.5,35L25.5,35c-0.275,0-0.5-0.225-0.5-0.5v-17c0-0.275,0.225-0.5,0.5-0.5l0,0 c0.275,0,0.5,0.225,0.5,0.5v17C26,34.775,25.775,35,25.5,35z"/>
<path style="fill:#4788C7;" d="M4.5,39L4.5,39C4.225,39,4,38.775,4,38.5v-19C4,19.225,4.225,19,4.5,19h0C4.775,19,5,19.225,5,19.5 v19C5,38.775,4.775,39,4.5,39z"/>
<g>
<rect x="32.5" y="4.5" style="fill:#98CCFD;" width="6" height="15"/>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
<defs>
<style>.cls-1{fill:#84aff1;}.cls-2{fill:url(#linear-gradient);}.cls-3{fill:#3865ab;}</style>
<linearGradient id="linear-gradient" y1="40.18" x2="82.99" y2="40.18" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f2cc0c" />
<stop offset="1" stop-color="#ff9830" />
</linearGradient>
</defs>
<g>
<path style="fill:#4788C7;" d="M38,5v14h-5V5H38 M39,4h-7v16h7V4L39,4z"/>
<path class="cls-3" d="M35.5,23L35.5,23c-0.275,0-0.5-0.225-0.5-0.5v-21C35,1.225,35.225,1,35.5,1l0,0 C35.775,1,36,1.225,36,1.5v21C36,22.775,35.775,23,35.5,23z" />
<path class="cls-3" d="M14.5,30L14.5,30c-0.275,0-0.5-0.225-0.5-0.5v-23C14,6.225,14.225,6,14.5,6h0 C14.775,6,15,6.225,15,6.5v23C15,29.775,14.775,30,14.5,30z" />
<path class="cls-3" d="M25.5,35L25.5,35c-0.275,0-0.5-0.225-0.5-0.5v-17c0-0.275,0.225-0.5,0.5-0.5l0,0 c0.275,0,0.5,0.225,0.5,0.5v17C26,34.775,25.775,35,25.5,35z" />
<path class="cls-3" d="M4.5,39L4.5,39C4.225,39,4,38.775,4,38.5v-19C4,19.225,4.225,19,4.5,19h0C4.775,19,5,19.225,5,19.5 v19C5,38.775,4.775,39,4.5,39z" />
<g>
<rect x="32.5" y="4.5" class="cls-1" width="6" height="15" />
<g>
<path class="cls-3" d="M38,5v14h-5V5H38 M39,4h-7v16h7V4L39,4z" />
</g>
</g>
<g>
<rect x="11.5" y="9.5" class="cls-1" width="6" height="17" />
<g>
<path class="cls-3" d="M17,10v16h-5V10H17 M18,9h-7v18h7V9L18,9z" />
</g>
</g>
<g>
<rect x="22.5" y="20.5" class="cls-1" width="6" height="11" />
<g>
<path class="cls-3" d="M28,21v10h-5V21H28 M29,20h-7v12h7V20L29,20z" />
</g>
</g>
<g>
<rect x="1.5" y="22.5" class="cls-1" width="6" height="13" />
<g>
<path class="cls-3" d="M7,23v12H2V23H7 M8,22H1v14h7V22L8,22z" />
</g>
</g>
</g>
</g>
<g>
<rect x="11.5" y="9.5" style="fill:#98CCFD;" width="6" height="17"/>
<g>
<path style="fill:#4788C7;" d="M17,10v16h-5V10H17 M18,9h-7v18h7V9L18,9z"/>
</g>
</g>
<g>
<rect x="22.5" y="20.5" style="fill:#DFF0FE;" width="6" height="11"/>
<g>
<path style="fill:#4788C7;" d="M28,21v10h-5V21H28 M29,20h-7v12h7V20L29,20z"/>
</g>
</g>
<g>
<rect x="1.5" y="22.5" style="fill:#DFF0FE;" width="6" height="13"/>
<g>
<path style="fill:#4788C7;" d="M7,23v12H2V23H7 M8,22H1v14h7V22L8,22z"/>
</g>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -3,7 +3,7 @@
// It is currenty hand written but will serve as the target for cuetsy
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import { TimeSeriesOptions } from '../timeseries/types';
import { LegendDisplayMode, OptionsWithLegend } from '@grafana/schema';
export const modelVersion = Object.freeze([1, 0]);
@ -27,8 +27,12 @@ export enum ColorStrategy {
Inter = 'inter',
}
interface SemanticFieldMap {
[semanticName: string]: string;
export interface CandlestickFieldMap {
open?: string;
high?: string;
low?: string;
close?: string;
volume?: string;
}
export interface MarketTrendColors {
@ -43,10 +47,23 @@ export const defaultColors: MarketTrendColors = {
flat: 'gray',
};
export interface MarketOptions extends TimeSeriesOptions {
export interface MarketOptions extends OptionsWithLegend {
mode: MarketTrendMode;
priceStyle: PriceStyle;
colorStrategy: ColorStrategy;
fieldMap: SemanticFieldMap;
fields: CandlestickFieldMap;
colors: MarketTrendColors;
}
export const defaultPanelOptions: MarketOptions = {
mode: MarketTrendMode.PriceVolume,
priceStyle: PriceStyle.Candles,
colorStrategy: ColorStrategy.Intra,
colors: defaultColors,
fields: {},
legend: {
displayMode: LegendDisplayMode.List,
placement: 'bottom',
calcs: [],
},
};

View File

@ -1,9 +1,26 @@
import { GraphFieldConfig } from '@grafana/schema';
import { FieldConfigProperty, PanelPlugin, SelectableValue } from '@grafana/data';
import {
Field,
FieldConfigProperty,
FieldType,
getFieldDisplayName,
PanelOptionsEditorBuilder,
PanelPlugin,
SelectableValue,
} from '@grafana/data';
import { commonOptionsBuilder } from '@grafana/ui';
import { MarketTrendPanel } from './MarketTrendPanel';
import { defaultColors, MarketOptions, MarketTrendMode, ColorStrategy, PriceStyle } from './models.gen';
import {
defaultColors,
MarketOptions,
MarketTrendMode,
ColorStrategy,
PriceStyle,
defaultPanelOptions,
} from './models.gen';
import { defaultGraphConfig, getGraphFieldConfig } from '../timeseries/config';
import { CandlestickData, candlestickFieldsInfo, FieldPickerInfo, prepareCandlestickFields } from './fields';
import { config } from '@grafana/runtime';
const modeOptions = [
{ label: 'Price & Volume', value: MarketTrendMode.PriceVolume },
@ -30,9 +47,42 @@ function getMarketFieldConfig() {
return v;
}
const numericFieldFilter = (f: Field) => f.type === FieldType.number;
function addFieldPicker(
builder: PanelOptionsEditorBuilder<MarketOptions>,
info: FieldPickerInfo,
data: CandlestickData
) {
const current = data[info.key] as Field;
let placeholderText = 'Auto ';
if (current?.config) {
placeholderText += '= ' + getFieldDisplayName(current);
if (current === data?.open && info.key !== 'open') {
placeholderText += ` (${info.defaults.join(',')})`;
}
} else {
placeholderText += `(${info.defaults.join(',')})`;
}
builder.addFieldNamePicker({
path: `fields.${info.key}`,
name: info.name,
description: info.description,
settings: {
filter: numericFieldFilter,
placeholderText,
},
});
}
export const plugin = new PanelPlugin<MarketOptions, GraphFieldConfig>(MarketTrendPanel)
.useFieldConfig(getMarketFieldConfig())
.setPanelOptions((builder) => {
.setPanelOptions((builder, context) => {
const opts = context.options ?? defaultPanelOptions;
const info = prepareCandlestickFields(context.data, opts, config.theme2);
builder
.addRadio({
path: 'mode',
@ -71,31 +121,19 @@ export const plugin = new PanelPlugin<MarketOptions, GraphFieldConfig>(MarketTre
path: 'colors.down',
name: 'Down color',
defaultValue: defaultColors.down,
})
.addFieldNamePicker({
path: 'fieldMap.open',
name: 'Open field',
})
.addFieldNamePicker({
path: 'fieldMap.high',
name: 'High field',
showIf: (opts) => opts.mode !== MarketTrendMode.Volume,
})
.addFieldNamePicker({
path: 'fieldMap.low',
name: 'Low field',
showIf: (opts) => opts.mode !== MarketTrendMode.Volume,
})
.addFieldNamePicker({
path: 'fieldMap.close',
name: 'Close field',
})
.addFieldNamePicker({
path: 'fieldMap.volume',
name: 'Volume field',
showIf: (opts) => opts.mode !== MarketTrendMode.Price,
});
addFieldPicker(builder, candlestickFieldsInfo.open, info);
if (opts.mode !== MarketTrendMode.Volume) {
addFieldPicker(builder, candlestickFieldsInfo.high, info);
addFieldPicker(builder, candlestickFieldsInfo.low, info);
}
addFieldPicker(builder, candlestickFieldsInfo.close, info);
if (opts.mode !== MarketTrendMode.Price) {
addFieldPicker(builder, candlestickFieldsInfo.volume, info);
}
// commonOptionsBuilder.addTooltipOptions(builder);
commonOptionsBuilder.addLegendOptions(builder);
})