react-native-vroom-chart 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/android/CMakeLists.txt +143 -0
  2. package/android/README.md +60 -5
  3. package/android/build.gradle +115 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/cpp/VroomChartJni.cpp +24 -0
  6. package/android/src/main/java/com/vroom/chart/VroomChartModule.kt +31 -0
  7. package/android/src/main/java/com/vroom/chart/VroomChartPackage.kt +31 -0
  8. package/cpp/VroomChartHostObject.cpp +368 -33
  9. package/cpp/VroomJsiInstaller.cpp +11 -1
  10. package/cpp/VroomSkiaContext.cpp +15 -7
  11. package/cpp/_core_include/vroom/vroom_chart.h +250 -22
  12. package/cpp/_core_src/bollinger.h +1 -1
  13. package/cpp/_core_src/candles.cpp +138 -41
  14. package/cpp/_core_src/candles.h +11 -1
  15. package/cpp/_core_src/chart.cpp +98 -40
  16. package/cpp/_core_src/chart.h +82 -20
  17. package/cpp/_core_src/chart_facade.cpp +297 -66
  18. package/cpp/_core_src/gradient.cpp +46 -0
  19. package/cpp/_core_src/gradient.h +31 -0
  20. package/cpp/_core_src/labels.cpp +49 -16
  21. package/cpp/_core_src/labels.h +33 -4
  22. package/cpp/_core_src/liquidity.cpp +3 -37
  23. package/cpp/_core_src/ma_overlay.cpp +174 -12
  24. package/cpp/_core_src/ma_overlay.h +55 -3
  25. package/cpp/_core_src/macd.cpp +13 -42
  26. package/cpp/_core_src/macd.h +11 -8
  27. package/cpp/_core_src/macd_pane.cpp +57 -27
  28. package/cpp/_core_src/price_line_layout.cpp +87 -0
  29. package/cpp/_core_src/price_line_layout.h +80 -0
  30. package/cpp/_core_src/price_lines.cpp +428 -0
  31. package/cpp/_core_src/price_lines.h +56 -0
  32. package/cpp/_core_src/rsi.cpp +4 -18
  33. package/cpp/_core_src/rsi.h +6 -6
  34. package/cpp/_core_src/rsi_pane.cpp +34 -19
  35. package/cpp/_core_src/series_ma.cpp +64 -0
  36. package/cpp/_core_src/series_ma.h +30 -0
  37. package/cpp/_core_src/style_inherit.h +35 -0
  38. package/cpp/_core_src/theme.cpp +2 -1
  39. package/cpp/_core_src/viewport.cpp +36 -12
  40. package/cpp/_core_src/viewport.h +50 -0
  41. package/cpp/_core_src/volume.cpp +33 -8
  42. package/cpp/_core_src/volume.h +12 -1
  43. package/cpp/_core_src/volume_anim.cpp +32 -0
  44. package/cpp/_core_src/volume_anim.h +41 -0
  45. package/lib/index.d.mts +261 -25
  46. package/lib/index.d.ts +261 -25
  47. package/lib/index.js +252 -36
  48. package/lib/index.js.map +1 -1
  49. package/lib/index.mjs +252 -36
  50. package/lib/index.mjs.map +1 -1
  51. package/package.json +5 -4
  52. package/src/VroomChart.tsx +162 -11
  53. package/src/easing.ts +40 -0
  54. package/src/index.ts +5 -0
  55. package/src/jsi.d.ts +124 -20
  56. package/src/theme.ts +1 -0
  57. package/src/types.ts +5 -0
  58. package/src/useChartCore.ts +166 -28
package/src/jsi.d.ts CHANGED
@@ -102,22 +102,53 @@ export interface ChartHandle {
102
102
  } | null;
103
103
  } | null;
