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
@@ -1,6 +1,8 @@
1
1
  #include "VroomChartHostObject.h"
2
2
 
3
3
  #include <cstring>
4
+ #include <string>
5
+ #include <vector>
4
6
 
5
7
  #include "chart_internal.h"
6
8
  #include "vroom/vroom_chart.h"
@@ -27,7 +29,7 @@ ChartHostObject::~ChartHostObject() {
27
29
  std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
28
30
  jsi::Runtime& rt) {
29
31
  std::vector<jsi::PropNameID> out;
30
- out.reserve(24);
32
+ out.reserve(29);
31
33
  out.push_back(jsi::PropNameID::forAscii(rt, "setCandles"));
32
34
  out.push_back(jsi::PropNameID::forAscii(rt, "setSize"));
33
35
  out.push_back(jsi::PropNameID::forAscii(rt, "setColor"));
@@ -52,10 +54,78 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
52
54
  out.push_back(jsi::PropNameID::forAscii(rt, "setOverlays"));
53
55
  out.push_back(jsi::PropNameID::forAscii(rt, "setVWAP"));
54
56
  out.push_back(jsi::PropNameID::forAscii(rt, "setBollinger"));
57
+ out.push_back(jsi::PropNameID::forAscii(rt, "setVolume"));
58
+ out.push_back(jsi::PropNameID::forAscii(rt, "setVolumeCollapse"));
59
+ out.push_back(jsi::PropNameID::forAscii(rt, "coordAt"));
60
+ out.push_back(jsi::PropNameID::forAscii(rt, "setPriceLines"));
61
+ out.push_back(jsi::PropNameID::forAscii(rt, "hitTestPriceLine"));
62
+ out.push_back(jsi::PropNameID::forAscii(rt, "setPriceLineHover"));
63
+ out.push_back(jsi::PropNameID::forAscii(rt, "setPriceLineDrag"));
55
64
  out.push_back(jsi::PropNameID::forAscii(rt, "render"));
56
65
  return out;
57
66
  }
58
67
 
68
+ #if defined(__ANDROID__)
69
+ // Android only: RN-Skia is compiled into its own librnskia.so, separate from
70
+ // libvroomchart.so. Constructing a RNSkia::JsiSkPicture directly here (as iOS
71
+ // does, below) gives it a vtable/typeinfo from *our* .so; when RN-Skia's own
72
+ // compiled code later consumes it (e.g. Convertor.h's
73
+ // `getPropertyValue<sk_sp<SkPicture>>`, which does
74
+ // `value.asObject(rt).asHostObject<JsiSkPicture>(rt)` — a dynamic_pointer_cast
75
+ // under the hood), the cast fails cross-.so with "Object is not a HostObject
76
+ // of desired type", because the object's *runtime* type was never actually
77
+ // compiled inside librnskia.so. iOS statically links everything into one
78
+ // binary, so no such mismatch exists there.
79
+ //
80
+ // The fix: build the picture through RN-Skia's own public JS API instead
81
+ // (`Skia.Picture.MakePicture(bytes)`, the same call `require(...).png`-style
82
+ // static pictures use), so the resulting JsiSkPicture is genuinely
83
+ // constructed by librnskia.so's own code. This costs a serialize +
84
+ // re-parse of the picture's draw ops per call — real overhead on a gesture
85
+ // hot path — but is the only ABI-safe option found so far. Revisit if
86
+ // profiling shows this mattering (e.g. a merged-.so build, or a lighter
87
+ // bridge that avoids the round trip).
88
+ #include "include/core/SkData.h"
89
+
90
+ namespace {
91
+ class SkDataMutableBuffer : public facebook::jsi::MutableBuffer {
92
+ public:
93
+ explicit SkDataMutableBuffer(sk_sp<SkData> data) : data_(std::move(data)) {}
94
+ size_t size() const override { return data_->size(); }
95
+ uint8_t* data() override {
96
+ return const_cast<uint8_t*>(
97
+ static_cast<const uint8_t*>(data_->data()));
98
+ }
99
+
100
+ private:
101
+ sk_sp<SkData> data_;
102
+ };
103
+ } // namespace
104
+
105
+ static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
106
+ const sk_sp<SkPicture>& pic) {
107
+ if (!pic) return facebook::jsi::Value::null();
108
+ sk_sp<SkData> serialized = pic->serialize();
109
+ if (!serialized) return facebook::jsi::Value::null();
110
+
111
+ auto buffer = std::make_shared<SkDataMutableBuffer>(std::move(serialized));
112
+ jsi::ArrayBuffer arrayBuffer(rt, buffer);
113
+
114
+ auto skiaApi = rt.global().getProperty(rt, "SkiaApi");
115
+ if (!skiaApi.isObject()) return facebook::jsi::Value::null();
116
+ auto pictureFactory = skiaApi.asObject(rt).getProperty(rt, "Picture");
117
+ if (!pictureFactory.isObject()) return facebook::jsi::Value::null();
118
+ auto makePicture =
119
+ pictureFactory.asObject(rt).getPropertyAsFunction(rt, "MakePicture");
120
+
121
+ // Mirrors JsiSkPictureFactory::MakePicture's expected argument shape: an
122
+ // object with a `.buffer` property holding the ArrayBuffer (matching a
123
+ // Uint8Array-like value; the factory ignores everything else about it).
124
+ jsi::Object arg(rt);
125
+ arg.setProperty(rt, "buffer", arrayBuffer);
126
+ return makePicture.call(rt, arg);
127
+ }
128
+ #else
59
129
  // Shared helper: wraps a fresh picture for return to JS, with memory pressure
