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.
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +9 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +2 -0
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +2 -6
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/delayedPanGuard.d.ts +31 -0
- package/dist/hooks/delayedPanGuard.d.ts.map +1 -0
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useXAxis.d.ts.map +1 -1
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairOverlay.tsx +6 -1
- package/src/components/LiveChart.tsx +505 -318
- package/src/components/MarkerOverlay.tsx +11 -16
- package/src/components/YAxisOverlay.tsx +43 -1
- package/src/draw/markerAtlas.ts +22 -2
- package/src/hooks/crosshairShared.ts +19 -18
- package/src/hooks/delayedPanGuard.ts +80 -0
- package/src/hooks/useCrosshair.ts +232 -182
- package/src/hooks/useCrosshairSeries.ts +38 -0
- package/src/hooks/useXAxis.ts +21 -5
- package/src/types.ts +7 -2
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
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
|
|
1376
|
+
function ChartWithYAxis({ model }: { model: LiveChartModel }) {
|
|
1420
1377
|
const {
|
|
1421
|
-
|
|
1378
|
+
engine,
|
|
1379
|
+
effectivePadding,
|
|
1380
|
+
formatValue,
|
|
1381
|
+
skiaFont,
|
|
1422
1382
|
yAxisCfg,
|
|
1423
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
-
<
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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({
|
|
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
|
-
|
|
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={
|
|
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
|
-
{
|
|
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
|
-
<
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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={
|
|
1816
|
-
packRevision={
|
|
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
|
-
/**
|
|
1886
|
-
|
|
1887
|
-
|
|
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 (!
|
|
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={
|
|
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({
|
|
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={
|
|
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({
|
|
2017
|
-
|
|
2018
|
-
|
|
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={
|
|
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({
|
|
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={
|
|
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 {
|
|
2100
|
-
|
|
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
|
-
|
|
2130
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
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
|
-
|
|
2245
|
-
|
|
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
|
+
}
|