104
104
  /**
105
- * Configures the RSI pane: enable, period (>=2), overbought/oversold band
106
- * levels (0..100), and the RSI-based MA trendline (toggle + length >=1).
107
- */
108
- setRSI(
109
- enabled: boolean,
110
- period: number,
111
- upperBand: number,
112
- lowerBand: number,
113
- maEnabled: boolean,
114
- maPeriod: number,
115
- ): void;
105
+ * Configures the RSI pane. maKind mirrors setOverlays' kind encoding; colors
106
+ * are packed 0xAARRGGBB where 0 means inherit, and a non-positive width
107
+ * inherits the default stroke.
108
+ */
109
+ setRSI(spec: {
110
+ enabled: boolean;
111
+ period: number;
112
+ upperBand: number;
113
+ lowerBand: number;
114
+ maPeriod: number;
115
+ maKind: number;
116
+ maVisible: boolean;
117
+ lineColor: number;
118
+ lineWidth: number;
119
+ lineVisible: boolean;
120
+ maColor: number;
121
+ maWidth: number;
122
+ bandColor: number;
123
+ bandsVisible: boolean;
124
+ }): void;
116
125
  /**
117
- * Configures the MACD pane: enable, fast/slow EMA lengths (slow forced > fast)
118
- * and the signal-line length. Defaults 12/26/9.
126
+ * Configures the MACD pane. source/maKind mirror setOverlays' encodings;
127
+ * colors are packed 0xAARRGGBB where 0 means inherit, and a non-positive
128
+ * width inherits the default stroke.
119
129
  */
120
- setMACD(enabled: boolean, fast: number, slow: number, signal: number): void;
130
+ setMACD(spec: {
131
+ enabled: boolean;
132
+ fast: number;
133
+ slow: number;
134
+ signal: number;
135
+ source: number;
136
+ maKind: number;
137
+ signalMaKind: number;
138
+ lineColor: number;
139
+ lineWidth: number;
140
+ lineVisible: boolean;
141
+ signalColor: number;
142
+ signalWidth: number;
143
+ signalVisible: boolean;
144
+ histVisible: boolean;
145
+ histUpColor: number;
146
+ histUpFadingColor: number;
147
+ histDownColor: number;
148
+ histDownFadingColor: number;
149
+ zeroColor: number;
150
+ zeroVisible: boolean;
151
+ }): void;
121
152
  /**
122
153
  * Replaces the full set of MA/EMA overlay lines drawn on the price pane.
123
154
  * kind: 0=SMA, 1=EMA; source: 0=close,1=open,2=high,3=low,4=hl2,5=hlc3,6=ohlc4;
@@ -136,12 +167,12 @@ export interface ChartHandle {
136
167
  * Configures the session VWAP overlay. `resetOffsetMin` shifts the session
137
168
  * boundary from UTC midnight (minutes); `color` is packed 0xAARRGGBB.
138
169
  */