60
130
  // reported to Hermes so GC keeps up under gesture-rate churn.
61
131
  static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
@@ -66,6 +136,7 @@ static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
66
136
  return JSI_CREATE_HOST_OBJECT_WITH_MEMORY_PRESSURE(rt, host,
67
137
  /*context=*/nullptr);
68
138
  }
139
+ #endif
69
140
 
70
141
  jsi::Value ChartHostObject::get(jsi::Runtime& rt,
71
142
  const jsi::PropNameID& propName) {
@@ -450,46 +521,106 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
450
521
  }
451
522
 
452
523
  if (name == "setRSI") {
453
- // setRSI(enabled, period, upperBand, lowerBand, maEnabled, maPeriod) —
454
- // configures the RSI pane. No render; the next render() picks it up.
524
+ // setRSI({enabled, period, upperBand, lowerBand, maPeriod, maKind,
525
+ // maVisible, lineColor, lineWidth, lineVisible, maColor, maWidth,
526
+ // bandColor, bandsVisible}) — configures the RSI pane. No render; the next
527
+ // render() picks it up.
455
528
  return jsi::Function::createFromHostFunction(
456
529
  rt,
457
530
  jsi::PropNameID::forAscii(rt, "setRSI"),
458
- 6,
459
- [this](jsi::Runtime& /*rt2*/,
531
+ 1,
532
+ [this](jsi::Runtime& rt2,
460
533
  const jsi::Value& /*thisVal*/,
461
534
  const jsi::Value* args,
462
535
  size_t count) -> jsi::Value {
463
- if (count < 6) return jsi::Value::undefined();
464
- const bool enabled = args[0].asBool();
465
- const int period = static_cast<int>(args[1].asNumber());
466
- const double upper = args[2].asNumber();
467
- const double lower = args[3].asNumber();
468
- const bool ma_enabled = args[4].asBool();
469
- const int ma_period = static_cast<int>(args[5].asNumber());
470
- vroom_chart_set_rsi(chart_, enabled, period, upper, lower, ma_enabled,
471
- ma_period);
536
+ if (count < 1 || !args[0].isObject()) return jsi::Value::undefined();
537
+ auto s = args[0].asObject(rt2);
538
+ VroomRSI cfg{};
539
+ cfg.enabled = s.getProperty(rt2, "enabled").asBool() ? 1 : 0;
540
+ cfg.period = static_cast<int32_t>(
541
+ s.getProperty(rt2, "period").asNumber());
542
+ cfg.upper_band = s.getProperty(rt2, "upperBand").asNumber();
543
+ cfg.lower_band = s.getProperty(rt2, "lowerBand").asNumber();
544
+ cfg.ma_period = static_cast<int32_t>(
545
+ s.getProperty(rt2, "maPeriod").asNumber());
546
+ cfg.ma_kind = static_cast<int32_t>(
547
+ s.getProperty(rt2, "maKind").asNumber());
548
+ cfg.ma_visible = s.getProperty(rt2, "maVisible").asBool() ? 1 : 0;
549
+ cfg.line_color = static_cast<uint32_t>(
550
+ s.getProperty(rt2, "lineColor").asNumber());
551
+ cfg.line_width = static_cast<float>(
552
+ s.getProperty(rt2, "lineWidth").asNumber());
553
+ cfg.line_visible = s.getProperty(rt2, "lineVisible").asBool() ? 1 : 0;
554
+ cfg.ma_color = static_cast<uint32_t>(
555
+ s.getProperty(rt2, "maColor").asNumber());
556
+ cfg.ma_width = static_cast<float>(
557
+ s.getProperty(rt2, "maWidth").asNumber());
558
+ cfg.band_color = static_cast<uint32_t>(
559
+ s.getProperty(rt2, "bandColor").asNumber());
560
+ cfg.bands_visible =
561
+ s.getProperty(rt2, "bandsVisible").asBool() ? 1 : 0;
562
+ vroom_chart_set_rsi(chart_, &cfg);
472
563
  return jsi::Value::undefined();
473
564
  });
474
565
  }
475
566
 
476
567
  if (name == "setMACD") {
477
- // setMACD(enabled, fast, slow, signal) configures the MACD pane. No
478
- // render; the next render() picks it up.
568
+ // setMACD({enabled, fast, slow, signal, source, maKind, signalMaKind,
569
+ // lineColor, lineWidth, lineVisible, signalColor, signalWidth,
570
+ // signalVisible, histVisible, histUpColor, histUpFadingColor,
571
+ // histDownColor, histDownFadingColor, zeroColor, zeroVisible}) — configures
572
+ // the MACD pane. No render; the next render() picks it up.
479
573
  return jsi::Function::createFromHostFunction(
480
574
  rt,
481
575
  jsi::PropNameID::forAscii(rt, "setMACD"),
482
- 4,
483
- [this](jsi::Runtime& /*rt2*/,
576
+ 1,
577
+ [this](jsi::Runtime& rt2,
484
578
  const jsi::Value& /*thisVal*/,
485
579
  const jsi::Value* args,
486
580
  size_t count) -> jsi::Value {
487
- if (count < 4) return jsi::Value::undefined();
488
- const bool enabled = args[0].asBool();
489
- const int fast = static_cast<int>(args[1].asNumber());
490
- const int slow = static_cast<int>(args[2].asNumber());
491
- const int signal = static_cast<int>(args[3].asNumber());
492
- vroom_chart_set_macd(chart_, enabled, fast, slow, signal);
581
+ if (count < 1 || !args[0].isObject()) return jsi::Value::undefined();
582
+ auto s = args[0].asObject(rt2);
583
+ VroomMACD cfg{};
584
+ cfg.enabled = s.getProperty(rt2, "enabled").asBool() ? 1 : 0;
585
+ cfg.fast = static_cast<int32_t>(
586
+ s.getProperty(rt2, "fast").asNumber());
587
+ cfg.slow = static_cast<int32_t>(
588
+ s.getProperty(rt2, "slow").asNumber());
589
+ cfg.signal = static_cast<int32_t>(
590
+ s.getProperty(rt2, "signal").asNumber());
591
+ cfg.source = static_cast<int32_t>(
592
+ s.getProperty(rt2, "source").asNumber());
593
+ cfg.ma_kind = static_cast<int32_t>(
594
+ s.getProperty(rt2, "maKind").asNumber());
595
+ cfg.signal_ma_kind = static_cast<int32_t>(
596
+ s.getProperty(rt2, "signalMaKind").asNumber());
597
+ cfg.line_color = static_cast<uint32_t>(
598
+ s.getProperty(rt2, "lineColor").asNumber());
599
+ cfg.line_width = static_cast<float>(
600
+ s.getProperty(rt2, "lineWidth").asNumber());
601
+ cfg.line_visible =
602
+ s.getProperty(rt2, "lineVisible").asBool() ? 1 : 0;
603
+ cfg.signal_color = static_cast<uint32_t>(
604
+ s.getProperty(rt2, "signalColor").asNumber());
605
+ cfg.signal_width = static_cast<float>(
606
+ s.getProperty(rt2, "signalWidth").asNumber());
607
+ cfg.signal_visible =
608
+ s.getProperty(rt2, "signalVisible").asBool() ? 1 : 0;
609
+ cfg.hist_visible =
610
+ s.getProperty(rt2, "histVisible").asBool() ? 1 : 0;
611
+ cfg.hist_up_color = static_cast<uint32_t>(
612
+ s.getProperty(rt2, "histUpColor").asNumber());
613
+ cfg.hist_up_fading_color = static_cast<uint32_t>(
614
+ s.getProperty(rt2, "histUpFadingColor").asNumber());
615
+ cfg.hist_down_color = static_cast<uint32_t>(
616
+ s.getProperty(rt2, "histDownColor").asNumber());
617
+ cfg.hist_down_fading_color = static_cast<uint32_t>(
618
+ s.getProperty(rt2, "histDownFadingColor").asNumber());
619
+ cfg.zero_color = static_cast<uint32_t>(
620
+ s.getProperty(rt2, "zeroColor").asNumber());
621
+ cfg.zero_visible =
622
+ s.getProperty(rt2, "zeroVisible").asBool() ? 1 : 0;
623
+ vroom_chart_set_macd(chart_, &cfg);
493
624
  return jsi::Value::undefined();
494
625
  });
495
626
  }
@@ -533,21 +664,26 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
533
664
  }
534
665
 
535
666
  if (name == "setVWAP") {
536
- // setVWAP(enabled, resetOffsetMin, color, width) — session VWAP overlay.
667
+ // setVWAP({enabled, resetOffsetMin, color, width}) — session VWAP overlay.
537
668
  return jsi::Function::createFromHostFunction(
538
669
  rt,
539
670
  jsi::PropNameID::forAscii(rt, "setVWAP"),
540
- 4,
541
- [this](jsi::Runtime& /*rt2*/,
671
+ 1,
672
+ [this](jsi::Runtime& rt2,
542
673
  const jsi::Value& /*thisVal*/,
543
674
  const jsi::Value* args,
544
675
  size_t count) -> jsi::Value {
545
- if (count < 4) return jsi::Value::undefined();
546
- const bool enabled = args[0].asBool();
547
- const int reset = static_cast<int>(args[1].asNumber());
548
- const uint32_t color = static_cast<uint32_t>(args[2].asNumber());
549
- const float width = static_cast<float>(args[3].asNumber());
550
- vroom_chart_set_vwap(chart_, enabled, reset, color, width);
676
+ if (count < 1 || !args[0].isObject()) return jsi::Value::undefined();
677
+ auto s = args[0].asObject(rt2);
678
+ VroomVWAP cfg{};
679
+ cfg.enabled = s.getProperty(rt2, "enabled").asBool() ? 1 : 0;
680
+ cfg.reset_offset_min = static_cast<int32_t>(
681
+ s.getProperty(rt2, "resetOffsetMin").asNumber());
682
+ cfg.color = static_cast<uint32_t>(
683
+ s.getProperty(rt2, "color").asNumber());
684
+ cfg.width = static_cast<float>(
685
+ s.getProperty(rt2, "width").asNumber());
686
+ vroom_chart_set_vwap(chart_, &cfg);
551
687
  return jsi::Value::undefined();
552
688
  });
553
689
  }
@@ -598,6 +734,205 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
598
734
  });
