mirror of
https://github.com/grafana/grafana.git
synced 2025-02-03 12:11:09 -06:00
MarketTrend: aggressive default field matching (#41574)
This commit is contained in:
parent
7a92faf398
commit
7ffdff9e1f
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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)} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
|
121
public/app/plugins/panel/market-trend/fields.test.ts
Normal file
121
public/app/plugins/panel/market-trend/fields.test.ts
Normal 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]);
|
||||
});
|
||||
});
|
181
public/app/plugins/panel/market-trend/fields.ts
Normal file
181
public/app/plugins/panel/market-trend/fields.ts
Normal 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;
|
||||
}
|
@ -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 |
@ -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: [],
|
||||
},
|
||||
};
|
||||
|
@ -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);
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user