139
- setVWAP(
140
- enabled: boolean,
141
- resetOffsetMin: number,
142
- color: number,
143
- width: number,
144
- ): void;
170
+ setVWAP(spec: {
171
+ enabled: boolean;
172
+ resetOffsetMin: number;
173
+ color: number;
174
+ width: number;
175
+ }): void;
145
176
  /**
146
177
  * Configures the Bollinger Bands overlay (three price-pane lines + optional
147
178
  * fill between the bands). source/basisKind mirror setOverlays' encodings;
@@ -162,6 +193,79 @@ export interface ChartHandle {
162
193
  fillEnabled: boolean;
163
194
  fillOpacity: number;
164
195
  }): void;
196
+ /**
197
+ * Configures the volume bars under the candles. `heightFrac` is the tallest
198
+ * bar as a fraction of the price pane. The style fields carry an inherit
199
+ * sentinel: a negative number or a transparent color falls back to the
200
+ * matching theme key.
201
+ */
202
+ setVolume(spec: {
203
+ enabled: boolean;
204
+ heightFrac: number;
205
+ opacity: number;
206
+ radiusPx: number;
207
+ upColor: number;
208
+ downColor: number;
209
+ }): void;
210
+ /**
211
+ * Staggered volume-bar collapse: 0 = full height, 1 = all bars flat. Bars fall
212
+ * tallest-first and land together; drive 1 → 0 to reveal them, which plays the
213
+ * cascade in reverse (shortest bar home first).
214
+ *
215
+ * Unlike setMorph, `t` must be **linear** progress — the core eases each bar
216
+ * over its own slice of the timeline, so the curve is applied there. `easing`
217
+ * indexes `linear | ease-in | ease-out | ease-in-out`. setVolume snaps this to
218
+ * match its `enabled`, so it's only needed while animating.
219
+ */
220
+ setVolumeCollapse(t: number, easing: number): void;
221
+ /**
222
+ * The continuous data coordinate at pixel (x, y) — not snapped to a candle
223
+ * slot. Null when there are no candles or the viewport is degenerate. Cheap to
224
+ * call at gesture rate (no rendering).
225
+ */
226
+ coordAt(x: number, y: number): { timeMs: number; price: number } | null;
227
+ /**
228
+ * Replaces the full set of price status lines (plus their shared style).
229
+ * lineStyle: 0=solid,1=dotted,2=dashed; color/bodyBg are packed 0xAARRGGBB;
230
+ * flags is a bitmask (1=draggable, 2=closable, 4=axis label, 8=extend left);
231
+ * align: 0=left,1=center,2=right. Pass an empty `lines` array to clear.
232
+ */
233
+ setPriceLines(spec: {
234
+ lines: {
235
+ price: number;
236
+ color: number;
237
+ width: number;
238
+ lineStyle: number;
239
+ text: string;
240
+ quantity: string;
241
+ flags: number;
242
+ }[];
243
+ bodyBg: number;
244
+ fontSizePx: number;
245
+ lineLengthFrac: number;
246
+ align: number;
247
+ hoverBoost: number;
248
+ }): void;
249
+ /**
250
+ * Hit-tests pixel (x, y) against the price lines. `part` is 0 for the line or
251
+ * its label body (the drag target) and 1 for the close button; close buttons
252
+ * win over lines and the nearest in y wins among several candidates. Only
253
+ * draggable lines report part 0 and only closable lines report part 1. Cheap
254
+ * to call at gesture rate (no rendering).
255
+ */
256
+ hitTestPriceLine(x: number, y: number): { index: number; part: number } | null;
257
+ /**
258
+ * Marks a price line's segment as hovered so it renders highlighted; -1 clears.
259
+ * `part` matches hitTestPriceLine. Touch has no hover state, so this is here
260
+ * for pointer devices and web parity.
261
+ */
262
+ setPriceLineHover(index: number, part: number): void;
263
+ /**
264
+ * Drives the live drag preview: the line, its label and its badge render at
265
+ * `price` with a ghost at the committed one. Pass -1 to end the preview. The
266
+ * committed price is untouched — restate setPriceLines to apply the move.
267
+ */
268
+ setPriceLineDrag(index: number, price: number): void;
165
269
  /** True while any axis-label fade is still in progress. Drives a RAF loop. */
166
270
  isAnimating(): boolean;
167
271
  render(): SkPicture | null;
package/src/theme.ts CHANGED
@@ -27,6 +27,7 @@ export const FLOAT_KEYS: Partial<Record<keyof VroomTheme, number>> = {
27
27
  candleRadius: 8, // VROOM_FLOAT_CANDLE_RADIUS_PX
28
28
  volumeRadius: 10, // VROOM_FLOAT_VOLUME_RADIUS_PX
29
29
  lineWidth: 11, // VROOM_FLOAT_LINE_WIDTH_PX
30
+ lineGradientOpacity: 12, // VROOM_FLOAT_LINE_GRADIENT_OPACITY
30
31
  };
31
32
 
32
33
  // Maps each boolean VroomTheme field to its VroomFloatKey index (pushed as 0/1).
