react-native-livechart 4.9.0 → 4.9.2

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.
@@ -1,4 +1,10 @@
1
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
1
+ import {
2
+ useEffect,
3
+ useLayoutEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from "react";
2
8
  import { StyleSheet, View } from "react-native";
3
9
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
10
  import Animated, {
@@ -118,7 +124,6 @@ import type {
118
124
  LiveChartPoint,
119
125
  LiveChartProps,
120
126
  Marker,
121
- TradeEvent,
122
127
  } from "../types";
123
128
  import {
124
129
  ThresholdBadgeOverlay,
@@ -159,7 +164,6 @@ import { ValueLineOverlay } from "./ValueLineOverlay";
159
164
  import { XAxisOverlay } from "./XAxisOverlay";
160
165
  import { YAxisOverlay } from "./YAxisOverlay";
161
166
 
162
-
163
167
  /** Stable empty grouping result (identity-stable so downstream worklets don't
164
168
  * re-run) used when reference-line grouping is off. */
165
169
  const EMPTY_GROUPING: ReferenceGrouping = { hidden: [], groups: [] };
@@ -324,8 +328,6 @@ function useLiveChartController({
324
328
  onReachStart,
325
329
  onDegenShake,
326
330
  }: LiveChartProps) {
327
- const emptyTradeStream = useSharedValue<TradeEvent[]>([]);
328
- const tradeStreamSV = tradeStream ?? emptyTradeStream;
329
331
  const emptyMarkers = useSharedValue<Marker[]>([]);
330
332
  const markersSV = markers ?? emptyMarkers;
331
333
  // Stand-in threshold value so `useThreshold` can be called unconditionally
@@ -408,7 +410,10 @@ function useLiveChartController({
408
410
 
409
411
  // Form-A lines a custom `renderReferenceLine` owns → suppress their built-in tag
410
412
  // (no double-draw). Probed on the JS thread, index-aligned with `allRefLines`.
411
- const refLineCustom = customReferenceLineFlags(allRefLines, renderReferenceLine);
413
+ const refLineCustom = customReferenceLineFlags(
414
+ allRefLines,
415
+ renderReferenceLine,
416
+ );
412
417
 
413
418
  // Live Y values of the *draggable* Form-A lines, folded into the engine's
414
419
  // axis-range fit so dragging a line toward / past the visible edge expands the
@@ -726,7 +731,14 @@ function useLiveChartController({
726
731
  continue;
727
732
  }
728
733
  const v = dragValues.get()[i] ?? l.value;
729
- const y = computeScrubDotY(v, dMin, dMax, ch, top, effectivePadding.bottom);
734
+ const y = computeScrubDotY(
735
+ v,
736
+ dMin,
737
+ dMax,
738
+ ch,
739
+ top,
740
+ effectivePadding.bottom,
741
+ );
730
742
  ys.push(y < 0 ? -1 : Math.min(bottom, Math.max(top, y)));
731
743
  }
732
744
  return groupReferenceLines(ys, refGroupingRadius);
@@ -890,27 +902,6 @@ function useLiveChartController({
890
902
  adA * (areaDotsCfg?.opacity ?? 1),
891
903
  ];
892
904
 
893
- const {
894
- upBodiesPath,
895
- downBodiesPath,
896
- upWicksPath,
897
- downWicksPath,
898
- upBarsPath,
899
- downBarsPath,
900
- } = useCandlePaths(
901
- engine,
902
- effectivePadding,
903
- // Match engine: stashed candles while reverse-morphing in candle mode.
904
- isCandle ? candlesEngine : candles,
905
- isCandle ? liveEngine : liveCandle,
906
- candleWidth,
907
- isCandle,
908
- metricsCfg.candle,
909
- volumeBandHeight,
910
- volumeCfg?.radius ?? 0,
911
- !isStatic, // static: no candle-width lerp loop
912
- transitionsCfg.candleLerpSpeed, // `transitions.candleLerpSpeed` (1 = instant)
913
- );
914
905
  const { dotX, dotY } = useLiveDot(
915
906
  engine,
916
907
  effectivePadding,
@@ -918,64 +909,9 @@ function useLiveChartController({
918
909
  badgeCfg?.followViewEdge ?? false,
919
910
  );
920
911
 
921
- // Price↔pixel / time↔pixel bridge for a custom `renderOverlay`. Built
922
- // unconditionally (hooks rule); only mounted when `renderOverlay` is provided.
923
- const overlayContext = useChartOverlayContext(engine, effectivePadding);
924
-
925
912
  const momentumSV = useMomentum(engine, momentum);
926
913
 
927
- const tradeMarkers = useTradeStream(
928
- engine,
929
- tradeStreamSV,
930
- effectivePadding,
931
- !isStatic && tradeStreamResolved !== null,
932
- !isStatic, // static: no trade-tape loop
933
- );
934
-
935
- const {
936
- pack: degenPack,
937
- packRevision: degenPackRevision,
938
- shakeTransform: degenShakeTransform,
939
- } = useDegen(engine, dotX, dotY, momentumSV, degenCfg, onDegenShake, isStatic);
940
-
941
914
  // ── Overlay hooks ─────────────────────────────────────────────────────
942
- const { yAxisEntries } = useYAxis(
943
- engine,
944
- effectivePadding,
945
- formatValue,
946
- skiaFont,
947
- yAxisCfg?.minGap ?? 36,
948
- metricsCfg.grid,
949
- yAxisCfg?.count ?? 0,
950
- );
951
-
952
- const { xAxisEntries } = useXAxis(
953
- engine,
954
- effectivePadding,
955
- formatTime,
956
- skiaFont,
957
- );
958
-
959
- const badgeData = useBadge(
960
- engine,
961
- effectivePadding,
962
- palette,
963
- formatValue,
964
- badgeFont,
965
- badgeCfg?.variant ?? "default",
966
- badgeCfg?.tail ?? true,
967
- momentumSV,
968
- badgeCfg?.position ?? "right",
969
- badgeCfg?.background,
970
- metricsCfg.badge,
971
- metricsCfg.motion.badgeColorSpeed,
972
- effectiveYAxisFloat,
973
- engine.edgeValue,
974
- badgeCfg?.followViewEdge ?? false,
975
- badgeCfg?.radius,
976
- badgeCfg?.textColor,
977
- );
978
-
979
915
  // Scrub/crosshair must see the same stash-backed candles as the engine.
980
916
  const candleOpts = isCandle
981
917
  ? {
@@ -1239,8 +1175,7 @@ function useLiveChartController({
1239
1175
  // Hide the live dot while scrubbing when a selection dot is marking the scrub
1240
1176
  // point instead — otherwise both dots show at once. Applies on static charts
1241
1177
  // too, now that they're scrubbable.
1242
- const selectionDotDuringScrub =
1243
- scrubCfg !== null && selectionDotCfg !== null;
1178
+ const selectionDotDuringScrub = scrubCfg !== null && selectionDotCfg !== null;
1244
1179
  const liveDotOpacity = useDerivedValue(
1245
1180
  () =>
1246
1181
  reveal.dotOpacity.value *
@@ -1276,7 +1211,9 @@ function useLiveChartController({
1276
1211
  valueMomentumColor,
1277
1212
  lineProp,
1278
1213
  formatValue,
1214
+ formatTime,
1279
1215
  isCandle,
1216
+ isStatic,
1280
1217
  // Half a candle width (seconds) so an "extrema" axis label's dot lands on the
1281
1218
  // candle's drawn center, not its bucket-start (left) edge. 0 in line mode.
1282
1219
  extremaTimeOffset: isCandle ? candleWidth / 2 : 0,
@@ -1297,6 +1234,7 @@ function useLiveChartController({
1297
1234
  gridStyleCfg,
1298
1235
  degenCfg,
1299
1236
  tradeStreamResolved,
1237
+ tradeStream,
1300
1238
  leftEdgeFadeCfg,
1301
1239
  metricsCfg,
1302
1240
  allRefLines,
@@ -1355,18 +1293,17 @@ function useLiveChartController({
1355
1293
  gradientPositions,
1356
1294
  lineGroupOpacity,
1357
1295
  candleGroupOpacity,
1296
+ candlesEngine,
1297
+ liveEngine,
1298
+ candleWidth,
1299
+ transitionsCfg,
1358
1300
  layoutWidth,
1359
1301
  onLayout,
1360
1302
  linePath,
1361
1303
  fillPath,
1362
1304
  thresholdFillPath,
1363
1305
  lineIsLinear,
1364
- upBodiesPath,
1365
- downBodiesPath,
1366
- upWicksPath,
1367
- downWicksPath,
1368
- upBarsPath,
1369
- downBarsPath,
1306
+ volumeCfg,
1370
1307
  // Volume bars: active flag, fade-in opacity, and resolved colors (default to
1371
1308
  // the candle palette). The reserved band height is read by the x-axis.
1372
1309
  volumeActive: volumeCfg !== null,
@@ -1380,13 +1317,7 @@ function useLiveChartController({
1380
1317
  overlayScrubFade,
1381
1318
  markerGroupOpacity,
1382
1319
  momentumSV,
1383
- tradeMarkers,
1384
- degenPack,
1385
- degenPackRevision,
1386
- degenShakeTransform,
1387
- yAxisEntries,
1388
- xAxisEntries,
1389
- badgeData,
1320
+ onDegenShake,
1390
1321
  crosshair,
1391
1322
  rootGesture,
1392
1323
  markersActive,
@@ -1395,7 +1326,6 @@ function useLiveChartController({
1395
1326
  renderMarker,
1396
1327
  renderTooltip,
1397
1328
  renderOverlay,
1398
- overlayContext,
1399
1329
  // selection dot: resolved config + fallback color (the chart line/accent color)
1400
1330
  selectionDot: selectionDotCfg,
1401
1331
  selectionColor: lineProp?.color ?? palette.line,
@@ -1410,27 +1340,151 @@ function useLiveChartController({
1410
1340
 
1411
1341
  type LiveChartModel = ReturnType<typeof useLiveChartController>;
1412
1342
 
1343
+ type YAxisEntries = ReturnType<typeof useYAxis>["yAxisEntries"];
1344
+ type DegenState = ReturnType<typeof useDegen>;
1345
+
1346
+ /** Owns the particle/shake state only while the degen effect is enabled. */
1347
+ function ChartWithDegen({
1348
+ model,
1349
+ yAxisEntries,
1350
+ }: {
1351
+ model: LiveChartModel;
1352
+ yAxisEntries: YAxisEntries | null;
1353
+ }) {
1354
+ const { engine, dotX, dotY, momentumSV, degenCfg, onDegenShake, isStatic } =
1355
+ model;
1356
+ const state = useDegen(
1357
+ engine,
1358
+ dotX,
1359
+ dotY,
1360
+ momentumSV,
1361
+ degenCfg,
1362
+ onDegenShake,
1363
+ isStatic,
1364
+ );
1365
+ return (
1366
+ <ChartView model={model} yAxisEntries={yAxisEntries} degen={state} />
1367
+ );
1368
+ }
1369
+
1413
1370
  /**
1414
- * Background fills drawn BENEATH the left-edge fade: the y-axis grid, the area
1415
- * gradient, and the threshold profit/loss band. Split out from `ChartStack` so
1416
- * the fade's `dstOut` only softens the fills the line and everything above it
1417
- * (drawn in `ChartStack`, after the fade) stay crisp at the left edge.
1371
+ * Owns the Y-axis worklets. The provider itself is mounted only while the axis
1372
+ * is enabled, so `yAxis={false}` never registers its shared values or mapper.
1373
+ * The entries are passed through ordinary props so both paint-order positions
1374
+ * receive the same mapper across the React Native Skia renderer boundary.
1418
1375
  */
1419
- function ChartFillLayer({ model }: { model: LiveChartModel }) {
1376
+ function ChartWithYAxis({ model }: { model: LiveChartModel }) {
1420
1377
  const {
1421
- degenShakeTransform,
1378
+ engine,
1379
+ effectivePadding,
1380
+ formatValue,
1381
+ skiaFont,
1422
1382
  yAxisCfg,
1423
- yAxisFloat,
1383
+ metricsCfg,
1384
+ } = model;
1385
+ const { yAxisEntries } = useYAxis(
1386
+ engine,
1387
+ effectivePadding,
1388
+ formatValue,
1389
+ skiaFont,
1390
+ yAxisCfg?.minGap ?? 36,
1391
+ metricsCfg.grid,
1392
+ yAxisCfg?.count ?? 0,
1393
+ );
1394
+ if (model.degenCfg) {
1395
+ return <ChartWithDegen model={model} yAxisEntries={yAxisEntries} />;
1396
+ }
1397
+ return <ChartView model={model} yAxisEntries={yAxisEntries} degen={null} />;
1398
+ }
1399
+
1400
+ function ChartYAxisLayer({
1401
+ model,
1402
+ variant,
1403
+ entries,
1404
+ }: {
1405
+ model: LiveChartModel;
1406
+ variant: "all" | "grid" | "labels";
1407
+ entries: YAxisEntries;
1408
+ }) {
1409
+ const {
1424
1410
  reveal,
1425
- yAxisEntries,
1426
1411
  engine,
1427
1412
  effectivePadding,
1428
1413
  palette,
1429
1414
  skiaFont,
1430
1415
  badgeUsesRightGutter,
1431
1416
  badgeCfg,
1432
- gridStyleCfg,
1433
1417
  metricsCfg,
1418
+ gridStyleCfg,
1419
+ yAxisFloat,
1420
+ } = model;
1421
+ return (
1422
+ <Group opacity={reveal.yAxisOpacity}>
1423
+ <YAxisOverlay
1424
+ variant={variant}
1425
+ float={variant === "labels" && yAxisFloat}
1426
+ entries={entries}
1427
+ engine={engine}
1428
+ padding={effectivePadding}
1429
+ palette={palette}
1430
+ font={skiaFont}
1431
+ badge={badgeUsesRightGutter}
1432
+ badgeTail={badgeCfg?.tail ?? true}
1433
+ badgeMetrics={metricsCfg.badge}
1434
+ gridStyle={gridStyleCfg}
1435
+ />
1436
+ </Group>
1437
+ );
1438
+ }
1439
+
1440
+ /** Owns the X-axis worklet and only mounts when `xAxis` is enabled. */
1441
+ function ChartXAxisLayer({ model }: { model: LiveChartModel }) {
1442
+ const {
1443
+ engine,
1444
+ effectivePadding,
1445
+ formatTime,
1446
+ skiaFont,
1447
+ palette,
1448
+ volumeBandHeight,
1449
+ } = model;
1450
+ const { xAxisEntries } = useXAxis(
1451
+ engine,
1452
+ effectivePadding,
1453
+ formatTime,
1454
+ skiaFont,
1455
+ );
1456
+ return (
1457
+ <XAxisOverlay
1458
+ entries={xAxisEntries}
1459
+ engine={engine}
1460
+ padding={effectivePadding}
1461
+ palette={palette}
1462
+ font={skiaFont}
1463
+ volumeBandHeight={volumeBandHeight}
1464
+ />
1465
+ );
1466
+ }
1467
+
1468
+ /**
1469
+ * Background fills drawn BENEATH the left-edge fade: the y-axis grid, the area
1470
+ * gradient, and the threshold profit/loss band. Split out from `ChartStack` so
1471
+ * the fade's `dstOut` only softens the fills — the line and everything above it
1472
+ * (drawn in `ChartStack`, after the fade) stay crisp at the left edge.
1473
+ */
1474
+ function ChartFillLayer({
1475
+ model,
1476
+ yAxisEntries,
1477
+ degen,
1478
+ }: {
1479
+ model: LiveChartModel;
1480
+ yAxisEntries: YAxisEntries | null;
1481
+ degen: DegenState | null;
1482
+ }) {
1483
+ const {
1484
+ yAxisCfg,
1485
+ yAxisFloat,
1486
+ reveal,
1487
+ effectivePadding,
1434
1488
  gradientCfg,
1435
1489
  areaDotsCfg,
1436
1490
  areaDotColorVec,
@@ -1446,27 +1500,17 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
1446
1500
  thresholdSeriesHasPoints,
1447
1501
  thresholdFillUniforms,
1448
1502
  } = model;
1449
-
1450
1503
  return (
1451
- <Group transform={degenShakeTransform}>
1504
+ <Group transform={degen?.shakeTransform}>
1452
1505
  {/* Y-axis. Default: grid + labels here (in a reserved gutter). Floating
1453
1506
  mode: grid only — the labels + a soft edge fade draw above the candles
1454
1507
  in ChartStack so the plot runs full-width and candles dim under them. */}
1455
1508
  {yAxisCfg && (
1456
- <Group opacity={reveal.yAxisOpacity}>
1457
- <YAxisOverlay
1458
- variant={yAxisFloat ? "grid" : "all"}
1459
- entries={yAxisEntries}
1460
- engine={engine}
1461
- padding={effectivePadding}
1462
- palette={palette}
1463
- font={skiaFont}
1464
- badge={badgeUsesRightGutter}
1465
- badgeTail={badgeCfg?.tail ?? true}
1466
- badgeMetrics={metricsCfg.badge}
1467
- gridStyle={gridStyleCfg}
1468
- />
1469
- </Group>
1509
+ <ChartYAxisLayer
1510
+ model={model}
1511
+ variant={yAxisFloat ? "grid" : "all"}
1512
+ entries={yAxisEntries!}
1513
+ />
1470
1514
  )}
1471
1515
 
1472
1516
  {/* Dot-lattice area fill (the under-line `fillPath` painted with a dot
@@ -1529,13 +1573,95 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
1529
1573
  );
1530
1574
  }
1531
1575
 
1576
+ /**
1577
+ * Candle/volume paths are a mode-specific subsystem. Keeping their hooks in a
1578
+ * child means a line chart never registers the candle-width frame callback or
1579
+ * the six derived path worklets.
1580
+ */
1581
+ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1582
+ const {
1583
+ engine,
1584
+ effectivePadding,
1585
+ candlesEngine,
1586
+ liveEngine,
1587
+ candleWidth,
1588
+ metricsCfg,
1589
+ volumeBandHeight,
1590
+ volumeCfg,
1591
+ isStatic,
1592
+ transitionsCfg,
1593
+ candleGroupOpacity,
1594
+ palette,
1595
+ volumeOpacity,
1596
+ volumeUpColor,
1597
+ volumeDownColor,
1598
+ } = model;
1599
+ const {
1600
+ upBodiesPath,
1601
+ downBodiesPath,
1602
+ upWicksPath,
1603
+ downWicksPath,
1604
+ upBarsPath,
1605
+ downBarsPath,
1606
+ } = useCandlePaths(
1607
+ engine,
1608
+ effectivePadding,
1609
+ candlesEngine,
1610
+ liveEngine,
1611
+ candleWidth,
1612
+ true,
1613
+ metricsCfg.candle,
1614
+ volumeBandHeight,
1615
+ volumeCfg?.radius ?? 0,
1616
+ !isStatic,
1617
+ transitionsCfg.candleLerpSpeed,
1618
+ );
1619
+
1620
+ return (
1621
+ <>
1622
+ <Group opacity={candleGroupOpacity}>
1623
+ <Path
1624
+ path={upWicksPath}
1625
+ style="stroke"
1626
+ strokeWidth={metricsCfg.candle.wickWidth}
1627
+ color={palette.wickUp}
1628
+ />
1629
+ <Path
1630
+ path={downWicksPath}
1631
+ style="stroke"
1632
+ strokeWidth={metricsCfg.candle.wickWidth}
1633
+ color={palette.wickDown}
1634
+ />
1635
+ <Path path={upBodiesPath} style="fill" color={palette.candleUp} />
1636
+ <Path path={downBodiesPath} style="fill" color={palette.candleDown} />
1637
+ </Group>
1638
+
1639
+ {volumeCfg && (
1640
+ <Group opacity={candleGroupOpacity}>
1641
+ <Group opacity={volumeOpacity}>
1642
+ <Path path={upBarsPath} style="fill" color={volumeUpColor} />
1643
+ <Path path={downBarsPath} style="fill" color={volumeDownColor} />
1644
+ </Group>
1645
+ </Group>
1646
+ )}
1647
+ </>
1648
+ );
1649
+ }
1650
+
1532
1651
  /** Main shaken chart stack drawn ABOVE the left-edge fade so the line stays crisp:
1533
1652
  * segment dividers, value/reference lines, the line/candles, axes, dot, degen,
1534
1653
  * markers, and the loading/empty art. Background fills are in `ChartFillLayer`
1535
1654
  * (below the fade); the live value text is `ChartValueOverlay` (above the fade). */
1536
- function ChartStack({ model }: { model: LiveChartModel }) {
1655
+ function ChartStack({
1656
+ model,
1657
+ yAxisEntries,
1658
+ degen,
1659
+ }: {
1660
+ model: LiveChartModel;
1661
+ yAxisEntries: YAxisEntries | null;
1662
+ degen: DegenState | null;
1663
+ }) {
1537
1664
  const {
1538
- degenShakeTransform,
1539
1665
  reveal,
1540
1666
  engine,
1541
1667
  effectivePadding,
@@ -1567,27 +1693,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1567
1693
  lineIsLinear,
1568
1694
  strokeWidth,
1569
1695
  lineProp,
1570
- candleGroupOpacity,
1571
- upWicksPath,
1572
- downWicksPath,
1573
- upBodiesPath,
1574
- downBodiesPath,
1575
- upBarsPath,
1576
- downBarsPath,
1577
- volumeActive,
1578
- volumeBandHeight,
1579
- volumeOpacity,
1580
- volumeUpColor,
1581
- volumeDownColor,
1696
+ isCandle,
1582
1697
  xAxisCfg,
1583
- xAxisEntries,
1584
1698
  dotX,
1585
1699
  liveDotOpacity,
1586
1700
  pulseCfg,
1587
1701
  dotCfg,
1588
1702
  degenCfg,
1589
- degenPack,
1590
- degenPackRevision,
1591
1703
  markersActive,
1592
1704
  markersSV,
1593
1705
  markerClusterCfg,
@@ -1600,17 +1712,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1600
1712
  layoutWidth,
1601
1713
  yAxisCfg,
1602
1714
  yAxisFloat,
1603
- yAxisEntries,
1604
- badgeUsesRightGutter,
1605
- gridStyleCfg,
1606
1715
  loadingLineColor,
1607
1716
  loadingStrokeWidth,
1608
1717
  loadingAmplitude,
1609
1718
  loadingSpeed,
1610
1719
  } = model;
1611
-
1612
1720
  return (
1613
- <Group transform={degenShakeTransform}>
1721
+ <Group transform={degen?.shakeTransform}>
1614
1722
  {/* Segment dividers + labels (behind the line). The scrub-focus emphasis is
1615
1723
  painted on the line stroke itself, below — this overlay draws no fill. */}
1616
1724
  {resolvedSegments.map((seg, i) => (
@@ -1722,73 +1830,22 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1722
1830
  </Path>
1723
1831
  </Group>
1724
1832
 
1725
- {/* Candle bodies/wicks (fades in in candle mode) */}
1726
- <Group opacity={candleGroupOpacity}>
1727
- <Path
1728
- path={upWicksPath}
1729
- style="stroke"
1730
- strokeWidth={metricsCfg.candle.wickWidth}
1731
- color={palette.wickUp}
1732
- />
1733
- <Path
1734
- path={downWicksPath}
1735
- style="stroke"
1736
- strokeWidth={metricsCfg.candle.wickWidth}
1737
- color={palette.wickDown}
1738
- />
1739
- <Path path={upBodiesPath} style="fill" color={palette.candleUp} />
1740
- <Path
1741
- path={downBodiesPath}
1742
- style="fill"
1743
- color={palette.candleDown}
1744
- />
1745
- </Group>
1746
-
1747
- {/* Volume bars in the reserved band below the candles. Fades in with the
1748
- candle group (outer opacity); the config opacity dims the whole band
1749
- (inner). Up/down bars carry their own colors (default candle palette). */}
1750
- {volumeActive && (
1751
- <Group opacity={candleGroupOpacity}>
1752
- <Group opacity={volumeOpacity}>
1753
- <Path path={upBarsPath} style="fill" color={volumeUpColor} />
1754
- <Path path={downBarsPath} style="fill" color={volumeDownColor} />
1755
- </Group>
1756
- </Group>
1757
- )}
1833
+ {isCandle && <ChartCandleLayer model={model} />}
1758
1834
 
1759
1835
  {/* Floating axis: the labels float ABOVE the candles (right-aligned at the
1760
1836
  edge) so the plot runs full-width and candles stay fully visible behind
1761
1837
  them. (Default non-floating axis draws grid + labels in ChartFillLayer.) */}
1762
1838
  {yAxisCfg && yAxisFloat && (
1763
- <Group opacity={reveal.yAxisOpacity}>
1764
- <YAxisOverlay
1765
- variant="labels"
1766
- float
1767
- entries={yAxisEntries}
1768
- engine={engine}
1769
- padding={effectivePadding}
1770
- palette={palette}
1771
- font={skiaFont}
1772
- badge={badgeUsesRightGutter}
1773
- badgeTail={badgeCfg?.tail ?? true}
1774
- badgeMetrics={metricsCfg.badge}
1775
- gridStyle={gridStyleCfg}
1776
- />
1777
- </Group>
1839
+ <ChartYAxisLayer
1840
+ model={model}
1841
+ variant="labels"
1842
+ entries={yAxisEntries!}
1843
+ />
1778
1844
  )}
1779
1845
 
1780
1846
  {/* X-axis time labels. With a volume band the bottom padding is inflated by
1781
1847
  the band height; pass it so the axis shifts back to the very bottom. */}
1782
- {xAxisCfg && (
1783
- <XAxisOverlay
1784
- entries={xAxisEntries}
1785
- engine={engine}
1786
- padding={effectivePadding}
1787
- palette={palette}
1788
- font={skiaFont}
1789
- volumeBandHeight={volumeBandHeight}
1790
- />
1791
- )}
1848
+ {xAxisCfg && <ChartXAxisLayer model={model} />}
1792
1849
 
1793
1850
  {/* Live dot — the badge is drawn later (after the scrub layer) so the
1794
1851
  scrub dim never clips the live-price badge's left edge. Hidden while
@@ -1812,8 +1869,8 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1812
1869
  {degenCfg && (
1813
1870
  <Group opacity={reveal.dotOpacity}>
1814
1871
  <DegenParticlesOverlay
1815
- pack={degenPack}
1816
- packRevision={degenPackRevision}
1872
+ pack={degen!.pack}
1873
+ packRevision={degen!.packRevision}
1817
1874
  engine={engine}
1818
1875
  palette={palette}
1819
1876
  particleSlotCount={degenCfg.particleSlotCount}
@@ -1882,18 +1939,58 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1882
1939
  );
1883
1940
  }
1884
1941
 
1885
- /** Trade-tape labels and the scrub crosshair/tooltip (drawn in canvas space, on
1886
- * top of the shaken stack). */
1887
- function ChartScrubLayer({ model }: { model: LiveChartModel }) {
1942
+ /** Owns the trade-tape frame callback and only mounts with a trade stream. */
1943
+ function ChartTradeStreamLayer({
1944
+ model,
1945
+ degen,
1946
+ }: {
1947
+ model: LiveChartModel;
1948
+ degen: DegenState | null;
1949
+ }) {
1888
1950
  const {
1951
+ engine,
1952
+ tradeStream,
1889
1953
  tradeStreamResolved,
1954
+ effectivePadding,
1955
+ palette,
1956
+ skiaFont,
1957
+ reveal,
1958
+ isStatic,
1959
+ } = model;
1960
+ const tradeMarkers = useTradeStream(
1961
+ engine,
1962
+ tradeStream!,
1963
+ effectivePadding,
1964
+ !isStatic,
1965
+ !isStatic,
1966
+ );
1967
+ return (
1968
+ <Group transform={degen?.shakeTransform}>
1969
+ <TradeStreamOverlay
1970
+ markers={tradeMarkers}
1971
+ palette={palette}
1972
+ padding={effectivePadding}
1973
+ font={skiaFont}
1974
+ opacity={reveal.dotOpacity}
1975
+ labelOffsetX={tradeStreamResolved!.labelOffsetX}
1976
+ />
1977
+ </Group>
1978
+ );
1979
+ }
1980
+
1981
+ /** Scrub crosshair/tooltip drawn in canvas space on top of the shaken stack. */
1982
+ function ChartScrubLayer({
1983
+ model,
1984
+ degen,
1985
+ }: {
1986
+ model: LiveChartModel;
1987
+ degen: DegenState | null;
1988
+ }) {
1989
+ const {
1890
1990
  scrubCfg,
1891
- degenShakeTransform,
1892
- tradeMarkers,
1893
1991
  palette,
1894
1992
  effectivePadding,
1895
1993
  skiaFont,
1896
- reveal,
1897
1994
  crosshair,
1898
1995
  isCandle,
1899
1996
  pulseCfg,
@@ -1902,13 +1999,12 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
1902
1999
  selectionColor,
1903
2000
  renderTooltip,
1904
2001
  } = model;
1905
-
1906
2002
  // A custom tooltip is an RN overlay (sibling of <Canvas>), so the built-in
1907
2003
  // Skia tooltip is suppressed here while it's active — the line pill in line
1908
2004
  // mode, and the OHLC stack in candle mode (see the stack gate below).
1909
2005
  const customTooltipActive = renderTooltip != null;
1910
2006
 
1911
- if (!tradeStreamResolved && !scrubCfg) return null;
2007
+ if (!scrubCfg) return null;
1912
2008
 
1913
2009
  // Extend the scrub dim past the plot's right edge to fully cover the live dot
1914
2010
  // (with its halo) and pulse ring, all centered on that edge. The gutter
@@ -1919,18 +2015,7 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
1919
2015
  );
1920
2016
 
1921
2017
  return (
1922
- <Group transform={degenShakeTransform}>
1923
- {tradeStreamResolved && (
1924
- <TradeStreamOverlay
1925
- markers={tradeMarkers}
1926
- palette={palette}
1927
- padding={effectivePadding}
1928
- font={skiaFont}
1929
- opacity={reveal.dotOpacity}
1930
- labelOffsetX={tradeStreamResolved.labelOffsetX}
1931
- />
1932
- )}
1933
-
2018
+ <Group transform={degen?.shakeTransform}>
1934
2019
  {scrubCfg && (
1935
2020
  <CrosshairOverlay
1936
2021
  scrubX={crosshair.scrubX}
@@ -1978,10 +2063,15 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
1978
2063
  /** Live-value text drawn as its own canvas layer, above both the area gradient
1979
2064
  * and the left-edge fade, so the large number stays crisp at the left edge
1980
2065
  * instead of being washed out by the fade's `dstOut` blend. */
1981
- function ChartValueOverlay({ model }: { model: LiveChartModel }) {
2066
+ function ChartValueOverlay({
2067
+ model,
2068
+ degen,
2069
+ }: {
2070
+ model: LiveChartModel;
2071
+ degen: DegenState | null;
2072
+ }) {
1982
2073
  const {
1983
2074
  showValue,
1984
- degenShakeTransform,
1985
2075
  engine,
1986
2076
  effectivePadding,
1987
2077
  palette,
@@ -1991,11 +2081,10 @@ function ChartValueOverlay({ model }: { model: LiveChartModel }) {
1991
2081
  valueMomentumColor,
1992
2082
  reveal,
1993
2083
  } = model;
1994
-
1995
2084
  if (!showValue) return null;
1996
2085
 
1997
2086
  return (
1998
- <Group transform={degenShakeTransform}>
2087
+ <Group transform={degen?.shakeTransform}>
1999
2088
  <Group opacity={reveal.lineOpacity}>
2000
2089
  <ValueTextOverlay
2001
2090
  engine={engine}
@@ -2013,11 +2102,46 @@ function ChartValueOverlay({ model }: { model: LiveChartModel }) {
2013
2102
 
2014
2103
  /** Live-price badge, drawn above the scrub dim so the dim never clips its left
2015
2104
  * edge. Shares the degen shake transform so it tracks the shaken stack. */
2016
- function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
2017
- const { badgeCfg, badgeData, badgeFont, reveal, degenShakeTransform } = model;
2018
- if (!badgeCfg) return null;
2105
+ function ChartBadgeLayer({
2106
+ model,
2107
+ degen,
2108
+ }: {
2109
+ model: LiveChartModel;
2110
+ degen: DegenState | null;
2111
+ }) {
2112
+ const {
2113
+ badgeFont,
2114
+ reveal,
2115
+ engine,
2116
+ effectivePadding,
2117
+ palette,
2118
+ formatValue,
2119
+ momentumSV,
2120
+ metricsCfg,
2121
+ yAxisFloat,
2122
+ } = model;
2123
+ const badgeCfg = model.badgeCfg!;
2124
+ const badgeData = useBadge(
2125
+ engine,
2126
+ effectivePadding,
2127
+ palette,
2128
+ formatValue,
2129
+ badgeFont,
2130
+ badgeCfg.variant,
2131
+ badgeCfg.tail,
2132
+ momentumSV,
2133
+ badgeCfg.position,
2134
+ badgeCfg.background,
2135
+ metricsCfg.badge,
2136
+ metricsCfg.motion.badgeColorSpeed,
2137
+ yAxisFloat,
2138
+ engine.edgeValue,
2139
+ badgeCfg.followViewEdge,
2140
+ badgeCfg.radius,
2141
+ badgeCfg.textColor,
2142
+ );
2019
2143
  return (
2020
- <Group transform={degenShakeTransform}>
2144
+ <Group transform={degen?.shakeTransform}>
2021
2145
  <Group opacity={reveal.badgeOpacity}>
2022
2146
  <BadgeOverlay
2023
2147
  badge={badgeData}
@@ -2036,7 +2160,13 @@ function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
2036
2160
  * left-pinned badge (off-axis / `labelBadge`) and any label stay crisp instead
2037
2161
  * of being erased by the fade's dstOut. The lines/bands themselves render in the
2038
2162
  * base pass inside ChartStack (behind the chart content). */
2039
- function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
2163
+ function ChartRefBadgeLayer({
2164
+ model,
2165
+ degen,
2166
+ }: {
2167
+ model: LiveChartModel;
2168
+ degen: DegenState | null;
2169
+ }) {
2040
2170
  const {
2041
2171
  allRefLines,
2042
2172
  refLineCustom,
@@ -2053,12 +2183,11 @@ function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
2053
2183
  formatValue,
2054
2184
  skiaFont,
2055
2185
  fontProp,
2056
- degenShakeTransform,
2057
2186
  overlayScrubFade,
2058
2187
  } = model;
2059
2188
  if (allRefLines.length === 0) return null;
2060
2189
  return (
2061
- <Group transform={degenShakeTransform} opacity={overlayScrubFade}>
2190
+ <Group transform={degen?.shakeTransform} opacity={overlayScrubFade}>
2062
2191
  {allRefLines.map((rl, i) => (
2063
2192
  <ReferenceLineOverlay
2064
2193
  key={i}
@@ -2096,8 +2225,14 @@ function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
2096
2225
  * shake group so the rendered badge stays aligned with the untransformed tap
2097
2226
  * hit-test; it tracks the locked reticle, not the shaken stack. */
2098
2227
  function ChartScrubActionLayer({ model }: { model: LiveChartModel }) {
2099
- const { scrubActionCfg, crosshair, engine, effectivePadding, palette, skiaFont } =
2100
- model;
2228
+ const {
2229
+ scrubActionCfg,
2230
+ crosshair,
2231
+ engine,
2232
+ effectivePadding,
2233
+ palette,
2234
+ skiaFont,
2235
+ } = model;
2101
2236
  if (
2102
2237
  !scrubActionCfg ||
2103
2238
  !crosshair.lockActive ||
@@ -2126,8 +2261,79 @@ function ChartScrubActionLayer({ model }: { model: LiveChartModel }) {
2126
2261
  );
2127
2262
  }
2128
2263
 
2129
- export function LiveChart(props: LiveChartProps) {
2130
- const model = useLiveChartController(props);
2264
+ /**
2265
+ * RN-backed marker and reference-line slots. Their animated fade mapper is
2266
+ * registered only when at least one custom annotation renderer is present.
2267
+ */
2268
+ function ChartCustomAnnotations({ model }: { model: LiveChartModel }) {
2269
+ const {
2270
+ markersActive,
2271
+ markersSV,
2272
+ markerClusterCfg,
2273
+ renderMarker,
2274
+ renderReferenceLine,
2275
+ allRefLines,
2276
+ refLineCustom,
2277
+ dragValues,
2278
+ dragActive,
2279
+ engine,
2280
+ effectivePadding,
2281
+ formatValue,
2282
+ lineIsLinear,
2283
+ overlayScrubFade,
2284
+ } = model;
2285
+ const overlayFadeStyle = useAnimatedStyle(() => ({
2286
+ opacity: overlayScrubFade.get(),
2287
+ }));
2288
+
2289
+ return (
2290
+ <Animated.View
2291
+ pointerEvents="box-none"
2292
+ style={[StyleSheet.absoluteFill, overlayFadeStyle]}
2293
+ >
2294
+ {markersActive && renderMarker && (
2295
+ <CustomMarkerOverlay
2296
+ markers={markersSV}
2297
+ renderMarker={renderMarker}
2298
+ engine={engine}
2299
+ padding={effectivePadding}
2300
+ lineData={engine.data}
2301
+ lineLinear={lineIsLinear}
2302
+ cluster={markerClusterCfg}
2303
+ />
2304
+ )}
2305
+ {renderReferenceLine && allRefLines.length > 0 && (
2306
+ <CustomReferenceLineOverlay
2307
+ lines={allRefLines}
2308
+ renderReferenceLine={renderReferenceLine}
2309
+ custom={refLineCustom}
2310
+ engine={engine}
2311
+ padding={effectivePadding}
2312
+ formatValue={formatValue}
2313
+ dragValues={dragValues}
2314
+ dragActive={dragActive}
2315
+ />
2316
+ )}
2317
+ </Animated.View>
2318
+ );
2319
+ }
2320
+
2321
+ /** Owns the price/time projection worklets for the custom overlay slot. */
2322
+ function ChartCustomConsumerOverlay({ model }: { model: LiveChartModel }) {
2323
+ const { engine, effectivePadding, renderOverlay } = model;
2324
+ const overlayContext = useChartOverlayContext(engine, effectivePadding);
2325
+ return <ChartOverlayLayer render={renderOverlay!} context={overlayContext} />;
2326
+ }
2327
+
2328
+ function ChartView({
2329
+ model,
2330
+ yAxisEntries,
2331
+ degen,
2332
+ }: {
2333
+ model: LiveChartModel;
2334
+ yAxisEntries: YAxisEntries | null;
2335
+ degen: DegenState | null;
2336
+ }) {
2131
2337
  const {
2132
2338
  rootGesture,
2133
2339
  backgroundColor,
@@ -2143,33 +2349,18 @@ export function LiveChart(props: LiveChartProps) {
2143
2349
  topLabelCfg,
2144
2350
  bottomLabelCfg,
2145
2351
  markersActive,
2146
- markersSV,
2147
- markerClusterCfg,
2148
2352
  renderMarker,
2149
2353
  renderTooltip,
2150
2354
  renderOverlay,
2151
2355
  renderReferenceLine,
2152
2356
  allRefLines,
2153
- refLineCustom,
2154
- dragValues,
2155
- dragActive,
2156
- overlayContext,
2157
2357
  scrubCfg,
2158
2358
  crosshair,
2159
- isCandle,
2160
2359
  extremaTimeOffset,
2161
2360
  topConnector,
2162
2361
  bottomConnector,
2163
- lineIsLinear,
2164
- overlayScrubFade,
2165
2362
  } = model;
2166
2363
 
2167
- // Mirror the Skia overlay fade onto the RN custom-overlay siblings (custom
2168
- // markers / reference-line tags) so `scrub.hideOverlaysOnScrub` hides them too.
2169
- const overlayFadeStyle = useAnimatedStyle(() => ({
2170
- opacity: overlayScrubFade.get(),
2171
- }));
2172
-
2173
2364
  return (
2174
2365
  <GestureDetector gesture={rootGesture}>
2175
2366
  <View
@@ -2183,7 +2374,11 @@ export function LiveChart(props: LiveChartProps) {
2183
2374
  {/* Background fills first, then the left-edge fade (a canvas-space sibling
2184
2375
  so dstOut blends correctly), then the line stack on top — so the fade
2185
2376
  softens only the fills and the line stays crisp at the left edge. */}
2186
- <ChartFillLayer model={model} />
2377
+ <ChartFillLayer
2378
+ model={model}
2379
+ yAxisEntries={yAxisEntries}
2380
+ degen={degen}
2381
+ />
2187
2382
 
2188
2383
  {leftEdgeFadeCfg && (
2189
2384
  <LeftEdgeFade
@@ -2196,28 +2391,38 @@ export function LiveChart(props: LiveChartProps) {
2196
2391
  )}
2197
2392
 
2198
2393
  {/* Line stack above the fade so the line stays crisp at the left edge. */}
2199
- <ChartStack model={model} />
2394
+ <ChartStack
2395
+ model={model}
2396
+ yAxisEntries={yAxisEntries}
2397
+ degen={degen}
2398
+ />
2200
2399
 
2201
2400
  {/* "extrema-edge" connector lines (dot → edge readout), above the chart
2202
2401
  content so the dashed guide reads over the line / candles. */}
2203
- <ExtremaConnectorOverlay
2204
- engine={engine}
2205
- padding={effectivePadding}
2206
- extremaTimeOffset={extremaTimeOffset}
2207
- top={topConnector}
2208
- bottom={bottomConnector}
2209
- />
2402
+ {(topConnector || bottomConnector) && (
2403
+ <ExtremaConnectorOverlay
2404
+ engine={engine}
2405
+ padding={effectivePadding}
2406
+ extremaTimeOffset={extremaTimeOffset}
2407
+ top={topConnector}
2408
+ bottom={bottomConnector}
2409
+ />
2410
+ )}
2210
2411
 
2211
2412
  {/* Reference-line badges + labels above the fade so they stay crisp. */}
2212
- <ChartRefBadgeLayer model={model} />
2413
+ <ChartRefBadgeLayer model={model} degen={degen} />
2213
2414
 
2214
- <ChartValueOverlay model={model} />
2415
+ <ChartValueOverlay model={model} degen={degen} />
2215
2416
 
2216
- <ChartScrubLayer model={model} />
2417
+ {model.tradeStreamResolved && model.tradeStream && (
2418
+ <ChartTradeStreamLayer model={model} degen={degen} />
2419
+ )}
2420
+
2421
+ <ChartScrubLayer model={model} degen={degen} />
2217
2422
 
2218
2423
  {/* Live-price badge on top of the scrub dim so the dim never clips
2219
2424
  its left edge (the badge tracks the live value, not the scrub). */}
2220
- <ChartBadgeLayer model={model} />
2425
+ {model.badgeCfg && <ChartBadgeLayer model={model} degen={degen} />}
2221
2426
 
2222
2427
  {/* Scrub-action reticle + action badge — top-most, no shake transform. */}
2223
2428
  <ChartScrubActionLayer model={model} />
@@ -2225,58 +2430,26 @@ export function LiveChart(props: LiveChartProps) {
2225
2430
 
2226
2431
  {/* RN labels floated over the canvas (sibling of <Canvas>, an RN view).
2227
2432
  Pinned to the plot's top/bottom edges via the resolved padding. */}
2228
- <AxisLabelOverlay
2229
- topLabel={topLabelCfg}
2230
- bottomLabel={bottomLabelCfg}
2231
- engine={engine}
2232
- formatValue={formatValue}
2233
- defaultColor={palette.gridLabel}
2234
- padding={effectivePadding}
2235
- extremaTimeOffset={extremaTimeOffset}
2236
- />
2433
+ {(topLabelCfg || bottomLabelCfg) && (
2434
+ <AxisLabelOverlay
2435
+ topLabel={topLabelCfg}
2436
+ bottomLabel={bottomLabelCfg}
2437
+ engine={engine}
2438
+ formatValue={formatValue}
2439
+ defaultColor={palette.gridLabel}
2440
+ padding={effectivePadding}
2441
+ extremaTimeOffset={extremaTimeOffset}
2442
+ />
2443
+ )}
2237
2444
 
2238
2445
  {/* Custom-rendered markers — RN views floated over the canvas (non-Skia),
2239
2446
  pinned to each marker's live position. Sibling of <Canvas>. Wrapped in
2240
2447
  a box-none fade layer so `scrub.hideOverlaysOnScrub` hides them with the
2241
2448
  Skia markers (the wrapper is full-bleed; children keep their own
2242
2449
  absolute positions). */}
2243
- {markersActive && renderMarker && (
2244
- <Animated.View
2245
- pointerEvents="box-none"
2246
- style={[StyleSheet.absoluteFill, overlayFadeStyle]}
2247
- >
2248
- <CustomMarkerOverlay
2249
- markers={markersSV}
2250
- renderMarker={renderMarker}
2251
- engine={engine}
2252
- padding={effectivePadding}
2253
- lineData={engine.data}
2254
- lineLinear={lineIsLinear}
2255
- cluster={markerClusterCfg}
2256
- />
2257
- </Animated.View>
2258
- )}
2259
-
2260
- {/* Custom-rendered reference-line tags — RN views floated over the canvas
2261
- (non-Skia), pinned to each Form-A line's value. Sibling of <Canvas>.
2262
- Built-in Skia tags for these lines are suppressed (no double-draw).
2263
- Same box-none fade wrapper as the custom markers above. */}
2264
- {renderReferenceLine && allRefLines.length > 0 && (
2265
- <Animated.View
2266
- pointerEvents="box-none"
2267
- style={[StyleSheet.absoluteFill, overlayFadeStyle]}
2268
- >
2269
- <CustomReferenceLineOverlay
2270
- lines={allRefLines}
2271
- renderReferenceLine={renderReferenceLine}
2272
- custom={refLineCustom}
2273
- engine={engine}
2274
- padding={effectivePadding}
2275
- formatValue={formatValue}
2276
- dragValues={dragValues}
2277
- dragActive={dragActive}
2278
- />
2279
- </Animated.View>
2450
+ {((markersActive && renderMarker) ||
2451
+ (renderReferenceLine && allRefLines.length > 0)) && (
2452
+ <ChartCustomAnnotations model={model} />
2280
2453
  )}
2281
2454
 
2282
2455
  {/* Custom scrub tooltip — an RN view floated over the canvas (non-Skia),
@@ -2304,10 +2477,19 @@ export function LiveChart(props: LiveChartProps) {
2304
2477
  {/* Custom consumer overlay — an RN view tree floated over the canvas with
2305
2478
  the price↔pixel / time↔pixel bridge, for order / avg-entry / liquidation
2306
2479
  tags etc. Topmost RN sibling; `box-none` so empty areas still scrub. */}
2307
- {renderOverlay && (
2308
- <ChartOverlayLayer render={renderOverlay} context={overlayContext} />
2309
- )}
2480
+ {renderOverlay && <ChartCustomConsumerOverlay model={model} />}
2310
2481
  </View>
2311
2482
  </GestureDetector>
2312
2483
  );
2313
2484
  }
2485
+
2486
+ export function LiveChart(props: LiveChartProps) {
2487
+ const model = useLiveChartController(props);
2488
+ if (model.yAxisCfg) {
2489
+ return <ChartWithYAxis model={model} />;
2490
+ }
2491
+ if (model.degenCfg) {
2492
+ return <ChartWithDegen model={model} yAxisEntries={null} />;
2493
+ }
2494
+ return <ChartView model={model} yAxisEntries={null} degen={null} />;
2495
+ }