The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/panel/candlestick/CandlestickPanel.tsx

338 lines
11 KiB

// this file is pretty much a copy-paste of TimeSeriesPanel.tsx :(
// with some extra renderers passed to the <TimeSeries> component
import { useMemo, useState } from 'react';
import uPlot from 'uplot';
import { Field, getDisplayProcessor, PanelProps } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { DashboardCursorSync, TooltipDisplayMode } from '@grafana/schema';
import {
EventBusPlugin,
KeyboardPlugin,
TooltipPlugin2,
UPlotConfigBuilder,
usePanelContext,
useTheme2,
} from '@grafana/ui';
import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder';
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries';
import { config } from 'app/core/config';
import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip';
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
import { ExemplarsPlugin } from '../timeseries/plugins/ExemplarsPlugin';
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { ThresholdControlsPlugin } from '../timeseries/plugins/ThresholdControlsPlugin';
import { prepareCandlestickFields } from './fields';
import { Options, defaultCandlestickColors, VizDisplayMode } from './types';
import { drawMarkers, FieldIndices } from './utils';
interface CandlestickPanelProps extends PanelProps<Options> {}
export const CandlestickPanel = ({
data,
id,
timeRange,
timeZone,
width,
height,
options,
fieldConfig,
onChangeTimeRange,
replaceVariables,
}: CandlestickPanelProps) => {
const {
sync,
eventsScope,
canAddAnnotations,
onThresholdsChange,
canEditThresholds,
showThresholds,
dataLinkPostProcessor,
eventBus,
} = usePanelContext();
const theme = useTheme2();
const info = useMemo(() => {
return prepareCandlestickFields(data.series, options, theme, timeRange);
}, [data.series, options, theme, timeRange]);
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
const cursorSync = sync?.() ?? DashboardCursorSync.Off;
const { renderers, tweakScale, tweakAxis, shouldRenderPrice } = useMemo(() => {
let tweakScale = (opts: ScaleProps, forField: Field) => opts;
let tweakAxis = (opts: AxisProps, forField: Field) => opts;
let doNothing = {
renderers: [],
tweakScale,
tweakAxis,
shouldRenderPrice: false,
};
if (!info) {
return doNothing;
}
// 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, candleStyle, colorStrategy } = options;
const colors = { ...defaultCandlestickColors, ...options.colors };
let { open, high, low, close, volume } = fieldMap; // names from matched fields
if (open == null || close == null) {
return doNothing;
}
let volumeAlpha = 0.5;
let volumeIdx = -1;
let shouldRenderVolume = false;
// find volume field and set overrides
if (volume != null && mode !== VizDisplayMode.Candles) {
let volumeField = info.volume!;
if (volumeField != null) {
shouldRenderVolume = true;
let { fillOpacity } = volumeField.config.custom;
if (fillOpacity) {
volumeAlpha = fillOpacity / 100;
}
// we only want to put volume on own shorter axis when rendered with price
if (mode !== VizDisplayMode.Volume) {
volumeField.config = { ...volumeField.config };
volumeField.config.unit = 'short';
volumeField.display = getDisplayProcessor({
field: volumeField,
theme: config.theme2,
});
tweakAxis = (opts: AxisProps, forField: Field) => {
// we can't do forField === info.volume because of copies :(
if (forField.name === info.volume?.name) {
let filter = (u: uPlot, splits: number[]) => {
let _splits = [];
let max = u.series[volumeIdx].max;
for (let i = 0; i < splits.length; i++) {
_splits.push(splits[i]);
if (max && splits[i] > max) {
break;
}
}
return _splits;
};
opts.space = 20; // reduce tick spacing
opts.filter = filter; // hide tick labels
opts.ticks = { ...opts.ticks, filter }; // hide tick marks
}
return opts;
};
tweakScale = (opts: ScaleProps, forField: Field) => {
// we can't do forField === info.volume because of copies :(
if (forField.name === info.volume?.name) {
opts.range = (u: uPlot, min: number, max: number) => [0, max * 7];
}
return opts;
};
}
}
}
let shouldRenderPrice = mode !== VizDisplayMode.Volume && high != null && low != null;
if (!shouldRenderPrice && !shouldRenderVolume) {
return doNothing;
}
let fields: Record<string, string> = {};
let indicesOnly = [];
if (shouldRenderPrice) {
fields = { open, high: high!, low: low!, close };
} else {
// these fields should not be omitted from normal rendering if they arent rendered
// as part of price markers. they're only here so we can get back their indicies in the
// init callback below. TODO: remove this when field mapping happens in the panel instead of deep
indicesOnly.push(open, close);
}
if (shouldRenderVolume) {
fields.volume = volume!;
fields.open = open;
fields.close = close;
}
return {
shouldRenderPrice,
renderers: [
{
fieldMap: fields,
indicesOnly,
init: (builder: UPlotConfigBuilder, fieldIndices: FieldIndices) => {
volumeIdx = fieldIndices.volume!;
builder.addHook(
'drawAxes',
drawMarkers({
mode,
fields: fieldIndices,
upColor: config.theme2.visualization.getColorByName(colors.up),
downColor: config.theme2.visualization.getColorByName(colors.down),
flatColor: config.theme2.visualization.getColorByName(colors.flat),
volumeAlpha,
colorStrategy,
candleStyle,
flatAsUp: true,
})
);
},
},
],
tweakScale,
tweakAxis,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options, data.structureRev, data.series.length]);
if (!info) {
return (
<PanelDataErrorView
panelId={id}
fieldConfig={fieldConfig}
data={data}
needsTimeField={true}
needsNumberField={true}
/>
);
}
if (shouldRenderPrice) {
// hide series from legend that are rendered as composite markers
for (let key in renderers[0].fieldMap) {
let field: Field = (info as any)[key];
field.config = {
...field.config,
custom: {
...field.config.custom,
hideFrom: { legend: true, tooltip: false, viz: false },
},
};
}
}
const enableAnnotationCreation = Boolean(canAddAnnotations?.());
return (
<TimeSeries
frames={[info.frame]}
structureRev={data.structureRev}
timeRange={timeRange}
timeZone={timeZone}
width={width}
height={height}
legend={options.legend}
renderers={renderers}
tweakAxis={tweakAxis}
tweakScale={tweakScale}
options={options}
replaceVariables={replaceVariables}
dataLinkPostProcessor={dataLinkPostProcessor}
cursorSync={cursorSync}
>
{(uplotConfig, alignedFrame) => {
return (
<>
<KeyboardPlugin config={uplotConfig} />
{cursorSync !== DashboardCursorSync.Off && (
<EventBusPlugin config={uplotConfig} eventBus={eventBus} frame={alignedFrame} />
)}
{options.tooltip.mode !== TooltipDisplayMode.None && (
<TooltipPlugin2
config={uplotConfig}
hoverMode={
options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll
}
queryZoom={onChangeTimeRange}
clientZoom={true}
syncMode={cursorSync}
syncScope={eventsScope}
render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2, viaSync) => {
if (enableAnnotationCreation && timeRange2 != null) {
setNewAnnotationRange(timeRange2);
dismiss();
return;
}
const annotate = () => {
let xVal = u.posToVal(u.cursor.left!, 'x');
setNewAnnotationRange({ from: xVal, to: xVal });
dismiss();
};
return (
<TimeSeriesTooltip
series={alignedFrame}
dataIdxs={dataIdxs}
seriesIdx={seriesIdx}
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
sortOrder={options.tooltip.sort}
isPinned={isPinned}
annotate={enableAnnotationCreation ? annotate : undefined}
maxHeight={options.tooltip.maxHeight}
/>
);
}}
maxWidth={options.tooltip.maxWidth}
/>
)}
<AnnotationsPlugin2
annotations={data.annotations ?? []}
config={uplotConfig}
timeZone={timeZone}
newRange={newAnnotationRange}
setNewRange={setNewAnnotationRange}
/>
<OutsideRangePlugin config={uplotConfig} onChangeTimeRange={onChangeTimeRange} />
{data.annotations && (
<ExemplarsPlugin config={uplotConfig} exemplars={data.annotations} timeZone={timeZone} />
)}
{((canEditThresholds && onThresholdsChange) || showThresholds) && (
<ThresholdControlsPlugin
config={uplotConfig}
fieldConfig={fieldConfig}
onThresholdsChange={canEditThresholds ? onThresholdsChange : undefined}
/>
)}
</>
);
}}
</TimeSeries>
);
};