package/src/types.ts CHANGED
@@ -12,11 +12,16 @@ export type {
12
12
  VisibleRange,
13
13
  RSIConfig,
14
14
  MASource,
15
+ MAKind,
15
16
  MovingAverageOverlay,
16
17
  VWAPConfig,
17
18
  BollingerBandsConfig,
19
+ VolumeConfig,
18
20
  MACDConfig,
19
21
  ChartType,
22
+ TransitionEasing,
23
+ PriceLine,
24
+ PriceLinesStyle,
20
25
  } from '@vroomchart/types';
21
26
 
22
27
  /**
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
+ import type { MutableRefObject } from 'react';
2
3
  import type { SkPicture } from '@shopify/react-native-skia';
3
4
 
4
5
  import NativeVroomChart from './NativeVroomChart';
@@ -11,8 +12,11 @@ import type {
11
12
  ChartType,
12
13
  MACDConfig,
13
14
  MovingAverageOverlay,
15
+ PriceLine,
16
+ PriceLinesStyle,
14
17
  RSIConfig,
15
18
  VisibleRange,
19
+ VolumeConfig,
16
20
  VroomTheme,
17
21
  VWAPConfig,
18
22
  } from './types';
@@ -28,17 +32,49 @@ const MA_SOURCES = [
28
32
  'ohlc4',
29
33
  ] as const;
30
34
 
35
+ // An unset style color marshals as the core's transparent inherit sentinel.
36
+ const inheritColor = (v: string | number | undefined): number =>
37
+ (v != null ? parseColor(v) : null) ?? 0;
38
+
31
39
  function overlayToNumeric(o: MovingAverageOverlay) {
32
40
  const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;
33
41
  return {
34
- kind: o.kind === 'ema' ? 1 : 0,
35
- period: o.length,
42
+ kind: o.maType === 'ema' ? 1 : 0,
43
+ period: o.period,
36
44
  source: srcIdx < 0 ? 0 : srcIdx,
37
45
  color: (o.color != null ? parseColor(o.color) : null) ?? 0xff2962ff,
38
46
  width: o.width ?? 1.5,
39
47
  };
40
48
  }
41
49
 
50
+ function rsiToSpec(cfg: RSIConfig | undefined) {
51
+ return {
52
+ enabled: cfg?.enabled ?? false,
53
+ period: cfg?.period ?? 14,
54
+ upperBand: cfg?.upperBand ?? 70,
55
+ lowerBand: cfg?.lowerBand ?? 30,
56
+ maPeriod: cfg?.maPeriod ?? 14,
57
+ maKind: cfg?.maType === 'ema' ? 1 : 0,
58
+ maVisible: cfg?.maVisible ?? true,
59
+ lineColor: inheritColor(cfg?.lineColor),
60
+ lineWidth: cfg?.lineWidth ?? -1,
61
+ lineVisible: cfg?.lineVisible ?? true,
62
+ maColor: inheritColor(cfg?.maColor),
63
+ maWidth: cfg?.maWidth ?? -1,
64
+ bandColor: inheritColor(cfg?.bandColor),
65
+ bandsVisible: cfg?.bandsVisible ?? true,
66
+ };
67
+ }
68
+
69
+ function vwapToSpec(cfg: VWAPConfig | undefined) {
70
+ return {
71
+ enabled: cfg?.enabled ?? false,
72
+ resetOffsetMin: cfg?.resetMinutes ?? 0,
73
+ color: inheritColor(cfg?.color),
74
+ width: cfg?.width ?? -1,
75
+ };
76
+ }
77
+
42
78
  // Bollinger defaults: blue bands / orange basis, matching the repo palette.
43
79
  const DEFAULT_BB_BAND_COLOR = 0xff2962ff;
44
80
  const DEFAULT_BB_BASIS_COLOR = 0xffff6d00;
@@ -50,7 +86,7 @@ function bollingerToSpec(cfg: BollingerBandsConfig | undefined) {
50
86
  period: cfg?.period ?? 20,
51
87
  mult: cfg?.stdDev ?? 2,
52
88
  source: srcIdx < 0 ? 0 : srcIdx,
53
- basisKind: cfg?.basis === 'ema' ? 1 : 0,
89
+ basisKind: cfg?.maType === 'ema' ? 1 : 0,
54
90
  upperColor:
55
91
  (cfg?.upperColor != null ? parseColor(cfg.upperColor) : null) ??
56
92
  DEFAULT_BB_BAND_COLOR,
@@ -63,11 +99,106 @@ function bollingerToSpec(cfg: BollingerBandsConfig | undefined) {
63
99
  (cfg?.lowerColor != null ? parseColor(cfg.lowerColor) : null) ??
64
100
  DEFAULT_BB_BAND_COLOR,
65
101
  lowerWidth: cfg?.lowerWidth ?? 1,
66
- fillEnabled: cfg?.fill ?? true,
102
+ fillEnabled: cfg?.fillVisible ?? true,
67
103
  fillOpacity: cfg?.fillOpacity ?? 0.1,
68
104
  };
69
105
  }
70
106
 
107
+ function macdToSpec(cfg: MACDConfig | undefined) {
108
+ const srcIdx = cfg?.source ? MA_SOURCES.indexOf(cfg.source) : 0;
109
+ return {
110
+ enabled: cfg?.enabled ?? false,
111
+ fast: cfg?.fast ?? 12,
112
+ slow: cfg?.slow ?? 26,
113
+ signal: cfg?.signal ?? 9,
114
+ source: srcIdx < 0 ? 0 : srcIdx,
115
+ maKind: cfg?.maType === 'sma' ? 0 : 1,
116
+ signalMaKind: cfg?.signalMaType === 'sma' ? 0 : 1,
117
+ lineColor: inheritColor(cfg?.lineColor),
118
+ lineWidth: cfg?.lineWidth ?? -1,
119
+ lineVisible: cfg?.lineVisible ?? true,
120
+ signalColor: inheritColor(cfg?.signalColor),
121
+ signalWidth: cfg?.signalWidth ?? -1,
122
+ signalVisible: cfg?.signalVisible ?? true,
123
+ histVisible: cfg?.histogramVisible ?? true,
124
+ histUpColor: inheritColor(cfg?.histogramUpColor),
125
+ histUpFadingColor: inheritColor(cfg?.histogramUpFadingColor),
126
+ histDownColor: inheritColor(cfg?.histogramDownColor),
127
+ histDownFadingColor: inheritColor(cfg?.histogramDownFadingColor),
128
+ zeroColor: inheritColor(cfg?.zeroLineColor),
129
+ zeroVisible: cfg?.zeroLineVisible ?? true,
130
+ };
131
+ }
132
+
133
+ // Unset style fields go down as the core's inherit sentinels (negative float,
134
+ // transparent color) rather than as literal defaults, so the theme keys stay in
135
+ // charge of anything the consumer didn't set.
136
+ function volumeToSpec(cfg: VolumeConfig | undefined) {
137
+ return {
138
+ enabled: cfg?.enabled ?? true,
139
+ heightFrac: cfg?.height ?? -1,
140
+ opacity: cfg?.opacity ?? -1,
141
+ radiusPx: cfg?.radius ?? -1,
142
+ upColor: (cfg?.upColor != null ? parseColor(cfg.upColor) : null) ?? 0,
143
+ downColor: (cfg?.downColor != null ? parseColor(cfg.downColor) : null) ?? 0,
144
+ };
145
+ }
146
+
147
+ // Price-line defaults: a soft red dotted rule with a dark translucent label,
148
+ // close in weight to the current-price indicator it sits beside.
149
+ const DEFAULT_PRICE_LINE_COLOR = 0xffef5350;
150
+ const DEFAULT_PRICE_LINE_BODY_BG = 0xd91c2128;
151
+ const DEFAULT_PRICE_LINE_HOVER_BOOST = 1.25;
152
+
153
+ const LINE_STYLES = { solid: 0, dotted: 1, dashed: 2 } as const;
154
+
155
+ // Mirrors VroomPriceLineFlags in packages/core/include/vroom/vroom_chart.h.
156
+ const PRICE_LINE_DRAGGABLE = 1 << 0;
157
+ const PRICE_LINE_CLOSABLE = 1 << 1;
158
+ const PRICE_LINE_AXIS_LABEL = 1 << 2;
159
+ const PRICE_LINE_EXTEND_LEFT = 1 << 3;
160
+
161
+ /** The price lines + their shared style, as the chart's props express them. */
162
+ export type PriceLinesProp = {
163
+ lines: PriceLine[];
164
+ style?: PriceLinesStyle;
165
+ /**
166
+ * Whether the host supplied a close handler. The close button is
167
+ * callback-gated, so with nothing for it to do it isn't drawn at all.
168
+ */
169
+ hasCloseHandler: boolean;
170
+ };
171
+
172
+ function priceLinesToSpec(cfg: PriceLinesProp) {
173
+ return {
174
+ lines: cfg.lines.map((l) => ({
175
+ price: l.price,
176
+ color:
177
+ (l.color != null ? parseColor(l.color) : null) ?? DEFAULT_PRICE_LINE_COLOR,
178
+ width: l.width ?? 1,
179
+ lineStyle: LINE_STYLES[l.lineStyle ?? 'dotted'],
180
+ text: l.text ?? '',
181
+ quantity: l.quantity ?? '',
182
+ flags:
183
+ (l.draggable ? PRICE_LINE_DRAGGABLE : 0) |
184
+ (cfg.hasCloseHandler && l.closable !== false ? PRICE_LINE_CLOSABLE : 0) |
185
+ (l.axisLabel !== false ? PRICE_LINE_AXIS_LABEL : 0) |
186
+ (l.extendLeft !== false ? PRICE_LINE_EXTEND_LEFT : 0),
187
+ })),
188
+ bodyBg:
189
+ (cfg.style?.bodyBackground != null ? parseColor(cfg.style.bodyBackground) : null) ??
190
+ DEFAULT_PRICE_LINE_BODY_BG,
191
+ fontSizePx: cfg.style?.fontSize ?? 0,
192
+ lineLengthFrac: cfg.style?.inset ?? 0,
193
+ align: cfg.style?.align === 'left' ? 0 : cfg.style?.align === 'center' ? 1 : 2,
194
+ hoverBoost: cfg.style?.hoverBoost ?? DEFAULT_PRICE_LINE_HOVER_BOOST,
195
+ };
196
+ }
197
+
198
+ // Cleared overlay: no lines (the style values are irrelevant, but the spec shape
199
+ // requires them).
200
+ const EMPTY_PRICE_LINES = priceLinesToSpec({ lines: [], hasCloseHandler: false });
201
+
71
202
  let installed = false;