599
735
  }
600
736
 
737
+ if (name == "setVolume") {
738
+ // setVolume({enabled, heightFrac, opacity, radiusPx, upColor, downColor})
739
+ // — the volume bars. Negative floats / zero colors inherit the theme. No
740
+ // render; the next render() picks it up.
741
+ return jsi::Function::createFromHostFunction(
742
+ rt,
743
+ jsi::PropNameID::forAscii(rt, "setVolume"),
744
+ 1,
745
+ [this](jsi::Runtime& rt2,
746
+ const jsi::Value& /*thisVal*/,
747
+ const jsi::Value* args,
748
+ size_t count) -> jsi::Value {
749
+ if (count < 1 || !args[0].isObject()) return jsi::Value::undefined();
750
+ auto s = args[0].asObject(rt2);
751
+ VroomVolume cfg{};
752
+ cfg.enabled = s.getProperty(rt2, "enabled").asBool() ? 1 : 0;
753
+ cfg.height_frac = static_cast<float>(
754
+ s.getProperty(rt2, "heightFrac").asNumber());
755
+ cfg.opacity = static_cast<float>(
756
+ s.getProperty(rt2, "opacity").asNumber());
757
+ cfg.radius_px = static_cast<float>(
758
+ s.getProperty(rt2, "radiusPx").asNumber());
759
+ cfg.up_color = static_cast<uint32_t>(
760
+ s.getProperty(rt2, "upColor").asNumber());
761
+ cfg.down_color = static_cast<uint32_t>(
762
+ s.getProperty(rt2, "downColor").asNumber());
763
+ vroom_chart_set_volume(chart_, &cfg);
764
+ return jsi::Value::undefined();
765
+ });
766
+ }
767
+
768
+ if (name == "setVolumeCollapse") {
769
+ // setVolumeCollapse(t, easing) — staggered collapse of the volume bars. `t`
770
+ // is LINEAR progress; the core eases each bar over its own window.
771
+ return jsi::Function::createFromHostFunction(
772
+ rt,
773
+ jsi::PropNameID::forAscii(rt, "setVolumeCollapse"),
774
+ 2,
775
+ [this](jsi::Runtime& /*rt2*/,
776
+ const jsi::Value& /*thisVal*/,
777
+ const jsi::Value* args,
778
+ size_t count) -> jsi::Value {
779
+ if (count < 2) return jsi::Value::undefined();
780
+ vroom_chart_set_volume_collapse(
781
+ chart_,
782
+ static_cast<float>(args[0].asNumber()),
783
+ static_cast<int32_t>(args[1].asNumber()));
784
+ return jsi::Value::undefined();
785
+ });
786
+ }
787
+
788
+ if (name == "coordAt") {
789
+ // coordAt(x, y) -> { timeMs, price } | null. The continuous data coordinate
790
+ // at a pixel — not snapped to a candle slot. Null when there are no candles
791
+ // or the viewport is degenerate. No rendering.
792
+ return jsi::Function::createFromHostFunction(
793
+ rt,
794
+ jsi::PropNameID::forAscii(rt, "coordAt"),
795
+ 2,
796
+ [this](jsi::Runtime& rt2,
797
+ const jsi::Value& /*thisVal*/,
798
+ const jsi::Value* args,
799
+ size_t count) -> jsi::Value {
800
+ if (count < 2) return jsi::Value::null();
801
+ VroomCoord c{};
802
+ if (!vroom_chart_coord_at(chart_,
803
+ static_cast<float>(args[0].asNumber()),
804
+ static_cast<float>(args[1].asNumber()), &c)) {
805
+ return jsi::Value::null();
806
+ }
807
+ jsi::Object obj(rt2);
808
+ obj.setProperty(rt2, "timeMs", static_cast<double>(c.time_ms));
809
+ obj.setProperty(rt2, "price", c.price);
810
+ return obj;
811
+ });
812
+ }
813
+
814
+ if (name == "setPriceLines") {
815
+ // setPriceLines({ lines: [{ price, color, width, lineStyle, text, quantity,
816
+ // flags }, ...], bodyBg, fontSizePx, lineLengthFrac, align, hoverBoost }) —
817
+ // replaces the full set of price status lines. No render; the next render()
818
+ // picks it up.
819
+ return jsi::Function::createFromHostFunction(
820
+ rt,
821
+ jsi::PropNameID::forAscii(rt, "setPriceLines"),
822
+ 1,
823
+ [this](jsi::Runtime& rt2,
824
+ const jsi::Value& /*thisVal*/,
825
+ const jsi::Value* args,
826
+ size_t count) -> jsi::Value {
827
+ if (count < 1 || !args[0].isObject()) return jsi::Value::undefined();
828
+ auto cfg = args[0].asObject(rt2);
829
+ auto lines_val = cfg.getProperty(rt2, "lines");
830
+ if (!lines_val.isObject()) return jsi::Value::undefined();
831
+ auto lines_obj = lines_val.asObject(rt2);
832
+ if (!lines_obj.isArray(rt2)) return jsi::Value::undefined();
833
+ auto arr = lines_obj.asArray(rt2);
834
+ const size_t len = arr.size(rt2);
835
+ std::vector<VroomPriceLine> lines(len);
836
+ // Label storage, kept alive until set_price_lines has copied it.
837
+ std::vector<std::string> texts(len);
838
+ std::vector<std::string> quantities(len);
839
+ for (size_t i = 0; i < len; ++i) {
840
+ auto l = arr.getValueAtIndex(rt2, i).asObject(rt2);
841
+ lines[i].price = l.getProperty(rt2, "price").asNumber();
842
+ lines[i].color = static_cast<uint32_t>(
843
+ l.getProperty(rt2, "color").asNumber());
844
+ lines[i].width = static_cast<float>(
845
+ l.getProperty(rt2, "width").asNumber());
846
+ lines[i].line_style = static_cast<int32_t>(
847
+ l.getProperty(rt2, "lineStyle").asNumber());
848
+ texts[i] = l.getProperty(rt2, "text").asString(rt2).utf8(rt2);
849
+ quantities[i] =
850
+ l.getProperty(rt2, "quantity").asString(rt2).utf8(rt2);
851
+ lines[i].text = texts[i].c_str();
852
+ lines[i].quantity = quantities[i].c_str();
853
+ lines[i].flags = static_cast<int32_t>(
854
+ l.getProperty(rt2, "flags").asNumber());
855
+ }
856
+ VroomPriceLineStyle style{};
857
+ style.body_bg = static_cast<uint32_t>(
858
+ cfg.getProperty(rt2, "bodyBg").asNumber());
859
+ style.font_size_px = static_cast<float>(
860
+ cfg.getProperty(rt2, "fontSizePx").asNumber());
861
+ style.line_length_frac = static_cast<float>(
862
+ cfg.getProperty(rt2, "lineLengthFrac").asNumber());
863
+ style.align = static_cast<int32_t>(
864
+ cfg.getProperty(rt2, "align").asNumber());
865
+ style.hover_boost = static_cast<float>(
866
+ cfg.getProperty(rt2, "hoverBoost").asNumber());
867
+ vroom_chart_set_price_lines(chart_, lines.data(), lines.size(), &style);
868
+ return jsi::Value::undefined();
869
+ });
870
+ }
871
+
872
+ if (name == "hitTestPriceLine") {
873
+ // hitTestPriceLine(x, y) -> { index, part } | null. `part` is 0 for the line
874
+ // or its label body (the drag target) and 1 for the close button. Cheap
875
+ // enough to call at gesture rate — no rendering.
876
+ return jsi::Function::createFromHostFunction(
877
+ rt,
878
+ jsi::PropNameID::forAscii(rt, "hitTestPriceLine"),
879
+ 2,
880
+ [this](jsi::Runtime& rt2,
881
+ const jsi::Value& /*thisVal*/,
882
+ const jsi::Value* args,
883
+ size_t count) -> jsi::Value {
884
+ if (count < 2) return jsi::Value::null();
885
+ int32_t index = -1, part = -1;
886
+ if (!vroom_chart_hit_test_price_line(
887
+ chart_, static_cast<float>(args[0].asNumber()),
888
+ static_cast<float>(args[1].asNumber()), &index, &part)) {
889
+ return jsi::Value::null();
890
+ }
891
+ jsi::Object obj(rt2);
892
+ obj.setProperty(rt2, "index", index);
893
+ obj.setProperty(rt2, "part", part);
894
+ return obj;
895
+ });
896
+ }
897
+
898
+ if (name == "setPriceLineHover") {
899
+ // setPriceLineHover(index, part) — highlight a price line's segment; -1
900
+ // clears. No hover on touch, so this exists for parity/pointer devices.
901
+ return jsi::Function::createFromHostFunction(
902
+ rt,
903
+ jsi::PropNameID::forAscii(rt, "setPriceLineHover"),
904
+ 2,
905
+ [this](jsi::Runtime& /*rt2*/,
906
+ const jsi::Value& /*thisVal*/,
907
+ const jsi::Value* args,
908
+ size_t count) -> jsi::Value {
909
+ if (count < 2) return jsi::Value::undefined();
910
+ vroom_chart_set_price_line_hover(
911
+ chart_, static_cast<int32_t>(args[0].asNumber()),
912
+ static_cast<int32_t>(args[1].asNumber()));
913
+ return jsi::Value::undefined();
914
+ });
915
+ }
916
+
917
+ if (name == "setPriceLineDrag") {
918
+ // setPriceLineDrag(index, price) — live drag preview; index -1 ends it. The
919
+ // committed price is untouched: restate setPriceLines to apply a move.
920
+ return jsi::Function::createFromHostFunction(
921
+ rt,
922
+ jsi::PropNameID::forAscii(rt, "setPriceLineDrag"),
923
+ 2,
924
+ [this](jsi::Runtime& /*rt2*/,
925
+ const jsi::Value& /*thisVal*/,
926
+ const jsi::Value* args,
927
+ size_t count) -> jsi::Value {
928
+ if (count < 2) return jsi::Value::undefined();
929
+ vroom_chart_set_price_line_drag(
930
+ chart_, static_cast<int32_t>(args[0].asNumber()),
931
+ args[1].asNumber());
932
+ return jsi::Value::undefined();
933
+ });
934
+ }
935
+
601
936
  if (name == "render") {
602
937
  return jsi::Function::createFromHostFunction(
603
938
  rt,
@@ -40,7 +40,17 @@ static void ensureAxisTypeface(jsi::Runtime& runtime) {
40
40
  if (!ctx) return;
41
41
  auto mgr = ctx->createFontMgr();
42
42
  if (!mgr) return;
43
- auto tf = mgr->matchFamilyStyle(nullptr, SkFontStyle()); // system default
43
+
44
+ // A null family name is meant to request "the platform default" — CoreText
45
+ // (iOS) honors that, but Android's SkFontMgr_New_Android does not: it
46
+ // returns null unless given an actual family name, even though its
47
+ // underlying font set (sans-serif, arial, ...) is perfectly populated. Fall
48
+ // back to "sans-serif" (Android's standard generic-family alias) whenever
49
+ // the null-family lookup comes back empty.
50
+ auto tf = mgr->matchFamilyStyle(nullptr, SkFontStyle());
51
+ if (!tf) {
52
+ tf = mgr->matchFamilyStyle("sans-serif", SkFontStyle());
53
+ }
44
54
  if (!tf) return;
45
55
  vroom::set_axis_typeface(tf);
46
56
  done = true;
@@ -12,13 +12,21 @@ std::shared_ptr<RNSkia::RNSkPlatformContext> getRNSkContext(
12
12
  if (!obj.isHostObject(runtime)) return nullptr;
13
13
  auto host = obj.asHostObject(runtime);
14
14
 
15
- auto base = std::dynamic_pointer_cast<RNSkia::JsiSkHostObject>(host);
16
- if (!base) return nullptr;
17
-
18
- // The accessor class adds no fields; static_cast is safe even though the
19
- // runtime type is JsiSkApi (a different JsiSkHostObject subclass). We only
20
- // call the inherited non-virtual getContext() through this view.
21
- auto* accessor = static_cast<HostObjectAccessor*>(base.get());
15
+ // `global.SkiaApi` is always installed by RN-Skia's own
16
+ // RNSkManager::installBindings() as exactly a `JsiSkApi` host object — never
17
+ // any other JsiSkHostObject subclass — so this cast is safe as a plain
18
+ // static_cast, without any RTTI check. We deliberately avoid
19
+ // dynamic_pointer_cast here: on Android, RN-Skia is compiled into its own
20
+ // librnskia.so, separate from libvroomchart.so, so a `JsiSkApi` constructed
21
+ // by librnskia.so's code carries typeinfo from that .so; a dynamic_cast
22
+ // performed here (in libvroomchart.so) sees an unmerged RTTI record for
23
+ // "the same" class and always fails, even though the object is perfectly
24
+ // valid to use. (Same underlying issue as the JsiSkPicture cross-.so cast
25
+ // documented in VroomChartHostObject.cpp — but there RN-Skia's own compiled
26
+ // code does the cast on an object *we* construct, so we can't avoid it;
27
+ // here *we* do the cast on an object *they* construct, so we can just skip
28
+ // the runtime check we don't need.)
29
+ auto* accessor = static_cast<HostObjectAccessor*>(host.get());
22
30
  return accessor->getContext();
23
31
  }
24
32