react-native-livechart 4.9.1 → 4.10.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.
@@ -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,156 @@ 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,
1415
+ dotY,
1430
1416
  badgeUsesRightGutter,
1431
1417
  badgeCfg,
1432
- gridStyleCfg,
1418
+ badgeFont,
1433
1419
  metricsCfg,
1420
+ gridStyleCfg,
1421
+ yAxisFloat,
1422
+ } = model;
1423
+ return (
1424
+ <Group opacity={reveal.yAxisOpacity}>
1425
+ <YAxisOverlay
1426
+ variant={variant}
1427
+ float={variant === "labels" && yAxisFloat}
1428
+ entries={entries}
1429
+ engine={engine}
1430
+ padding={effectivePadding}
1431
+ palette={palette}
1432
+ font={skiaFont}
1433
+ badge={badgeUsesRightGutter}
1434
+ badgeTail={badgeCfg?.tail ?? true}
1435
+ badgeMetrics={metricsCfg.badge}
1436
+ badgeCenterY={badgeUsesRightGutter ? dotY : undefined}
1437
+ badgeFontSize={badgeUsesRightGutter ? badgeFont.getSize() : undefined}
1438
+ badgeOffsetY={badgeCfg?.offsetY ?? 0}
1439
+ gridStyle={gridStyleCfg}
1440
+ />
1441
+ </Group>
1442
+ );
1443
+ }
1444
+
1445
+ /** Owns the X-axis worklet and only mounts when `xAxis` is enabled. */
1446
+ function ChartXAxisLayer({ model }: { model: LiveChartModel }) {
1447
+ const {
1448
+ engine,
1449
+ effectivePadding,
1450
+ formatTime,
1451
+ skiaFont,
1452
+ palette,
1453
+ volumeBandHeight,
1454
+ } = model;
1455
+ const { xAxisEntries } = useXAxis(
1456
+ engine,
1457
+ effectivePadding,
1458
+ formatTime,
1459
+ skiaFont,
1460
+ );
1461
+ return (
1462
+ <XAxisOverlay
1463
+ entries={xAxisEntries}
1464
+ engine={engine}
1465
+ padding={effectivePadding}
1466
+ palette={palette}
1467
+ font={skiaFont}
1468
+ volumeBandHeight={volumeBandHeight}
1469
+ />
1470
+ );
1471
+ }
1472
+
1473
+ /**
1474
+ * Background fills drawn BENEATH the left-edge fade: the y-axis grid, the area
1475
+ * gradient, and the threshold profit/loss band. Split out from `ChartStack` so
1476
+ * the fade's `dstOut` only softens the fills — the line and everything above it
1477
+ * (drawn in `ChartStack`, after the fade) stay crisp at the left edge.
1478
+ */
1479
+ function ChartFillLayer({
1480
+ model,
1481
+ yAxisEntries,
1482
+ degen,
1483
+ }: {
1484
+ model: LiveChartModel;
1485
+ yAxisEntries: YAxisEntries | null;
1486
+ degen: DegenState | null;
1487
+ }) {
1488
+ const {
1489
+ yAxisCfg,
1490
+ yAxisFloat,
1491
+ reveal,
1492
+ effectivePadding,
1434
1493
  gradientCfg,
1435
1494
  areaDotsCfg,
1436
1495
  areaDotColorVec,
@@ -1446,27 +1505,17 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
1446
1505
  thresholdSeriesHasPoints,
1447
1506
  thresholdFillUniforms,
1448
1507
  } = model;
1449
-
1450
1508
  return (
1451
- <Group transform={degenShakeTransform}>
1509
+ <Group transform={degen?.shakeTransform}>
1452
1510
  {/* Y-axis. Default: grid + labels here (in a reserved gutter). Floating
1453
1511
  mode: grid only — the labels + a soft edge fade draw above the candles
1454
1512
  in ChartStack so the plot runs full-width and candles dim under them. */}
1455
1513
  {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>
1514
+ <ChartYAxisLayer
1515
+ model={model}
1516
+ variant={yAxisFloat ? "grid" : "all"}
1517
+ entries={yAxisEntries!}
1518
+ />
1470
1519
  )}
1471
1520
 
1472
1521
  {/* Dot-lattice area fill (the under-line `fillPath` painted with a dot
@@ -1529,13 +1578,95 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
1529
1578
  );
1530
1579
  }
1531
1580
 
1581
+ /**
1582
+ * Candle/volume paths are a mode-specific subsystem. Keeping their hooks in a
1583
+ * child means a line chart never registers the candle-width frame callback or
1584
+ * the six derived path worklets.
1585
+ */
1586
+ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1587
+ const {
1588
+ engine,
1589
+ effectivePadding,
1590
+ candlesEngine,
1591
+ liveEngine,
1592
+ candleWidth,
1593
+ metricsCfg,
1594
+ volumeBandHeight,
1595
+ volumeCfg,
1596
+ isStatic,
1597
+ transitionsCfg,
1598
+ candleGroupOpacity,
1599
+ palette,
1600
+ volumeOpacity,
1601
+ volumeUpColor,
1602
+ volumeDownColor,
1603
+ } = model;
1604
+ const {
1605
+ upBodiesPath,
1606
+ downBodiesPath,
1607
+ upWicksPath,
1608
+ downWicksPath,
1609
+ upBarsPath,
1610
+ downBarsPath,
1611
+ } = useCandlePaths(
1612
+ engine,
1613
+ effectivePadding,
1614
+ candlesEngine,
1615
+ liveEngine,
1616
+ candleWidth,
1617
+ true,
1618
+ metricsCfg.candle,
1619
+ volumeBandHeight,
1620
+ volumeCfg?.radius ?? 0,
1621
+ !isStatic,
1622
+ transitionsCfg.candleLerpSpeed,
1623
+ );
1624
+
1625
+ return (
1626
+ <>
1627
+ <Group opacity={candleGroupOpacity}>
1628
+ <Path
1629
+ path={upWicksPath}
1630
+ style="stroke"
1631
+ strokeWidth={metricsCfg.candle.wickWidth}
1632
+ color={palette.wickUp}
1633
+ />
1634
+ <Path
1635
+ path={downWicksPath}
1636
+ style="stroke"
1637
+ strokeWidth={metricsCfg.candle.wickWidth}
1638
+ color={palette.wickDown}
1639
+ />
1640
+ <Path path={upBodiesPath} style="fill" color={palette.candleUp} />
1641
+ <Path path={downBodiesPath} style="fill" color={palette.candleDown} />
1642
+ </Group>
1643
+
1644
+ {volumeCfg && (
1645
+ <Group opacity={candleGroupOpacity}>
1646
+ <Group opacity={volumeOpacity}>
1647
+ <Path path={upBarsPath} style="fill" color={volumeUpColor} />
1648
+ <Path path={downBarsPath} style="fill" color={volumeDownColor} />
1649
+ </Group>
1650
+ </Group>
1651
+ )}
1652
+ </>
1653
+ );
1654
+ }
1655
+
1532
1656
  /** Main shaken chart stack drawn ABOVE the left-edge fade so the line stays crisp:
1533
1657
  * segment dividers, value/reference lines, the line/candles, axes, dot, degen,
1534
1658
  * markers, and the loading/empty art. Background fills are in `ChartFillLayer`
1535
1659
  * (below the fade); the live value text is `ChartValueOverlay` (above the fade). */
1536
- function ChartStack({ model }: { model: LiveChartModel }) {
1660
+ function ChartStack({
1661
+ model,
1662
+ yAxisEntries,
1663
+ degen,
1664
+ }: {
1665
+ model: LiveChartModel;
1666
+ yAxisEntries: YAxisEntries | null;
1667
+ degen: DegenState | null;
1668
+ }) {
1537
1669
  const {
1538
- degenShakeTransform,
1539
1670
  reveal,
1540
1671
  engine,
1541
1672
  effectivePadding,
@@ -1567,27 +1698,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1567
1698
  lineIsLinear,
1568
1699
  strokeWidth,
1569
1700
  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,
1701
+ isCandle,
1582
1702
  xAxisCfg,
1583
- xAxisEntries,
1584
1703
  dotX,
1585
1704
  liveDotOpacity,
1586
1705
  pulseCfg,
1587
1706
  dotCfg,
1588
1707
  degenCfg,
1589
- degenPack,
1590
- degenPackRevision,
1591
1708
  markersActive,
1592
1709
  markersSV,
1593
1710
  markerClusterCfg,
@@ -1600,17 +1717,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1600
1717
  layoutWidth,
1601
1718
  yAxisCfg,
1602
1719
  yAxisFloat,
1603
- yAxisEntries,
1604
- badgeUsesRightGutter,
1605
- gridStyleCfg,
1606
1720
  loadingLineColor,
1607
1721
  loadingStrokeWidth,
1608
1722
  loadingAmplitude,
1609
1723
  loadingSpeed,
1610
1724
  } = model;
1611
-
1612
1725
  return (
1613
- <Group transform={degenShakeTransform}>
1726
+ <Group transform={degen?.shakeTransform}>
1614
1727
  {/* Segment dividers + labels (behind the line). The scrub-focus emphasis is
1615
1728
  painted on the line stroke itself, below — this overlay draws no fill. */}
1616
1729
  {resolvedSegments.map((seg, i) => (
@@ -1722,73 +1835,22 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1722
1835
  </Path>
1723
1836
  </Group>
1724
1837
 
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
- )}
1838
+ {isCandle && <ChartCandleLayer model={model} />}
1758
1839
 
1759
1840
  {/* Floating axis: the labels float ABOVE the candles (right-aligned at the
1760
1841
  edge) so the plot runs full-width and candles stay fully visible behind
1761
1842
  them. (Default non-floating axis draws grid + labels in ChartFillLayer.) */}
1762
1843
  {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>
1844
+ <ChartYAxisLayer
1845
+ model={model}
1846
+ variant="labels"
1847
+ entries={yAxisEntries!}
1848
+ />
1778
1849
  )}
1779
1850
 
1780
1851
  {/* X-axis time labels. With a volume band the bottom padding is inflated by
1781
1852
  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
- )}
1853
+ {xAxisCfg && <ChartXAxisLayer model={model} />}
1792
1854
 
1793
1855
  {/* Live dot — the badge is drawn later (after the scrub layer) so the
1794
1856
  scrub dim never clips the live-price badge's left edge. Hidden while
@@ -1812,8 +1874,8 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1812
1874
  {degenCfg && (
1813
1875
  <Group opacity={reveal.dotOpacity}>
1814
1876
  <DegenParticlesOverlay
1815
- pack={degenPack}
1816
- packRevision={degenPackRevision}
1877
+ pack={degen!.pack}
1878
+ packRevision={degen!.packRevision}
1817
1879
  engine={engine}
1818
1880
  palette={palette}
1819
1881
  particleSlotCount={degenCfg.particleSlotCount}
@@ -1882,18 +1944,58 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1882
1944
  );
1883
1945
  }
1884
1946
 
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 }) {
1947
+ /** Owns the trade-tape frame callback and only mounts with a trade stream. */
1948
+ function ChartTradeStreamLayer({
1949
+ model,
1950
+ degen,
1951
+ }: {
1952
+ model: LiveChartModel;
1953
+ degen: DegenState | null;
1954
+ }) {
1888
1955
  const {
1956
+ engine,
1957
+ tradeStream,
1889
1958
  tradeStreamResolved,
1959
+ effectivePadding,
1960
+ palette,
1961
+ skiaFont,
1962
+ reveal,
1963
+ isStatic,
1964
+ } = model;
1965
+ const tradeMarkers = useTradeStream(
1966
+ engine,
1967
+ tradeStream!,
1968
+ effectivePadding,
1969
+ !isStatic,
1970
+ !isStatic,
1971
+ );
1972
+ return (
1973
+ <Group transform={degen?.shakeTransform}>
1974
+ <TradeStreamOverlay
1975
+ markers={tradeMarkers}
1976
+ palette={palette}
1977
+ padding={effectivePadding}
1978
+ font={skiaFont}
1979
+ opacity={reveal.dotOpacity}
1980
+ labelOffsetX={tradeStreamResolved!.labelOffsetX}
1981
+ />
1982
+ </Group>
1983
+ );
1984
+ }
1985
+
1986
+ /** Scrub crosshair/tooltip drawn in canvas space on top of the shaken stack. */
1987
+ function ChartScrubLayer({
1988
+ model,
1989
+ degen,
1990
+ }: {
1991
+ model: LiveChartModel;
1992
+ degen: DegenState | null;
1993
+ }) {
1994
+ const {
1890
1995
  scrubCfg,
1891
- degenShakeTransform,
1892
- tradeMarkers,
1893
1996
  palette,
1894
1997
  effectivePadding,
1895
1998
  skiaFont,
1896
- reveal,
1897
1999
  crosshair,
1898
2000
  isCandle,
1899
2001
  pulseCfg,
@@ -1902,13 +2004,12 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
1902
2004
  selectionColor,
1903
2005
  renderTooltip,
1904
2006
  } = model;
1905
-
1906
2007
  // A custom tooltip is an RN overlay (sibling of <Canvas>), so the built-in
1907
2008
  // Skia tooltip is suppressed here while it's active — the line pill in line
1908
2009
  // mode, and the OHLC stack in candle mode (see the stack gate below).
1909
2010
  const customTooltipActive = renderTooltip != null;
1910
2011
 
1911
- if (!tradeStreamResolved && !scrubCfg) return null;
2012
+ if (!scrubCfg) return null;
1912
2013
 
1913
2014
  // Extend the scrub dim past the plot's right edge to fully cover the live dot
1914
2015
  // (with its halo) and pulse ring, all centered on that edge. The gutter
@@ -1919,18 +2020,7 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
1919
2020
  );
1920
2021
 
1921
2022
  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
-
2023
+ <Group transform={degen?.shakeTransform}>
1934
2024
  {scrubCfg && (
1935
2025
  <CrosshairOverlay
1936
2026
  scrubX={crosshair.scrubX}
@@ -1978,10 +2068,15 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
1978
2068
  /** Live-value text drawn as its own canvas layer, above both the area gradient
1979
2069
  * and the left-edge fade, so the large number stays crisp at the left edge
1980
2070
  * instead of being washed out by the fade's `dstOut` blend. */
1981
- function ChartValueOverlay({ model }: { model: LiveChartModel }) {
2071
+ function ChartValueOverlay({
2072
+ model,
2073
+ degen,
2074
+ }: {
2075
+ model: LiveChartModel;
2076
+ degen: DegenState | null;
2077
+ }) {
1982
2078
  const {
1983
2079
  showValue,
1984
- degenShakeTransform,
1985
2080
  engine,
1986
2081
  effectivePadding,
1987
2082
  palette,
@@ -1991,11 +2086,10 @@ function ChartValueOverlay({ model }: { model: LiveChartModel }) {
1991
2086
  valueMomentumColor,
1992
2087
  reveal,
1993
2088
  } = model;
1994
-
1995
2089
  if (!showValue) return null;
1996
2090
 
1997
2091
  return (
1998
- <Group transform={degenShakeTransform}>
2092
+ <Group transform={degen?.shakeTransform}>
1999
2093
  <Group opacity={reveal.lineOpacity}>
2000
2094
  <ValueTextOverlay
2001
2095
  engine={engine}
@@ -2013,11 +2107,46 @@ function ChartValueOverlay({ model }: { model: LiveChartModel }) {
2013
2107
 
2014
2108
  /** Live-price badge, drawn above the scrub dim so the dim never clips its left
2015
2109
  * 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;
2110
+ function ChartBadgeLayer({
2111
+ model,
2112
+ degen,
2113
+ }: {
2114
+ model: LiveChartModel;
2115
+ degen: DegenState | null;
2116
+ }) {
2117
+ const {
2118
+ badgeFont,
2119
+ reveal,
2120
+ engine,
2121
+ effectivePadding,
2122
+ palette,
2123
+ formatValue,
2124
+ momentumSV,
2125
+ metricsCfg,
2126
+ yAxisFloat,
2127
+ } = model;
2128
+ const badgeCfg = model.badgeCfg!;
2129
+ const badgeData = useBadge(
2130
+ engine,
2131
+ effectivePadding,
2132
+ palette,
2133
+ formatValue,
2134
+ badgeFont,
2135
+ badgeCfg.variant,
2136
+ badgeCfg.tail,
2137
+ momentumSV,
2138
+ badgeCfg.position,
2139
+ badgeCfg.background,
2140
+ metricsCfg.badge,
2141
+ metricsCfg.motion.badgeColorSpeed,
2142
+ yAxisFloat,
2143
+ engine.edgeValue,
2144
+ badgeCfg.followViewEdge,
2145
+ badgeCfg.radius,
2146
+ badgeCfg.textColor,
2147
+ );
2019
2148
  return (
2020
- <Group transform={degenShakeTransform}>
2149
+ <Group transform={degen?.shakeTransform}>
2021
2150
  <Group opacity={reveal.badgeOpacity}>
2022
2151
  <BadgeOverlay
2023
2152
  badge={badgeData}
@@ -2036,7 +2165,13 @@ function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
2036
2165
  * left-pinned badge (off-axis / `labelBadge`) and any label stay crisp instead
2037
2166
  * of being erased by the fade's dstOut. The lines/bands themselves render in the
2038
2167
  * base pass inside ChartStack (behind the chart content). */
2039
- function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
2168
+ function ChartRefBadgeLayer({
2169
+ model,
2170
+ degen,
2171
+ }: {
2172
+ model: LiveChartModel;
2173
+ degen: DegenState | null;
2174
+ }) {
2040
2175
  const {
2041
2176
  allRefLines,
2042
2177
  refLineCustom,
@@ -2053,12 +2188,11 @@ function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
2053
2188
  formatValue,
2054
2189
  skiaFont,
2055
2190
  fontProp,
2056
- degenShakeTransform,
2057
2191
  overlayScrubFade,
2058
2192
  } = model;
2059
2193
  if (allRefLines.length === 0) return null;
2060
2194
  return (
2061
- <Group transform={degenShakeTransform} opacity={overlayScrubFade}>
2195
+ <Group transform={degen?.shakeTransform} opacity={overlayScrubFade}>
2062
2196
  {allRefLines.map((rl, i) => (
2063
2197
  <ReferenceLineOverlay
2064
2198
  key={i}
@@ -2096,8 +2230,14 @@ function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
2096
2230
  * shake group so the rendered badge stays aligned with the untransformed tap
2097
2231
  * hit-test; it tracks the locked reticle, not the shaken stack. */
2098
2232
  function ChartScrubActionLayer({ model }: { model: LiveChartModel }) {
2099
- const { scrubActionCfg, crosshair, engine, effectivePadding, palette, skiaFont } =
2100
- model;
2233
+ const {
2234
+ scrubActionCfg,
2235
+ crosshair,
2236
+ engine,
2237
+ effectivePadding,
2238
+ palette,
2239
+ skiaFont,
2240
+ } = model;
2101
2241
  if (
2102
2242
  !scrubActionCfg ||
2103
2243
  !crosshair.lockActive ||
@@ -2126,8 +2266,79 @@ function ChartScrubActionLayer({ model }: { model: LiveChartModel }) {
2126
2266
  );
2127
2267
  }
2128
2268
 
2129
- export function LiveChart(props: LiveChartProps) {
2130
- const model = useLiveChartController(props);
2269
+ /**
2270
+ * RN-backed marker and reference-line slots. Their animated fade mapper is
2271
+ * registered only when at least one custom annotation renderer is present.
2272
+ */
2273
+ function ChartCustomAnnotations({ model }: { model: LiveChartModel }) {
2274
+ const {
2275
+ markersActive,
2276
+ markersSV,
2277
+ markerClusterCfg,
2278
+ renderMarker,
2279
+ renderReferenceLine,
2280
+ allRefLines,
2281
+ refLineCustom,
2282
+ dragValues,
2283
+ dragActive,
2284
+ engine,
2285
+ effectivePadding,
2286
+ formatValue,
2287
+ lineIsLinear,
2288
+ overlayScrubFade,
2289
+ } = model;
2290
+ const overlayFadeStyle = useAnimatedStyle(() => ({
2291
+ opacity: overlayScrubFade.get(),
2292
+ }));
2293
+
2294
+ return (
2295
+ <Animated.View
2296
+ pointerEvents="box-none"
2297
+ style={[StyleSheet.absoluteFill, overlayFadeStyle]}
2298
+ >
2299
+ {markersActive && renderMarker && (
2300
+ <CustomMarkerOverlay
2301
+ markers={markersSV}
2302
+ renderMarker={renderMarker}
2303
+ engine={engine}
2304
+ padding={effectivePadding}
2305
+ lineData={engine.data}
2306
+ lineLinear={lineIsLinear}
2307
+ cluster={markerClusterCfg}
2308
+ />
2309
+ )}
2310
+ {renderReferenceLine && allRefLines.length > 0 && (
2311
+ <CustomReferenceLineOverlay
2312
+ lines={allRefLines}
2313
+ renderReferenceLine={renderReferenceLine}
2314
+ custom={refLineCustom}
2315
+ engine={engine}
2316
+ padding={effectivePadding}
2317
+ formatValue={formatValue}
2318
+ dragValues={dragValues}
2319
+ dragActive={dragActive}
2320
+ />
2321
+ )}
2322
+ </Animated.View>
2323
+ );
2324
+ }
2325
+
2326
+ /** Owns the price/time projection worklets for the custom overlay slot. */
2327
+ function ChartCustomConsumerOverlay({ model }: { model: LiveChartModel }) {
2328
+ const { engine, effectivePadding, renderOverlay } = model;
2329
+ const overlayContext = useChartOverlayContext(engine, effectivePadding);
2330
+ return <ChartOverlayLayer render={renderOverlay!} context={overlayContext} />;
2331
+ }
2332
+
2333
+ function ChartView({
2334
+ model,
2335
+ yAxisEntries,
2336
+ degen,
2337
+ }: {
2338
+ model: LiveChartModel;
2339
+ yAxisEntries: YAxisEntries | null;
2340
+ degen: DegenState | null;
2341
+ }) {
2131
2342
  const {
2132
2343
  rootGesture,
2133
2344
  backgroundColor,
@@ -2143,33 +2354,18 @@ export function LiveChart(props: LiveChartProps) {
2143
2354
  topLabelCfg,
2144
2355
  bottomLabelCfg,
2145
2356
  markersActive,
2146
- markersSV,
2147
- markerClusterCfg,
2148
2357
  renderMarker,
2149
2358
  renderTooltip,
2150
2359
  renderOverlay,
2151
2360
  renderReferenceLine,
2152
2361
  allRefLines,
2153
- refLineCustom,
2154
- dragValues,
2155
- dragActive,
2156
- overlayContext,
2157
2362
  scrubCfg,
2158
2363
  crosshair,
2159
- isCandle,
2160
2364
  extremaTimeOffset,
2161
2365
  topConnector,
2162
2366
  bottomConnector,
2163
- lineIsLinear,
2164
- overlayScrubFade,
2165
2367
  } = model;
2166
2368
 
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
2369
  return (
2174
2370
  <GestureDetector gesture={rootGesture}>
2175
2371
  <View
@@ -2183,7 +2379,11 @@ export function LiveChart(props: LiveChartProps) {
2183
2379
  {/* Background fills first, then the left-edge fade (a canvas-space sibling
2184
2380
  so dstOut blends correctly), then the line stack on top — so the fade
2185
2381
  softens only the fills and the line stays crisp at the left edge. */}
2186
- <ChartFillLayer model={model} />
2382
+ <ChartFillLayer
2383
+ model={model}
2384
+ yAxisEntries={yAxisEntries}
2385
+ degen={degen}
2386
+ />
2187
2387
 
2188
2388
  {leftEdgeFadeCfg && (
2189
2389
  <LeftEdgeFade
@@ -2196,28 +2396,38 @@ export function LiveChart(props: LiveChartProps) {
2196
2396
  )}
2197
2397
 
2198
2398
  {/* Line stack above the fade so the line stays crisp at the left edge. */}
2199
- <ChartStack model={model} />
2399
+ <ChartStack
2400
+ model={model}
2401
+ yAxisEntries={yAxisEntries}
2402
+ degen={degen}
2403
+ />
2200
2404
 
2201
2405
  {/* "extrema-edge" connector lines (dot → edge readout), above the chart
2202
2406
  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
- />
2407
+ {(topConnector || bottomConnector) && (
2408
+ <ExtremaConnectorOverlay
2409
+ engine={engine}
2410
+ padding={effectivePadding}
2411
+ extremaTimeOffset={extremaTimeOffset}
2412
+ top={topConnector}
2413
+ bottom={bottomConnector}
2414
+ />
2415
+ )}
2210
2416
 
2211
2417
  {/* Reference-line badges + labels above the fade so they stay crisp. */}
2212
- <ChartRefBadgeLayer model={model} />
2418
+ <ChartRefBadgeLayer model={model} degen={degen} />
2213
2419
 
2214
- <ChartValueOverlay model={model} />
2420
+ <ChartValueOverlay model={model} degen={degen} />
2215
2421
 
2216
- <ChartScrubLayer model={model} />
2422
+ {model.tradeStreamResolved && model.tradeStream && (
2423
+ <ChartTradeStreamLayer model={model} degen={degen} />
2424
+ )}
2425
+
2426
+ <ChartScrubLayer model={model} degen={degen} />
2217
2427
 
2218
2428
  {/* Live-price badge on top of the scrub dim so the dim never clips
2219
2429
  its left edge (the badge tracks the live value, not the scrub). */}
2220
- <ChartBadgeLayer model={model} />
2430
+ {model.badgeCfg && <ChartBadgeLayer model={model} degen={degen} />}
2221
2431
 
2222
2432
  {/* Scrub-action reticle + action badge — top-most, no shake transform. */}
2223
2433
  <ChartScrubActionLayer model={model} />
@@ -2225,58 +2435,26 @@ export function LiveChart(props: LiveChartProps) {
2225
2435
 
2226
2436
  {/* RN labels floated over the canvas (sibling of <Canvas>, an RN view).
2227
2437
  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
- />
2438
+ {(topLabelCfg || bottomLabelCfg) && (
2439
+ <AxisLabelOverlay
2440
+ topLabel={topLabelCfg}
2441
+ bottomLabel={bottomLabelCfg}
2442
+ engine={engine}
2443
+ formatValue={formatValue}
2444
+ defaultColor={palette.gridLabel}
2445
+ padding={effectivePadding}
2446
+ extremaTimeOffset={extremaTimeOffset}
2447
+ />
2448
+ )}
2237
2449
 
2238
2450
  {/* Custom-rendered markers — RN views floated over the canvas (non-Skia),
2239
2451
  pinned to each marker's live position. Sibling of <Canvas>. Wrapped in
2240
2452
  a box-none fade layer so `scrub.hideOverlaysOnScrub` hides them with the
2241
2453
  Skia markers (the wrapper is full-bleed; children keep their own
2242
2454
  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>
2455
+ {((markersActive && renderMarker) ||
2456
+ (renderReferenceLine && allRefLines.length > 0)) && (
2457
+ <ChartCustomAnnotations model={model} />
2280
2458
  )}
2281
2459
 
2282
2460
  {/* Custom scrub tooltip — an RN view floated over the canvas (non-Skia),
@@ -2304,10 +2482,19 @@ export function LiveChart(props: LiveChartProps) {
2304
2482
  {/* Custom consumer overlay — an RN view tree floated over the canvas with
2305
2483
  the price↔pixel / time↔pixel bridge, for order / avg-entry / liquidation
2306
2484
  tags etc. Topmost RN sibling; `box-none` so empty areas still scrub. */}
2307
- {renderOverlay && (
2308
- <ChartOverlayLayer render={renderOverlay} context={overlayContext} />
2309
- )}
2485
+ {renderOverlay && <ChartCustomConsumerOverlay model={model} />}
2310
2486
  </View>
2311
2487
  </GestureDetector>
2312
2488
  );
2313
2489
  }
2490
+
2491
+ export function LiveChart(props: LiveChartProps) {
2492
+ const model = useLiveChartController(props);
2493
+ if (model.yAxisCfg) {
2494
+ return <ChartWithYAxis model={model} />;
2495
+ }
2496
+ if (model.degenCfg) {
2497
+ return <ChartWithDegen model={model} yAxisEntries={null} />;
2498
+ }
2499
+ return <ChartView model={model} yAxisEntries={null} degen={null} />;
2500
+ }