CandlestickPanel.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. // this file is pretty much a copy-paste of TimeSeriesPanel.tsx :(
  2. // with some extra renderers passed to the <TimeSeries> component
  3. import React, { useMemo } from 'react';
  4. import uPlot from 'uplot';
  5. import { Field, getDisplayProcessor, PanelProps } from '@grafana/data';
  6. import { PanelDataErrorView } from '@grafana/runtime';
  7. import { TooltipDisplayMode } from '@grafana/schema';
  8. import { usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin, UPlotConfigBuilder, useTheme2 } from '@grafana/ui';
  9. import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
  10. import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder';
  11. import { config } from 'app/core/config';
  12. import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
  13. import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin';
  14. import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
  15. import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin';
  16. import { ExemplarsPlugin } from '../timeseries/plugins/ExemplarsPlugin';
  17. import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
  18. import { ThresholdControlsPlugin } from '../timeseries/plugins/ThresholdControlsPlugin';
  19. import { prepareCandlestickFields } from './fields';
  20. import { defaultColors, CandlestickOptions, VizDisplayMode } from './models.gen';
  21. import { drawMarkers, FieldIndices } from './utils';
  22. interface CandlestickPanelProps extends PanelProps<CandlestickOptions> {}
  23. export const CandlestickPanel: React.FC<CandlestickPanelProps> = ({
  24. data,
  25. id,
  26. timeRange,
  27. timeZone,
  28. width,
  29. height,
  30. options,
  31. fieldConfig,
  32. onChangeTimeRange,
  33. replaceVariables,
  34. }) => {
  35. const { sync, canAddAnnotations, onThresholdsChange, canEditThresholds, onSplitOpen } = usePanelContext();
  36. const getFieldLinks = (field: Field, rowIndex: number) => {
  37. return getFieldLinksForExplore({ field, rowIndex, splitOpenFn: onSplitOpen, range: timeRange });
  38. };
  39. const theme = useTheme2();
  40. const info = useMemo(() => {
  41. return prepareCandlestickFields(data?.series, options, theme, timeRange);
  42. }, [data, options, theme, timeRange]);
  43. const { renderers, tweakScale, tweakAxis } = useMemo(() => {
  44. let tweakScale = (opts: ScaleProps, forField: Field) => opts;
  45. let tweakAxis = (opts: AxisProps, forField: Field) => opts;
  46. let doNothing = {
  47. renderers: [],
  48. tweakScale,
  49. tweakAxis,
  50. };
  51. if (!info) {
  52. return doNothing;
  53. }
  54. // Un-encoding the already parsed special fields
  55. // This takes currently matched fields and saves the name so they can be looked up by name later
  56. // ¯\_(ツ)_/¯ someday this can make more sense!
  57. const fieldMap = info.names;
  58. if (!Object.keys(fieldMap).length) {
  59. return doNothing;
  60. }
  61. const { mode, candleStyle, colorStrategy } = options;
  62. const colors = { ...defaultColors, ...options.colors };
  63. let { open, high, low, close, volume } = fieldMap; // names from matched fields
  64. if (open == null || close == null) {
  65. return doNothing;
  66. }
  67. let volumeAlpha = 0.5;
  68. let volumeIdx = -1;
  69. let shouldRenderVolume = false;
  70. // find volume field and set overrides
  71. if (volume != null && mode !== VizDisplayMode.Candles) {
  72. let volumeField = info.volume!;
  73. if (volumeField != null) {
  74. shouldRenderVolume = true;
  75. let { fillOpacity } = volumeField.config.custom;
  76. if (fillOpacity) {
  77. volumeAlpha = fillOpacity / 100;
  78. }
  79. // we only want to put volume on own shorter axis when rendered with price
  80. if (mode !== VizDisplayMode.Volume) {
  81. volumeField.config = { ...volumeField.config };
  82. volumeField.config.unit = 'short';
  83. volumeField.display = getDisplayProcessor({
  84. field: volumeField,
  85. theme: config.theme2,
  86. });
  87. tweakAxis = (opts: AxisProps, forField: Field) => {
  88. // we can't do forField === info.volume because of copies :(
  89. if (forField.name === info.volume?.name) {
  90. let filter = (u: uPlot, splits: number[]) => {
  91. let _splits = [];
  92. let max = u.series[volumeIdx].max as number;
  93. for (let i = 0; i < splits.length; i++) {
  94. _splits.push(splits[i]);
  95. if (splits[i] > max) {
  96. break;
  97. }
  98. }
  99. return _splits;
  100. };
  101. opts.space = 20; // reduce tick spacing
  102. opts.filter = filter; // hide tick labels
  103. opts.ticks = { ...opts.ticks, filter }; // hide tick marks
  104. }
  105. return opts;
  106. };
  107. tweakScale = (opts: ScaleProps, forField: Field) => {
  108. // we can't do forField === info.volume because of copies :(
  109. if (forField.name === info.volume?.name) {
  110. opts.range = (u: uPlot, min: number, max: number) => [0, max * 7];
  111. }
  112. return opts;
  113. };
  114. }
  115. }
  116. }
  117. let shouldRenderPrice = mode !== VizDisplayMode.Volume && high != null && low != null;
  118. if (!shouldRenderPrice && !shouldRenderVolume) {
  119. return doNothing;
  120. }
  121. let fields: Record<string, string> = {};
  122. let indicesOnly = [];
  123. if (shouldRenderPrice) {
  124. fields = { open, high: high!, low: low!, close };
  125. // hide series from legend that are rendered as composite markers
  126. for (let key in fields) {
  127. let field = (info as any)[key] as Field;
  128. field.config = {
  129. ...field.config,
  130. custom: {
  131. ...field.config.custom,
  132. hideFrom: { legend: true, tooltip: false, viz: false },
  133. },
  134. };
  135. }
  136. } else {
  137. // these fields should not be omitted from normal rendering if they arent rendered
  138. // as part of price markers. they're only here so we can get back their indicies in the
  139. // init callback below. TODO: remove this when field mapping happens in the panel instead of deep
  140. indicesOnly.push(open, close);
  141. }
  142. if (shouldRenderVolume) {
  143. fields.volume = volume!;
  144. fields.open = open;
  145. fields.close = close;
  146. }
  147. return {
  148. renderers: [
  149. {
  150. fieldMap: fields,
  151. indicesOnly,
  152. init: (builder: UPlotConfigBuilder, fieldIndices: FieldIndices) => {
  153. volumeIdx = fieldIndices.volume!;
  154. builder.addHook(
  155. 'drawAxes',
  156. drawMarkers({
  157. mode,
  158. fields: fieldIndices,
  159. upColor: config.theme2.visualization.getColorByName(colors.up),
  160. downColor: config.theme2.visualization.getColorByName(colors.down),
  161. flatColor: config.theme2.visualization.getColorByName(colors.flat),
  162. volumeAlpha,
  163. colorStrategy,
  164. candleStyle,
  165. flatAsUp: true,
  166. })
  167. );
  168. },
  169. },
  170. ],
  171. tweakScale,
  172. tweakAxis,
  173. };
  174. // eslint-disable-next-line react-hooks/exhaustive-deps
  175. }, [options, data.structureRev]);
  176. if (!info) {
  177. return (
  178. <PanelDataErrorView
  179. panelId={id}
  180. fieldConfig={fieldConfig}
  181. data={data}
  182. needsTimeField={true}
  183. needsNumberField={true}
  184. />
  185. );
  186. }
  187. const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
  188. return (
  189. <TimeSeries
  190. frames={[info.frame]}
  191. structureRev={data.structureRev}
  192. timeRange={timeRange}
  193. timeZone={timeZone}
  194. width={width}
  195. height={height}
  196. legend={options.legend}
  197. renderers={renderers}
  198. tweakAxis={tweakAxis}
  199. tweakScale={tweakScale}
  200. options={options}
  201. >
  202. {(config, alignedDataFrame) => {
  203. return (
  204. <>
  205. <ZoomPlugin config={config} onZoom={onChangeTimeRange} />
  206. <TooltipPlugin
  207. data={alignedDataFrame}
  208. config={config}
  209. mode={TooltipDisplayMode.Multi}
  210. sync={sync}
  211. timeZone={timeZone}
  212. />
  213. {/* Renders annotation markers*/}
  214. {data.annotations && (
  215. <AnnotationsPlugin annotations={data.annotations} config={config} timeZone={timeZone} />
  216. )}
  217. {/* Enables annotations creation*/}
  218. {enableAnnotationCreation ? (
  219. <AnnotationEditorPlugin data={alignedDataFrame} timeZone={timeZone} config={config}>
  220. {({ startAnnotating }) => {
  221. return (
  222. <ContextMenuPlugin
  223. data={alignedDataFrame}
  224. config={config}
  225. timeZone={timeZone}
  226. replaceVariables={replaceVariables}
  227. defaultItems={
  228. enableAnnotationCreation
  229. ? [
  230. {
  231. items: [
  232. {
  233. label: 'Add annotation',
  234. ariaLabel: 'Add annotation',
  235. icon: 'comment-alt',
  236. onClick: (e, p) => {
  237. if (!p) {
  238. return;
  239. }
  240. startAnnotating({ coords: p.coords });
  241. },
  242. },
  243. ],
  244. },
  245. ]
  246. : []
  247. }
  248. />
  249. );
  250. }}
  251. </AnnotationEditorPlugin>
  252. ) : (
  253. <ContextMenuPlugin
  254. data={alignedDataFrame}
  255. config={config}
  256. timeZone={timeZone}
  257. replaceVariables={replaceVariables}
  258. defaultItems={[]}
  259. />
  260. )}
  261. {data.annotations && (
  262. <ExemplarsPlugin
  263. config={config}
  264. exemplars={data.annotations}
  265. timeZone={timeZone}
  266. getFieldLinks={getFieldLinks}
  267. />
  268. )}
  269. {canEditThresholds && onThresholdsChange && (
  270. <ThresholdControlsPlugin
  271. config={config}
  272. fieldConfig={fieldConfig}
  273. onThresholdsChange={onThresholdsChange}
  274. />
  275. )}
  276. <OutsideRangePlugin config={config} onChangeTimeRange={onChangeTimeRange} />
  277. </>
  278. );
  279. }}
  280. </TimeSeries>
  281. );
  282. };