72
203
  function ensureInstalled(): void {
73
204
  if (installed) return;
@@ -79,10 +210,19 @@ function ensureInstalled(): void {
79
210
  installed = true;
80
211
  }
81
212
 
213
+ /** Progress + curve of the staggered volume-bar collapse. See setVolumeCollapse. */
214
+ export type VolumeCollapse = { t: number; easing: number };
215
+
82
216
  export type ChartCoreState = {
83
217
  handle: ChartHandle | null;
84
218
  /** Picture freshly rendered after the latest data/size/range push. */
85
219
  picture: SkPicture | null;
220
+ /**
221
+ * The last volume collapse handed to the core, or null before the first push.
222
+ * VroomChart's animation loop owns this — it lives here only so the data effect
223
+ * can restore it, since setVolume snaps the scalar (see below).
224
+ */
225
+ volumeCollapseRef: MutableRefObject<VolumeCollapse | null>;
86
226
  };
87
227
 
88
228
  // Owns a ChartHandle and produces an "initial" picture whenever data, size,
@@ -101,12 +241,15 @@ export function useChartCore(
101
241
  movingAverages?: MovingAverageOverlay[],
102
242
  vwap?: VWAPConfig,
103
243
  bollingerBands?: BollingerBandsConfig,
244
+ volume?: VolumeConfig,
245
+ priceLines?: PriceLinesProp,
104
246
  ): ChartCoreState {
105
247
  const handleRef = useRef<ChartHandle | null>(null);
106
248
  // Push setDefaultCandleWidth only once (first load): setCandles re-runs on
107
249
  // every data change, and the core setter re-frames when candles are present,
108
250
  // so re-pushing would snap the view away from the user's pan/zoom.
109
251
  const defaultWidthAppliedRef = useRef(false);
252
+ const volumeCollapseRef = useRef<VolumeCollapse | null>(null);
110
253
  const [picture, setPicture] = useState<SkPicture | null>(null);
111
254
 
112
255
  if (!handleRef.current && size.width > 0 && size.height > 0) {
@@ -130,6 +273,8 @@ export function useChartCore(
130
273
  const maKey = movingAverages ? JSON.stringify(movingAverages) : '';
131
274
  const vwapKey = vwap ? JSON.stringify(vwap) : '';
132
275
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : '';
276
+ const volumeKey = volume ? JSON.stringify(volume) : '';
277
+ const priceLinesKey = priceLines ? JSON.stringify(priceLines) : '';
133
278
 
134
279
  useEffect(() => {
135
280
  const h = handleRef.current;
@@ -158,35 +303,28 @@ export function useChartCore(
158
303
  if (theme) {
159
304
  applyTheme(h, theme);
160
305
  }
161
- h.setRSI(
162
- rsi?.enabled ?? false,
163
- rsi?.period ?? 14,
164
- rsi?.upperBand ?? 70,
165
- rsi?.lowerBand ?? 30,
166
- rsi?.maEnabled ?? true,
167
- rsi?.maPeriod ?? 14,
168
- );
169
- h.setMACD(
170
- macd?.enabled ?? false,
171
- macd?.fast ?? 12,
172
- macd?.slow ?? 26,
173
- macd?.signal ?? 9,
174
- );
306
+ h.setRSI(rsiToSpec(rsi));
307
+ h.setMACD(macdToSpec(macd));
175
308
  h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
176
- h.setVWAP(
177
- vwap?.enabled ?? false,
178
- vwap?.resetMinutes ?? 0,
179
- (vwap?.color != null ? parseColor(vwap.color) : null) ?? 0xff00bcd4,
180
- vwap?.width ?? 1.5,
181
- );
309
+ h.setVWAP(vwapToSpec(vwap));
182
310
  h.setBollinger(bollingerToSpec(bollingerBands));
311
+ h.setVolume(volumeToSpec(volume));
312
+ // setVolume snaps the collapse scalar to its `enabled`, which would cut a
313
+ // toggle animation short whenever this effect re-runs (a streaming candle, a
314
+ // resize). Hand the in-flight value back; VroomChart's loop drives it from
315
+ // there.
316
+ const collapse = volumeCollapseRef.current;
317
+ if (collapse) h.setVolumeCollapse(collapse.t, collapse.easing);
318
+ h.setPriceLines(
319
+ priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES,
320
+ );
183
321
  // TODO(rn-parity): mirror the web `liquidity` overlay here (setLiquidity +
184
322
  // the VroomBand structs in the JSI handle) — web-only for now.
185
323
  setPicture(h.render());
186
- // theme/rsi/macd/movingAverages/vwap/bollingerBands are represented by
187
- // their *Key deps.
324
+ // theme/rsi/macd/movingAverages/vwap/bollingerBands/volume/priceLines are
325
+ // represented by their *Key deps.
188
326
  // eslint-disable-next-line react-hooks/exhaustive-deps
189
- }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey]);
327
+ }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey]);
190
328
 
191
- return { handle: handleRef.current, picture };
329
+ return { handle: handleRef.current, picture, volumeCollapseRef };
192
330
  }