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/lib/index.mjs CHANGED
@@ -72,8 +72,10 @@ var FLOAT_KEYS = {
72
72
  // VROOM_FLOAT_CANDLE_RADIUS_PX
73
73
  volumeRadius: 10,
74
74
  // VROOM_FLOAT_VOLUME_RADIUS_PX
75
- lineWidth: 11
75
+ lineWidth: 11,
76
76
  // VROOM_FLOAT_LINE_WIDTH_PX
77
+ lineGradientOpacity: 12
78
+ // VROOM_FLOAT_LINE_GRADIENT_OPACITY
77
79
  };
78
80
  var BOOL_KEYS = {
79
81
  wickRoundCap: 9
@@ -119,16 +121,43 @@ var MA_SOURCES = [
119
121
  "hlc3",
120
122
  "ohlc4"
121
123
  ];
124
+ var inheritColor = (v) => (v != null ? parseColor(v) : null) ?? 0;
122
125
  function overlayToNumeric(o) {
123
126
  const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;
124
127
  return {
125
- kind: o.kind === "ema" ? 1 : 0,
126
- period: o.length,
128
+ kind: o.maType === "ema" ? 1 : 0,
129
+ period: o.period,
127
130
  source: srcIdx < 0 ? 0 : srcIdx,
128
131
  color: (o.color != null ? parseColor(o.color) : null) ?? 4280902399,
129
132
  width: o.width ?? 1.5
130
133
  };
131
134
  }
135
+ function rsiToSpec(cfg) {
136
+ return {
137
+ enabled: cfg?.enabled ?? false,
138
+ period: cfg?.period ?? 14,
139
+ upperBand: cfg?.upperBand ?? 70,
140
+ lowerBand: cfg?.lowerBand ?? 30,
141
+ maPeriod: cfg?.maPeriod ?? 14,
142
+ maKind: cfg?.maType === "ema" ? 1 : 0,
143
+ maVisible: cfg?.maVisible ?? true,
144
+ lineColor: inheritColor(cfg?.lineColor),
145
+ lineWidth: cfg?.lineWidth ?? -1,
146
+ lineVisible: cfg?.lineVisible ?? true,
147
+ maColor: inheritColor(cfg?.maColor),
148
+ maWidth: cfg?.maWidth ?? -1,
149
+ bandColor: inheritColor(cfg?.bandColor),
150
+ bandsVisible: cfg?.bandsVisible ?? true
151
+ };
152
+ }
153
+ function vwapToSpec(cfg) {
154
+ return {
155
+ enabled: cfg?.enabled ?? false,
156
+ resetOffsetMin: cfg?.resetMinutes ?? 0,
157
+ color: inheritColor(cfg?.color),
158
+ width: cfg?.width ?? -1
159
+ };
160
+ }
132
161
  var DEFAULT_BB_BAND_COLOR = 4280902399;
133
162
  var DEFAULT_BB_BASIS_COLOR = 4294929664;
134
163
  function bollingerToSpec(cfg) {
@@ -138,17 +167,79 @@ function bollingerToSpec(cfg) {
138
167
  period: cfg?.period ?? 20,
139
168
  mult: cfg?.stdDev ?? 2,
140
169
  source: srcIdx < 0 ? 0 : srcIdx,
141
- basisKind: cfg?.basis === "ema" ? 1 : 0,
170
+ basisKind: cfg?.maType === "ema" ? 1 : 0,
142
171
  upperColor: (cfg?.upperColor != null ? parseColor(cfg.upperColor) : null) ?? DEFAULT_BB_BAND_COLOR,
143
172
  upperWidth: cfg?.upperWidth ?? 1,
144
173
  middleColor: (cfg?.middleColor != null ? parseColor(cfg.middleColor) : null) ?? DEFAULT_BB_BASIS_COLOR,
145
174
  middleWidth: cfg?.middleWidth ?? 1,
146
175
  lowerColor: (cfg?.lowerColor != null ? parseColor(cfg.lowerColor) : null) ?? DEFAULT_BB_BAND_COLOR,
147
176
  lowerWidth: cfg?.lowerWidth ?? 1,
148
- fillEnabled: cfg?.fill ?? true,
177
+ fillEnabled: cfg?.fillVisible ?? true,
149
178
  fillOpacity: cfg?.fillOpacity ?? 0.1
150
179
  };
151
180
  }
181
+ function macdToSpec(cfg) {
182
+ const srcIdx = cfg?.source ? MA_SOURCES.indexOf(cfg.source) : 0;
183
+ return {
184
+ enabled: cfg?.enabled ?? false,
185
+ fast: cfg?.fast ?? 12,
186
+ slow: cfg?.slow ?? 26,
187
+ signal: cfg?.signal ?? 9,
188
+ source: srcIdx < 0 ? 0 : srcIdx,
189
+ maKind: cfg?.maType === "sma" ? 0 : 1,
190
+ signalMaKind: cfg?.signalMaType === "sma" ? 0 : 1,
191
+ lineColor: inheritColor(cfg?.lineColor),
192
+ lineWidth: cfg?.lineWidth ?? -1,
193
+ lineVisible: cfg?.lineVisible ?? true,
194
+ signalColor: inheritColor(cfg?.signalColor),
195
+ signalWidth: cfg?.signalWidth ?? -1,
196
+ signalVisible: cfg?.signalVisible ?? true,
197
+ histVisible: cfg?.histogramVisible ?? true,
198
+ histUpColor: inheritColor(cfg?.histogramUpColor),
199
+ histUpFadingColor: inheritColor(cfg?.histogramUpFadingColor),
200
+ histDownColor: inheritColor(cfg?.histogramDownColor),
201
+ histDownFadingColor: inheritColor(cfg?.histogramDownFadingColor),
202
+ zeroColor: inheritColor(cfg?.zeroLineColor),
203
+ zeroVisible: cfg?.zeroLineVisible ?? true
204
+ };
205
+ }
206
+ function volumeToSpec(cfg) {
207
+ return {
208
+ enabled: cfg?.enabled ?? true,
209
+ heightFrac: cfg?.height ?? -1,
210
+ opacity: cfg?.opacity ?? -1,
211
+ radiusPx: cfg?.radius ?? -1,
212
+ upColor: (cfg?.upColor != null ? parseColor(cfg.upColor) : null) ?? 0,
213
+ downColor: (cfg?.downColor != null ? parseColor(cfg.downColor) : null) ?? 0
214
+ };
215
+ }
216
+ var DEFAULT_PRICE_LINE_COLOR = 4293874512;
217
+ var DEFAULT_PRICE_LINE_BODY_BG = 3642499368;
218
+ var DEFAULT_PRICE_LINE_HOVER_BOOST = 1.25;
219
+ var LINE_STYLES = { solid: 0, dotted: 1, dashed: 2 };
220
+ var PRICE_LINE_DRAGGABLE = 1 << 0;
221
+ var PRICE_LINE_CLOSABLE = 1 << 1;
222
+ var PRICE_LINE_AXIS_LABEL = 1 << 2;
223
+ var PRICE_LINE_EXTEND_LEFT = 1 << 3;
224
+ function priceLinesToSpec(cfg) {
225
+ return {
226
+ lines: cfg.lines.map((l) => ({
227
+ price: l.price,
228
+ color: (l.color != null ? parseColor(l.color) : null) ?? DEFAULT_PRICE_LINE_COLOR,
229
+ width: l.width ?? 1,
230
+ lineStyle: LINE_STYLES[l.lineStyle ?? "dotted"],
231
+ text: l.text ?? "",
232
+ quantity: l.quantity ?? "",
233
+ flags: (l.draggable ? PRICE_LINE_DRAGGABLE : 0) | (cfg.hasCloseHandler && l.closable !== false ? PRICE_LINE_CLOSABLE : 0) | (l.axisLabel !== false ? PRICE_LINE_AXIS_LABEL : 0) | (l.extendLeft !== false ? PRICE_LINE_EXTEND_LEFT : 0)
234
+ })),
235
+ bodyBg: (cfg.style?.bodyBackground != null ? parseColor(cfg.style.bodyBackground) : null) ?? DEFAULT_PRICE_LINE_BODY_BG,
236
+ fontSizePx: cfg.style?.fontSize ?? 0,
237
+ lineLengthFrac: cfg.style?.inset ?? 0,
238
+ align: cfg.style?.align === "left" ? 0 : cfg.style?.align === "center" ? 1 : 2,
239
+ hoverBoost: cfg.style?.hoverBoost ?? DEFAULT_PRICE_LINE_HOVER_BOOST
240
+ };
241
+ }
242
+ var EMPTY_PRICE_LINES = priceLinesToSpec({ lines: [], hasCloseHandler: false });
152
243
  var installed = false;
153
244
  function ensureInstalled() {
154
245
  if (installed) return;
@@ -159,9 +250,10 @@ function ensureInstalled() {
159
250
  }
160
251
  installed = true;
161
252
  }
162
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands) {
253
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines) {
163
254
  const handleRef = useRef(null);
164
255
  const defaultWidthAppliedRef = useRef(false);
256
+ const volumeCollapseRef = useRef(null);
165
257
  const [picture, setPicture] = useState(null);
166
258
  if (!handleRef.current && size.width > 0 && size.height > 0) {
167
259
  ensureInstalled();
@@ -176,6 +268,8 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
176
268
  const maKey = movingAverages ? JSON.stringify(movingAverages) : "";
177
269
  const vwapKey = vwap ? JSON.stringify(vwap) : "";
178
270
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
271
+ const volumeKey = volume ? JSON.stringify(volume) : "";
272
+ const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
179
273
  useEffect(() => {
180
274
  const h = handleRef.current;
181
275
  if (!h) return;
@@ -193,31 +287,44 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
193
287
  if (theme) {
194
288
  applyTheme(h, theme);
195
289
  }
196
- h.setRSI(
197
- rsi?.enabled ?? false,
198
- rsi?.period ?? 14,
199
- rsi?.upperBand ?? 70,
200
- rsi?.lowerBand ?? 30,
201
- rsi?.maEnabled ?? true,
202
- rsi?.maPeriod ?? 14
203
- );
204
- h.setMACD(
205
- macd?.enabled ?? false,
206
- macd?.fast ?? 12,
207
- macd?.slow ?? 26,
208
- macd?.signal ?? 9
209
- );
290
+ h.setRSI(rsiToSpec(rsi));
291
+ h.setMACD(macdToSpec(macd));
210
292
  h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
211
- h.setVWAP(
212
- vwap?.enabled ?? false,
213
- vwap?.resetMinutes ?? 0,
214
- (vwap?.color != null ? parseColor(vwap.color) : null) ?? 4278238420,
215
- vwap?.width ?? 1.5
216
- );
293
+ h.setVWAP(vwapToSpec(vwap));
217
294
  h.setBollinger(bollingerToSpec(bollingerBands));
295
+ h.setVolume(volumeToSpec(volume));
296
+ const collapse = volumeCollapseRef.current;
297
+ if (collapse) h.setVolumeCollapse(collapse.t, collapse.easing);
298
+ h.setPriceLines(
299
+ priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
300
+ );
218
301
  setPicture(h.render());
219
- }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey]);
220
- return { handle: handleRef.current, picture };
302
+ }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey]);
303
+ return { handle: handleRef.current, picture, volumeCollapseRef };
304
+ }
305
+
306
+ // src/easing.ts
307
+ function ease(kind, p) {
308
+ switch (kind) {
309
+ case "linear":
310
+ return p;
311
+ case "ease-in":
312
+ return p * p;
313
+ case "ease-out":
314
+ return p * (2 - p);
315
+ default:
316
+ return p * p * (3 - 2 * p);
317
+ }
318
+ }
319
+ var EASINGS = [
320
+ "linear",
321
+ "ease-in",
322
+ "ease-out",
323
+ "ease-in-out"
324
+ ];
325
+ function easingIndex(kind) {
326
+ const i = kind ? EASINGS.indexOf(kind) : -1;
327
+ return i < 0 ? EASINGS.indexOf("ease-in-out") : i;
221
328
  }
