mirror of
https://github.com/grafana/grafana.git
synced 2025-02-14 01:23:32 -06:00
254 lines
6.1 KiB
TypeScript
254 lines
6.1 KiB
TypeScript
import {
|
|
ArrayVector,
|
|
DataFrame,
|
|
Field,
|
|
FieldType,
|
|
getFieldDisplayName,
|
|
GrafanaTheme2,
|
|
outerJoinDataFrames,
|
|
} from '@grafana/data';
|
|
import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames';
|
|
import { findField } from 'app/features/dimensions';
|
|
import { prepareGraphableFields } from '../timeseries/utils';
|
|
import { CandlestickOptions, CandlestickFieldMap, VizDisplayMode } 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: 'Value at the start of the period',
|
|
},
|
|
high: {
|
|
key: 'high',
|
|
name: 'High',
|
|
defaults: ['high', 'h', 'max'],
|
|
description: 'Maximum value within the period',
|
|
},
|
|
low: {
|
|
key: 'low',
|
|
name: 'Low',
|
|
defaults: ['low', 'l', 'min'],
|
|
description: 'Minimum value within the period',
|
|
},
|
|
close: {
|
|
key: 'close',
|
|
name: 'Close',
|
|
defaults: ['close', 'c'],
|
|
description: 'Value at the end of the period',
|
|
},
|
|
volume: {
|
|
key: 'volume',
|
|
name: 'Volume',
|
|
defaults: ['volume', 'v'],
|
|
description: 'Sample count within the period',
|
|
},
|
|
};
|
|
|
|
export interface CandlestickData {
|
|
autoOpenClose?: 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: CandlestickOptions,
|
|
theme: GrafanaTheme2
|
|
): CandlestickData | null {
|
|
if (!series?.length) {
|
|
return null;
|
|
}
|
|
|
|
// All fields
|
|
const fieldMap = options.fields ?? {};
|
|
const aligned =
|
|
series.length === 1
|
|
? maybeSortFrame(
|
|
series[0],
|
|
series[0].fields.findIndex((f) => f.type === FieldType.time)
|
|
)
|
|
: outerJoinDataFrames({ frames: series });
|
|
if (!aligned?.length) {
|
|
return null;
|
|
}
|
|
|
|
const data: CandlestickData = { aligned, frame: aligned, names: {} };
|
|
|
|
// Apply same filter as everythign else in timeseries
|
|
const timeSeriesFrames = prepareGraphableFields([aligned], theme);
|
|
if (!timeSeriesFrames) {
|
|
return null;
|
|
}
|
|
|
|
const frame = (data.frame = timeSeriesFrames[0]);
|
|
const timeIndex = frame.fields.findIndex((f) => f.type === FieldType.time);
|
|
|
|
if (timeIndex < 0) {
|
|
return null;
|
|
}
|
|
|
|
// Find the known fields
|
|
const used = new Set<Field>();
|
|
for (const info of Object.values(candlestickFieldsInfo)) {
|
|
const field = findFieldOrAuto(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 = 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,
|
|
};
|
|
used.add(data.close);
|
|
frame.fields.push(data.close);
|
|
data.autoOpenClose = true;
|
|
}
|
|
|
|
// 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 = frame.length;
|
|
data.open = {
|
|
...data.close,
|
|
values: new ArrayVector(values),
|
|
name: 'Previous close',
|
|
state: undefined,
|
|
};
|
|
used.add(data.open);
|
|
frame.fields.push(data.open);
|
|
data.autoOpenClose = true;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// unmap low and high fields in volume-only mode, and volume field in candles-only mode
|
|
// so they fall through to unmapped fields and get appropriate includeAllFields treatment
|
|
if (options.mode === VizDisplayMode.Volume) {
|
|
if (data.high) {
|
|
if (data.high !== data.open) {
|
|
used.delete(data.high);
|
|
}
|
|
data.high = undefined;
|
|
}
|
|
if (data.low) {
|
|
if (data.low !== data.open) {
|
|
used.delete(data.low);
|
|
}
|
|
data.low = undefined;
|
|
}
|
|
} else if (options.mode === VizDisplayMode.Candles) {
|
|
if (data.volume) {
|
|
used.delete(data.volume);
|
|
data.volume = undefined;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
const timeField = frame.fields[timeIndex];
|
|
|
|
// Make sure first field is time!
|
|
const fields: Field[] = [timeField];
|
|
|
|
if (!options.includeAllFields) {
|
|
fields.push(...used);
|
|
} else {
|
|
fields.push(...frame.fields.filter((f) => f !== timeField));
|
|
}
|
|
|
|
data.frame = {
|
|
...data.frame,
|
|
fields,
|
|
};
|
|
|
|
// Force update all the indicies
|
|
for (let i = 0; i < data.frame.fields.length; i++) {
|
|
const field = data.frame.fields[i];
|
|
|
|
field.state = {
|
|
...field.state,
|
|
|
|
// time is unused (-1), y series enumerate from 0
|
|
seriesIndex: i - 1,
|
|
|
|
origin: {
|
|
fieldIndex: i,
|
|
frameIndex: 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
return data;
|
|
}
|