222
329
 
223
330
  // src/VroomChart.tsx
@@ -231,15 +338,22 @@ function VroomChart(props) {
231
338
  defaultCandleWidth,
232
339
  chartType,
233
340
  transitionMs,
341
+ transitionEasing,
234
342
  theme,
235
343
  rsi,
236
344
  macd,
237
345
  movingAverages,
238
346
  vwap,
239
347
  bollingerBands,
348
+ volume,
240
349
  crosshairOffset = 40,
241
350
  onCrosshair,
242
- onViewportChange
351
+ onViewportChange,
352
+ priceLines,
353
+ priceLinesStyle,
354
+ onPriceLineDrag,
355
+ onPriceLineDragEnd,
356
+ onPriceLineClose
243
357
  } = props;
244
358
  const [measured, setMeasured] = useState2({ width: 0, height: 0 });
245
359
  const width = widthProp ?? measured.width;
@@ -251,7 +365,15 @@ function VroomChart(props) {
251
365
  (prev) => prev.width === w && prev.height === h ? prev : { width: w, height: h }
252
366
  );
253
367
  }, []);
254
- const { handle, picture } = useChartCore(
368
+ const priceLinesProp = useMemo(
369
+ () => priceLines ? {
370
+ lines: priceLines,
371
+ style: priceLinesStyle,
372
+ hasCloseHandler: onPriceLineClose != null
373
+ } : void 0,
374
+ [priceLines, priceLinesStyle, onPriceLineClose]
375
+ );
376
+ const { handle, picture, volumeCollapseRef } = useChartCore(
255
377
  candles,
256
378
  { width, height },
257
379
  visibleRange,
@@ -262,7 +384,9 @@ function VroomChart(props) {
262
384
  macd,
263
385
  movingAverages,
264
386
  vwap,
265
- bollingerBands
387
+ bollingerBands,
388
+ volume,
389
+ priceLinesProp
266
390
  );
267
391
  const emptyPicture = useMemo(() => {
268
392
  const rec = Skia.PictureRecorder();
@@ -309,6 +433,8 @@ function VroomChart(props) {
309
433
  const morphRaf = useRef2(null);
310
434
  const morphFade = useRef2(null);
311
435
  const morphHandle = useRef2(null);
436
+ const easingRef = useRef2(transitionEasing);
437
+ easingRef.current = transitionEasing;
312
438
  useEffect2(() => {
313
439
  if (!handle) return void 0;
314
440
  const target = chartType === "line" ? 1 : 0;
@@ -338,8 +464,7 @@ function VroomChart(props) {
338
464
  const step = (now) => {
339
465
  if (startTs == null) startTs = now;
340
466
  const prog = Math.min(1, (now - startTs) / dur);
341
- const e = prog * prog * (3 - 2 * prog);
342
- const fade = from + (target - from) * e;
467
+ const fade = from + (target - from) * ease(easingRef.current, prog);
343
468
  morphFade.current = fade;
344
469
  handle.setMorph(fade, fade);
345
470
  const p = handle.render();
@@ -362,6 +487,51 @@ function VroomChart(props) {
362
487
  }
363
488
  };
364
489
  }, [handle, chartType, transitionMs, pictureSV]);
490
+ const volumeRaf = useRef2(null);
491
+ const volumeHandle = useRef2(null);
492
+ useEffect2(() => {
493
+ if (!handle) return void 0;
494
+ const target = volume?.enabled ?? true ? 0 : 1;
495
+ const easing = easingIndex(easingRef.current);
496
+ if (volumeHandle.current !== handle || volumeCollapseRef.current == null) {
497
+ volumeHandle.current = handle;
498
+ volumeCollapseRef.current = { t: target, easing };
499
+ return void 0;
500
+ }
501
+ if (volumeCollapseRef.current.t === target) return void 0;
502
+ if (volumeRaf.current != null) {
503
+ cancelAnimationFrame(volumeRaf.current);
504
+ volumeRaf.current = null;
505
+ }
506
+ const dur = Math.max(0, transitionMs ?? 300);
507
+ if (dur === 0) {
508
+ volumeCollapseRef.current = { t: target, easing };
509
+ handle.setVolumeCollapse(target, easing);
510
+ const p = handle.render();
511
+ if (p) pictureSV.value = p;
512
+ return void 0;
513
+ }
514
+ const from = volumeCollapseRef.current.t;
515
+ let startTs = null;
516
+ const step = (now) => {
517
+ if (startTs == null) startTs = now;
518
+ const prog = Math.min(1, (now - startTs) / dur);
519
+ const t = prog < 1 ? from + (target - from) * prog : target;
520
+ const kind = easingIndex(easingRef.current);
521
+ volumeCollapseRef.current = { t, easing: kind };
522
+ handle.setVolumeCollapse(t, kind);
523
+ const p = handle.render();
524
+ if (p) pictureSV.value = p;
525
+ volumeRaf.current = prog < 1 ? requestAnimationFrame(step) : null;
526
+ };
527
+ volumeRaf.current = requestAnimationFrame(step);
528
+ return () => {
529
+ if (volumeRaf.current != null) {
530
+ cancelAnimationFrame(volumeRaf.current);
531
+ volumeRaf.current = null;
532
+ }
533
+ };
534
+ }, [handle, volume?.enabled, transitionMs, pictureSV, volumeCollapseRef]);
365
535
  const hitAxis = useCallback(
366
536
  (x, y) => {
367
537
  if (!handle) return "chart";
@@ -375,12 +545,33 @@ function VroomChart(props) {
375
545
  },
376
546
  [handle, width, height]
377
547
  );
378
- const panMode = useRef2(
379
- "chart"
548
+ const hitPriceLine = useCallback(
549
+ (x, y) => {
550
+ if (!handle || !priceLines?.length) return null;
551
+ const hit = handle.hitTestPriceLine(x, y);
552
+ const line = hit ? priceLines[hit.index] : void 0;
553
+ return hit && line ? { index: hit.index, part: hit.part, line } : null;
554
+ },
555
+ [handle, priceLines]
380
556
  );
557
+ const priceDrag = useRef2(
558
+ null
559
+ );
560
+ const panMode = useRef2("chart");
381
561
  const pan = Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
382
562
  cancelDecay();
383
563
  panMode.current = hitAxis(e.x, e.y);
564
+ priceDrag.current = null;
565
+ if (handle && panMode.current === "chart" && !crosshairActive.current) {
566
+ const pl = hitPriceLine(e.x, e.y);
567
+ if (pl && pl.part === 0) {
568
+ panMode.current = "price-line";
569
+ priceDrag.current = { index: pl.index, id: pl.line.id, price: pl.line.price };
570
+ handle.setPriceLineDrag(pl.index, pl.line.price);
571
+ const p = handle.render();
572
+ if (p) pictureSV.value = p;
573
+ }
574
+ }
384
575
  }).onChange((e) => {
385
576
  if (!handle) return;
386
577
  let next = null;
@@ -390,6 +581,15 @@ function VroomChart(props) {
390
581
  next = handle.scaleTimeAxis(e.changeX);
391
582
  } else if (panMode.current === "indicator") {
392
583
  next = handle.pan(e.changeX, 0);
584
+ } else if (panMode.current === "price-line") {
585
+ const g = priceDrag.current;
586
+ if (!g) return;
587
+ const c = handle.coordAt(e.x, e.y);
588
+ if (!c) return;
589
+ g.price = c.price;
590
+ handle.setPriceLineDrag(g.index, c.price);
591
+ onPriceLineDrag?.(g.id, c.price);
592
+ next = handle.render();
393
593
  } else if (crosshairActive.current) {
394
594
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
395
595
  if (ch) pictureSV.value = ch;
@@ -407,6 +607,15 @@ function VroomChart(props) {
407
607
  maybeStartAnim();
408
608
  }).onEnd((e) => {
409
609
  if (!handle) return;
610
+ if (panMode.current === "price-line") {
611
+ const g = priceDrag.current;
612
+ priceDrag.current = null;
613
+ handle.setPriceLineDrag(-1, 0);
614
+ const p = handle.render();
615
+ if (p) pictureSV.value = p;
616
+ if (g) onPriceLineDragEnd?.(g.id, g.price);
617
+ return;
618
+ }
410
619
  if (panMode.current === "chart" && crosshairActive.current) return;
411
620
  onViewportChange?.(0, 0);
412
621
  if (panMode.current !== "chart" && panMode.current !== "indicator") return;
@@ -483,6 +692,7 @@ function VroomChart(props) {
483
692
  const longPress = Gesture.LongPress().runOnJS(true).onStart((e) => {
484
693
  if (!handle) return;
485
694
  if (hitAxis(e.x, e.y) !== "chart") return;
695
+ if (hitPriceLine(e.x, e.y)) return;
486
696
  cancelDecay();
487
697
  crosshairActive.current = true;
488
698
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
@@ -499,7 +709,13 @@ function VroomChart(props) {
499
709
  });
500
710
  });
501
711
  const tap = Gesture.Tap().runOnJS(true).onStart((e) => {
502
- if (!handle || !crosshairActive.current) return;
712
+ if (!handle) return;
713
+ const pl = hitPriceLine(e.x, e.y);
714
+ if (pl && pl.part === 1) {
715
+ onPriceLineClose?.(pl.line.id);
716
+ return;
717
+ }
718
+ if (!crosshairActive.current) return;
503
719
  if (hitAxis(e.x, e.y) !== "chart") return;
504
720
  crosshairActive.current = false;
505
721
  const ch = handle.clearCrosshair();