react-native-livechart 3.0.0 → 3.1.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/README.md +9 -3
- package/dist/components/AxisLabelOverlay.d.ts +29 -0
- package/dist/components/AxisLabelOverlay.d.ts.map +1 -0
- package/dist/components/CrosshairLine.d.ts +12 -1
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +15 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts +17 -0
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/SelectionDot.d.ts +32 -0
- package/dist/components/SelectionDot.d.ts.map +1 -0
- package/dist/core/resolveConfig.d.ts +46 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +6 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/draw/particleAtlas.d.ts +39 -0
- package/dist/draw/particleAtlas.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +10 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +3 -1
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartColors.d.ts +5 -0
- package/dist/hooks/useChartColors.d.ts.map +1 -1
- package/dist/hooks/useChartReveal.d.ts +3 -1
- package/dist/hooks/useChartReveal.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +1 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts +3 -1
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +3 -1
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useTradeStream.d.ts +3 -1
- package/dist/hooks/useTradeStream.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AxisLabelOverlay.tsx +133 -0
- package/src/components/CrosshairLine.tsx +27 -0
- package/src/components/CrosshairOverlay.tsx +50 -18
- package/src/components/DegenParticlesOverlay.tsx +93 -109
- package/src/components/LiveChart.tsx +66 -10
- package/src/components/LiveChartSeries.tsx +40 -0
- package/src/components/SelectionDot.tsx +92 -0
- package/src/core/resolveConfig.ts +98 -0
- package/src/core/useLiveChartEngine.ts +78 -27
- package/src/draw/particleAtlas.ts +95 -0
- package/src/hooks/crosshairShared.ts +26 -0
- package/src/hooks/useCandlePaths.ts +3 -1
- package/src/hooks/useChartColors.ts +19 -0
- package/src/hooks/useChartReveal.ts +7 -4
- package/src/hooks/useCrosshair.ts +46 -7
- package/src/hooks/useCrosshairSeries.ts +47 -8
- package/src/hooks/useDegen.ts +6 -0
- package/src/hooks/useMarkers.ts +3 -0
- package/src/hooks/useTradeStream.ts +3 -0
- package/src/index.ts +4 -0
- package/src/types.ts +101 -0
|
@@ -1,99 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Atlas,
|
|
3
|
+
Skia,
|
|
4
|
+
type SkColor,
|
|
5
|
+
type SkRSXform,
|
|
6
|
+
type SkRect,
|
|
7
|
+
} from "@shopify/react-native-skia";
|
|
2
8
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
3
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
buildParticleInstances,
|
|
11
|
+
buildParticleSprite,
|
|
12
|
+
} from "../draw/particleAtlas";
|
|
13
|
+
import { parseColorRgb } from "../theme";
|
|
4
14
|
import type { LiveChartPalette } from "../types";
|
|
5
15
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
6
16
|
|
|
7
17
|
type DegenPack = SharedValue<Float64Array<ArrayBuffer>>;
|
|
8
18
|
|
|
9
19
|
/**
|
|
10
|
-
* Renders
|
|
11
|
-
* Buffer layout per slot (stride = DEGEN_STRIDE = 8):
|
|
12
|
-
* [0] x, [1] y, [2] vx, [3] vy, [4] t0, [5] active, [6] size, [7] colorIndex
|
|
13
|
-
* See `math/degenTick.ts` for the authoritative layout documentation.
|
|
20
|
+
* Renders the degen particle burst with a single `drawAtlas` call.
|
|
14
21
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
22
|
+
* Every active particle shares one pre-rasterized white-circle sprite and
|
|
23
|
+
* differs only in position, scale, color, and opacity — the ideal `drawAtlas`
|
|
24
|
+
* case. A single per-frame worklet projects the packed ring buffer (layout:
|
|
25
|
+
* `[x, y, vx, vy, t0, active, size, colorIndex]`, stride = DEGEN_STRIDE = 8;
|
|
26
|
+
* see `math/degenTick.ts`) into transforms + per-sprite colors, replacing the
|
|
27
|
+
* old O(slots × 4 derived values + 1 `<Circle>` each) fan-out with O(1) mappers
|
|
28
|
+
* and one draw.
|
|
29
|
+
*
|
|
30
|
+
* Per-particle color/opacity is baked into the sprite color (`rgba(r,g,b,a)`)
|
|
31
|
+
* and applied with the `"modulate"` blend mode, which multiplies the white
|
|
32
|
+
* sprite by each color — so a white AA circle becomes a soft, correctly-tinted,
|
|
33
|
+
* correctly-faded particle, visually equivalent to the old `<Circle color
|
|
34
|
+
* opacity>`.
|
|
17
35
|
*/
|
|
18
|
-
function DegenSlot({
|
|
19
|
-
index,
|
|
20
|
-
pack,
|
|
21
|
-
packRevision,
|
|
22
|
-
engine,
|
|
23
|
-
particleBurstDurationSec,
|
|
24
|
-
particleOpacity,
|
|
25
|
-
colorList,
|
|
26
|
-
}: {
|
|
27
|
-
index: number;
|
|
28
|
-
pack: DegenPack;
|
|
29
|
-
packRevision: SharedValue<number>;
|
|
30
|
-
engine: ChartEngineLayout;
|
|
31
|
-
particleBurstDurationSec: number;
|
|
32
|
-
particleOpacity: number;
|
|
33
|
-
colorList: string[];
|
|
34
|
-
}) {
|
|
35
|
-
const base = index * DEGEN_STRIDE;
|
|
36
|
-
|
|
37
|
-
/* istanbul ignore next -- useDerivedValue worklet runs on UI thread, not in Jest */
|
|
38
|
-
const cx = useDerivedValue(() => {
|
|
39
|
-
const rev = packRevision.value;
|
|
40
|
-
const buf = pack.value;
|
|
41
|
-
const active = buf[base + 5];
|
|
42
|
-
if (!(rev >= 0) || !(active > 0.5)) return -9999;
|
|
43
|
-
return buf[base + 0];
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
/* istanbul ignore next -- worklet */
|
|
47
|
-
const cy = useDerivedValue(() => {
|
|
48
|
-
const rev = packRevision.value;
|
|
49
|
-
const buf = pack.value;
|
|
50
|
-
const active = buf[base + 5];
|
|
51
|
-
if (!(rev >= 0) || !(active > 0.5)) return -9999;
|
|
52
|
-
return buf[base + 1];
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
/* istanbul ignore next -- worklet */
|
|
56
|
-
const r = useDerivedValue(() => {
|
|
57
|
-
const rev = packRevision.value;
|
|
58
|
-
const buf = pack.value;
|
|
59
|
-
const active = buf[base + 5];
|
|
60
|
-
if (!(rev >= 0) || !(active > 0.5)) return 0;
|
|
61
|
-
const now = engine.timestamp.value;
|
|
62
|
-
const dt = now - buf[base + 4];
|
|
63
|
-
if (dt < 0) return 0;
|
|
64
|
-
const life = Math.max(0, 1 - dt / particleBurstDurationSec);
|
|
65
|
-
const size = buf[base + 6] || 1;
|
|
66
|
-
return size * (0.5 + life * 0.5);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
/* istanbul ignore next -- worklet */
|
|
70
|
-
const opacity = useDerivedValue(() => {
|
|
71
|
-
const rev = packRevision.value;
|
|
72
|
-
const buf = pack.value;
|
|
73
|
-
const active = buf[base + 5];
|
|
74
|
-
if (!(rev >= 0) || !(active > 0.5)) return 0;
|
|
75
|
-
const now = engine.timestamp.value;
|
|
76
|
-
const dt = now - buf[base + 4];
|
|
77
|
-
if (dt < 0) return 0;
|
|
78
|
-
const life = Math.max(0, 1 - dt / particleBurstDurationSec);
|
|
79
|
-
return life * particleOpacity;
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
/* istanbul ignore next -- worklet */
|
|
83
|
-
const color = useDerivedValue(() => {
|
|
84
|
-
// Read packRevision so this re-runs each frame — the pack buffer is mutated
|
|
85
|
-
// in place (stable reference), so without this the color would freeze at the
|
|
86
|
-
// mount-time colorIndex (0) for every particle.
|
|
87
|
-
const rev = packRevision.value;
|
|
88
|
-
const buf = pack.value;
|
|
89
|
-
if (!(rev >= 0)) return colorList[0];
|
|
90
|
-
const idx = Math.max(0, Math.floor(buf[base + 7]));
|
|
91
|
-
return colorList[idx % colorList.length];
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return <Circle cx={cx} cy={cy} r={r} color={color} opacity={opacity} />;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
36
|
export function DegenParticlesOverlay({
|
|
98
37
|
pack,
|
|
99
38
|
packRevision,
|
|
@@ -115,27 +54,72 @@ export function DegenParticlesOverlay({
|
|
|
115
54
|
}) {
|
|
116
55
|
/* istanbul ignore next -- branch depends on render-time props */
|
|
117
56
|
const colorList = colors && colors.length > 0 ? colors : [palette.line];
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
57
|
+
|
|
58
|
+
// White-circle sprite has no per-render inputs; React Compiler memoizes it.
|
|
59
|
+
const sprite = buildParticleSprite();
|
|
60
|
+
|
|
61
|
+
// Pre-parse the color list to [r,g,b] tuples so the per-frame worklet never
|
|
62
|
+
// parses arbitrary color strings (only formats `rgba(...)`). React Compiler
|
|
63
|
+
// memoizes this on `colorList`.
|
|
64
|
+
const colorRgb = colorList.map((c) => parseColorRgb(c));
|
|
65
|
+
|
|
66
|
+
// Single per-frame worklet. Reads `packRevision` so it re-runs each frame
|
|
67
|
+
// while a burst is alive (the pack buffer is mutated in place, so its
|
|
68
|
+
// reference is stable and wouldn't otherwise re-notify).
|
|
69
|
+
const atlasData = useDerivedValue(() => {
|
|
70
|
+
const rev = packRevision.get();
|
|
71
|
+
const transforms: SkRSXform[] = [];
|
|
72
|
+
const sprites: SkRect[] = [];
|
|
73
|
+
const colorsOut: SkColor[] = [];
|
|
74
|
+
if (rev >= 0) {
|
|
75
|
+
const instances = buildParticleInstances(
|
|
76
|
+
pack.get(),
|
|
77
|
+
particleSlotCount,
|
|
78
|
+
engine.timestamp.get(),
|
|
79
|
+
particleBurstDurationSec,
|
|
80
|
+
particleOpacity,
|
|
81
|
+
sprite.radius,
|
|
82
|
+
);
|
|
83
|
+
const half = sprite.size / 2;
|
|
84
|
+
for (let i = 0; i < instances.length; i++) {
|
|
85
|
+
const inst = instances[i];
|
|
86
|
+
// Center the scaled sprite on (x, y); RSXform has no rotation.
|
|
87
|
+
transforms.push(
|
|
88
|
+
Skia.RSXform(
|
|
89
|
+
inst.scale,
|
|
90
|
+
0,
|
|
91
|
+
inst.x - inst.scale * half,
|
|
92
|
+
inst.y - inst.scale * half,
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
sprites.push(Skia.XYWHRect(0, 0, sprite.size, sprite.size));
|
|
96
|
+
const [r, g, b] = colorRgb[inst.colorIndex % colorRgb.length];
|
|
97
|
+
colorsOut.push(Skia.Color(`rgba(${r}, ${g}, ${b}, ${inst.alpha})`));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { transforms, sprites, colors: colorsOut };
|
|
101
|
+
}, [
|
|
102
|
+
pack,
|
|
103
|
+
packRevision,
|
|
104
|
+
engine,
|
|
105
|
+
particleSlotCount,
|
|
106
|
+
particleBurstDurationSec,
|
|
107
|
+
particleOpacity,
|
|
108
|
+
sprite,
|
|
109
|
+
colorRgb,
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const transforms = useDerivedValue(() => atlasData.get().transforms, [atlasData]);
|
|
113
|
+
const sprites = useDerivedValue(() => atlasData.get().sprites, [atlasData]);
|
|
114
|
+
const atlasColors = useDerivedValue(() => atlasData.get().colors, [atlasData]);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Atlas
|
|
118
|
+
image={sprite.image}
|
|
119
|
+
sprites={sprites}
|
|
120
|
+
transforms={transforms}
|
|
121
|
+
colors={atlasColors}
|
|
122
|
+
colorBlendMode="modulate"
|
|
138
123
|
/>
|
|
139
|
-
)
|
|
140
|
-
return <Group>{slots}</Group>;
|
|
124
|
+
);
|
|
141
125
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
|
|
20
20
|
import { DEFAULT_ACCENT_COLOR } from "../constants";
|
|
21
21
|
import {
|
|
22
|
+
resolveAxisLabel,
|
|
22
23
|
resolveBadge,
|
|
23
24
|
resolveDegen,
|
|
24
25
|
resolveDot,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
resolveMetrics,
|
|
29
30
|
resolvePulse,
|
|
30
31
|
resolveScrub,
|
|
32
|
+
resolveSelectionDot,
|
|
31
33
|
resolveTradeStream,
|
|
32
34
|
resolveValueLine,
|
|
33
35
|
resolveXAxis,
|
|
@@ -66,6 +68,7 @@ import {
|
|
|
66
68
|
resolveTheme,
|
|
67
69
|
} from "../theme";
|
|
68
70
|
import type { LiveChartProps, Marker, TradeEvent } from "../types";
|
|
71
|
+
import { AxisLabelOverlay } from "./AxisLabelOverlay";
|
|
69
72
|
import { BadgeOverlay } from "./BadgeOverlay";
|
|
70
73
|
import { CrosshairOverlay } from "./CrosshairOverlay";
|
|
71
74
|
import { DegenParticlesOverlay } from "./DegenParticlesOverlay";
|
|
@@ -111,6 +114,8 @@ function useLiveChartController({
|
|
|
111
114
|
timeWindow = 30,
|
|
112
115
|
paused = false,
|
|
113
116
|
loading = false,
|
|
117
|
+
// `static` is a reserved word — alias it so the destructure parses.
|
|
118
|
+
static: isStatic = false,
|
|
114
119
|
smoothing = 0.08,
|
|
115
120
|
exaggerate = false,
|
|
116
121
|
nonNegative = false,
|
|
@@ -126,6 +131,8 @@ function useLiveChartController({
|
|
|
126
131
|
// ── Overlays ────────────────────────────────────────────────────────────
|
|
127
132
|
yAxis = true,
|
|
128
133
|
xAxis = true,
|
|
134
|
+
topLabel,
|
|
135
|
+
bottomLabel,
|
|
129
136
|
badge = true,
|
|
130
137
|
momentum = true,
|
|
131
138
|
pulse = true,
|
|
@@ -138,6 +145,7 @@ function useLiveChartController({
|
|
|
138
145
|
palette: paletteOverride,
|
|
139
146
|
metrics,
|
|
140
147
|
scrub = true,
|
|
148
|
+
selectionDot,
|
|
141
149
|
tradeStream,
|
|
142
150
|
degen,
|
|
143
151
|
markers,
|
|
@@ -147,6 +155,8 @@ function useLiveChartController({
|
|
|
147
155
|
|
|
148
156
|
// ── Callbacks ───────────────────────────────────────────────────────────
|
|
149
157
|
onScrub,
|
|
158
|
+
onGestureStart,
|
|
159
|
+
onGestureEnd,
|
|
150
160
|
onDegenShake,
|
|
151
161
|
}: LiveChartProps) {
|
|
152
162
|
const emptyTradeStream = useSharedValue<TradeEvent[]>([]);
|
|
@@ -158,16 +168,23 @@ function useLiveChartController({
|
|
|
158
168
|
// ── Resolve feature configs ────────────────────────────────────────────
|
|
159
169
|
const yAxisCfg = resolveYAxis(yAxis);
|
|
160
170
|
const xAxisCfg = resolveXAxis(xAxis);
|
|
171
|
+
const topLabelCfg = resolveAxisLabel(topLabel);
|
|
172
|
+
const bottomLabelCfg = resolveAxisLabel(bottomLabel);
|
|
161
173
|
const badgeCfg = resolveBadge(badge);
|
|
162
174
|
const scrubCfg = resolveScrub(scrub);
|
|
163
175
|
const gradientCfg = isCandle ? null : resolveGradient(gradient);
|
|
164
176
|
const valueLineCfg = resolveValueLine(valueLine);
|
|
165
|
-
|
|
177
|
+
// Static charts run zero loops: force the pulse off so its `withRepeat`-driven
|
|
178
|
+
// ring never starts (the DotOverlay reads `pulseCfg`, so null = no pulse).
|
|
179
|
+
const pulseCfg = isStatic ? null : resolvePulse(pulse);
|
|
166
180
|
const dotCfg = resolveDot(dot);
|
|
181
|
+
const selectionDotCfg = resolveSelectionDot(selectionDot);
|
|
167
182
|
// Outer footprint of the dot (color-filled radius plus the halo ring).
|
|
168
183
|
const dotOuterRadius = dotCfg.radius + (dotCfg.ring?.width ?? 0);
|
|
169
184
|
const gridStyleCfg = resolveGridStyle(gridStyle);
|
|
170
|
-
|
|
185
|
+
// Static charts run zero loops: force degen off so `useDegen`'s frame callback
|
|
186
|
+
// never starts (also passed `isStatic` below as a belt-and-braces autostart gate).
|
|
187
|
+
const degenCfg = isStatic ? null : resolveDegen(degen);
|
|
171
188
|
const tradeStreamResolved = resolveTradeStream(tradeStream);
|
|
172
189
|
const metricsCfg = resolveMetrics(metrics);
|
|
173
190
|
|
|
@@ -248,7 +265,7 @@ function useLiveChartController({
|
|
|
248
265
|
candles,
|
|
249
266
|
});
|
|
250
267
|
|
|
251
|
-
const reveal = useChartReveal(loading, hasData);
|
|
268
|
+
const reveal = useChartReveal(loading, hasData, isStatic);
|
|
252
269
|
|
|
253
270
|
// After data clears, keep last snapshot until morphT finishes dropping (web parity).
|
|
254
271
|
const { lineEngineData, candlesEngine, liveEngine } =
|
|
@@ -269,6 +286,7 @@ function useLiveChartController({
|
|
|
269
286
|
value,
|
|
270
287
|
timeWindow,
|
|
271
288
|
paused,
|
|
289
|
+
static: isStatic,
|
|
272
290
|
smoothing,
|
|
273
291
|
adaptiveSpeedBoost: metricsCfg.motion.adaptiveSpeedBoost,
|
|
274
292
|
exaggerate,
|
|
@@ -306,6 +324,7 @@ function useLiveChartController({
|
|
|
306
324
|
candleWidth,
|
|
307
325
|
isCandle,
|
|
308
326
|
metricsCfg.candle,
|
|
327
|
+
!isStatic, // static: no candle-width lerp loop
|
|
309
328
|
);
|
|
310
329
|
const { dotX, dotY } = useLiveDot(engine, effectivePadding);
|
|
311
330
|
|
|
@@ -315,14 +334,15 @@ function useLiveChartController({
|
|
|
315
334
|
engine,
|
|
316
335
|
tradeStreamSV,
|
|
317
336
|
effectivePadding,
|
|
318
|
-
tradeStreamResolved !== null,
|
|
337
|
+
!isStatic && tradeStreamResolved !== null,
|
|
338
|
+
!isStatic, // static: no trade-tape loop
|
|
319
339
|
);
|
|
320
340
|
|
|
321
341
|
const {
|
|
322
342
|
pack: degenPack,
|
|
323
343
|
packRevision: degenPackRevision,
|
|
324
344
|
shakeTransform: degenShakeTransform,
|
|
325
|
-
} = useDegen(engine, dotX, dotY, momentumSV, degenCfg, onDegenShake);
|
|
345
|
+
} = useDegen(engine, dotX, dotY, momentumSV, degenCfg, onDegenShake, isStatic);
|
|
326
346
|
|
|
327
347
|
// ── Overlay hooks ─────────────────────────────────────────────────────
|
|
328
348
|
const { yAxisEntries } = useYAxis(
|
|
@@ -373,10 +393,13 @@ function useLiveChartController({
|
|
|
373
393
|
formatValue,
|
|
374
394
|
formatTime,
|
|
375
395
|
skiaFont,
|
|
376
|
-
|
|
396
|
+
// Static charts have an inert pan gesture — no scrub work on the UI thread.
|
|
397
|
+
!isStatic && scrubCfg !== null,
|
|
377
398
|
onScrub,
|
|
378
399
|
candleOpts,
|
|
379
400
|
scrubCfg?.panGestureDelay ?? 0,
|
|
401
|
+
onGestureStart,
|
|
402
|
+
onGestureEnd,
|
|
380
403
|
);
|
|
381
404
|
|
|
382
405
|
const markersActive = markers != null;
|
|
@@ -386,11 +409,12 @@ function useLiveChartController({
|
|
|
386
409
|
engine,
|
|
387
410
|
effectivePadding,
|
|
388
411
|
markersSV,
|
|
389
|
-
markersActive,
|
|
412
|
+
!isStatic && markersActive,
|
|
390
413
|
markerHitRadius,
|
|
391
414
|
onMarkerHover,
|
|
392
415
|
undefined, // seriesSV — single-series has none
|
|
393
416
|
engine.data, // anchor value-less markers to the line
|
|
417
|
+
!isStatic, // static: no marker-projection loop
|
|
394
418
|
);
|
|
395
419
|
|
|
396
420
|
const rootGesture = markersActive
|
|
@@ -403,6 +427,8 @@ function useLiveChartController({
|
|
|
403
427
|
gradientEnd,
|
|
404
428
|
gradientTopColor,
|
|
405
429
|
gradientBottomColor,
|
|
430
|
+
gradientColors,
|
|
431
|
+
gradientPositions,
|
|
406
432
|
} = useChartColors(
|
|
407
433
|
palette,
|
|
408
434
|
gradientCfg,
|
|
@@ -453,6 +479,8 @@ function useLiveChartController({
|
|
|
453
479
|
gradientEnd,
|
|
454
480
|
gradientTopColor,
|
|
455
481
|
gradientBottomColor,
|
|
482
|
+
gradientColors,
|
|
483
|
+
gradientPositions,
|
|
456
484
|
lineGroupOpacity,
|
|
457
485
|
candleGroupOpacity,
|
|
458
486
|
onLayout,
|
|
@@ -476,6 +504,12 @@ function useLiveChartController({
|
|
|
476
504
|
rootGesture,
|
|
477
505
|
markersActive,
|
|
478
506
|
markersSV,
|
|
507
|
+
// selection dot: resolved config + fallback color (the chart line/accent color)
|
|
508
|
+
selectionDot: selectionDotCfg,
|
|
509
|
+
selectionColor: lineProp?.color ?? palette.line,
|
|
510
|
+
// RN axis edge labels (floated over the canvas as a sibling layer)
|
|
511
|
+
topLabelCfg,
|
|
512
|
+
bottomLabelCfg,
|
|
479
513
|
};
|
|
480
514
|
}
|
|
481
515
|
|
|
@@ -500,8 +534,8 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
500
534
|
gradientCfg,
|
|
501
535
|
fillPath,
|
|
502
536
|
gradientEnd,
|
|
503
|
-
|
|
504
|
-
|
|
537
|
+
gradientColors,
|
|
538
|
+
gradientPositions,
|
|
505
539
|
valueLineCfg,
|
|
506
540
|
dotY,
|
|
507
541
|
allRefLines,
|
|
@@ -555,7 +589,8 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
555
589
|
<LinearGradient
|
|
556
590
|
start={vec(0, effectivePadding.top)}
|
|
557
591
|
end={vec(0, gradientEnd)}
|
|
558
|
-
colors={
|
|
592
|
+
colors={gradientColors}
|
|
593
|
+
positions={gradientPositions}
|
|
559
594
|
/>
|
|
560
595
|
</Path>
|
|
561
596
|
</Group>
|
|
@@ -713,6 +748,8 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
|
|
|
713
748
|
isCandle,
|
|
714
749
|
pulseCfg,
|
|
715
750
|
dotOuterRadius,
|
|
751
|
+
selectionDot,
|
|
752
|
+
selectionColor,
|
|
716
753
|
} = model;
|
|
717
754
|
|
|
718
755
|
if (!tradeStreamResolved && !scrubCfg) return null;
|
|
@@ -748,6 +785,10 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
|
|
|
748
785
|
palette={palette}
|
|
749
786
|
font={skiaFont}
|
|
750
787
|
showTooltip={scrubCfg.tooltip}
|
|
788
|
+
selectionDot={selectionDot}
|
|
789
|
+
selectionY={crosshair.scrubDotY}
|
|
790
|
+
scrubActive={crosshair.scrubActive}
|
|
791
|
+
selectionColor={selectionColor}
|
|
751
792
|
dimOpacity={scrubCfg.dimOpacity}
|
|
752
793
|
liveDotExtent={liveDotExtent}
|
|
753
794
|
crosshairLineColor={scrubCfg.crosshairLineColor}
|
|
@@ -834,6 +875,10 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
834
875
|
leftEdgeFadeCfg,
|
|
835
876
|
effectivePadding,
|
|
836
877
|
engine,
|
|
878
|
+
palette,
|
|
879
|
+
formatValue,
|
|
880
|
+
topLabelCfg,
|
|
881
|
+
bottomLabelCfg,
|
|
837
882
|
} = model;
|
|
838
883
|
|
|
839
884
|
return (
|
|
@@ -867,6 +912,17 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
867
912
|
its left edge (the badge tracks the live value, not the scrub). */}
|
|
868
913
|
<ChartBadgeLayer model={model} />
|
|
869
914
|
</Canvas>
|
|
915
|
+
|
|
916
|
+
{/* RN labels floated over the canvas (sibling of <Canvas>, an RN view).
|
|
917
|
+
Pinned to the plot's top/bottom edges via the resolved padding. */}
|
|
918
|
+
<AxisLabelOverlay
|
|
919
|
+
topLabel={topLabelCfg}
|
|
920
|
+
bottomLabel={bottomLabelCfg}
|
|
921
|
+
engine={engine}
|
|
922
|
+
formatValue={formatValue}
|
|
923
|
+
defaultColor={palette.gridLabel}
|
|
924
|
+
padding={effectivePadding}
|
|
925
|
+
/>
|
|
870
926
|
</View>
|
|
871
927
|
</GestureDetector>
|
|
872
928
|
);
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type SeriesLineStyle,
|
|
25
25
|
} from "../core/multiSeriesLayout";
|
|
26
26
|
import {
|
|
27
|
+
resolveAxisLabel,
|
|
27
28
|
resolveDegen,
|
|
28
29
|
resolveGridStyle,
|
|
29
30
|
resolveLeftEdgeFade,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
resolveMetrics,
|
|
32
33
|
resolveMultiSeriesDot,
|
|
33
34
|
resolveScrub,
|
|
35
|
+
resolveSelectionDot,
|
|
34
36
|
resolveXAxis,
|
|
35
37
|
resolveYAxis,
|
|
36
38
|
} from "../core/resolveConfig";
|
|
@@ -60,6 +62,7 @@ import {
|
|
|
60
62
|
resolveTheme,
|
|
61
63
|
} from "../theme";
|
|
62
64
|
import type { LiveChartSeriesProps, Marker, SeriesConfig } from "../types";
|
|
65
|
+
import { AxisLabelOverlay } from "./AxisLabelOverlay";
|
|
63
66
|
import { CrosshairLine } from "./CrosshairLine";
|
|
64
67
|
import { DegenParticlesOverlay } from "./DegenParticlesOverlay";
|
|
65
68
|
import { LeftEdgeFade } from "./LeftEdgeFade";
|
|
@@ -121,12 +124,17 @@ function useLiveChartSeriesController({
|
|
|
121
124
|
formatTime = defaultFormatTime,
|
|
122
125
|
yAxis = true,
|
|
123
126
|
xAxis = true,
|
|
127
|
+
topLabel,
|
|
128
|
+
bottomLabel,
|
|
124
129
|
referenceLines,
|
|
125
130
|
gridStyle,
|
|
126
131
|
palette: paletteOverride,
|
|
127
132
|
metrics,
|
|
128
133
|
scrub = true,
|
|
134
|
+
selectionDot,
|
|
129
135
|
onScrub,
|
|
136
|
+
onGestureStart,
|
|
137
|
+
onGestureEnd,
|
|
130
138
|
onSeriesToggle,
|
|
131
139
|
dot: dotProp,
|
|
132
140
|
legend: legendProp,
|
|
@@ -142,8 +150,11 @@ function useLiveChartSeriesController({
|
|
|
142
150
|
const markersActive = markers != null;
|
|
143
151
|
const yAxisCfg = resolveYAxis(yAxis);
|
|
144
152
|
const xAxisCfg = resolveXAxis(xAxis);
|
|
153
|
+
const topLabelCfg = resolveAxisLabel(topLabel);
|
|
154
|
+
const bottomLabelCfg = resolveAxisLabel(bottomLabel);
|
|
145
155
|
const scrubCfg = resolveScrub(scrub);
|
|
146
156
|
const scrubEnabled = scrubCfg !== null;
|
|
157
|
+
const selectionDotCfg = resolveSelectionDot(selectionDot);
|
|
147
158
|
const gridStyleCfg = resolveGridStyle(gridStyle);
|
|
148
159
|
const dotCfg = resolveMultiSeriesDot(dotProp);
|
|
149
160
|
// Outer footprint of a dot (the color-filled radius plus the halo ring).
|
|
@@ -307,6 +318,8 @@ function useLiveChartSeriesController({
|
|
|
307
318
|
scrubEnabled,
|
|
308
319
|
onScrub,
|
|
309
320
|
scrubCfg?.panGestureDelay ?? 0,
|
|
321
|
+
onGestureStart,
|
|
322
|
+
onGestureEnd,
|
|
310
323
|
);
|
|
311
324
|
|
|
312
325
|
// `projected` is used internally by the hit-test gesture; the overlay
|
|
@@ -373,6 +386,12 @@ function useLiveChartSeriesController({
|
|
|
373
386
|
rootGesture,
|
|
374
387
|
markersActive,
|
|
375
388
|
markersSV,
|
|
389
|
+
// selection dot: resolved config + fallback color (the leading series' color)
|
|
390
|
+
selectionDot: selectionDotCfg,
|
|
391
|
+
selectionColor: lineColors[0],
|
|
392
|
+
// RN axis edge labels (floated over the canvas as a sibling layer)
|
|
393
|
+
topLabelCfg,
|
|
394
|
+
bottomLabelCfg,
|
|
376
395
|
};
|
|
377
396
|
}
|
|
378
397
|
|
|
@@ -591,6 +610,11 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
591
610
|
palette,
|
|
592
611
|
dotCfg,
|
|
593
612
|
dotOuterRadius,
|
|
613
|
+
selectionDot,
|
|
614
|
+
selectionColor,
|
|
615
|
+
formatValue,
|
|
616
|
+
topLabelCfg,
|
|
617
|
+
bottomLabelCfg,
|
|
594
618
|
} = model;
|
|
595
619
|
|
|
596
620
|
// Extend the scrub dim past the plot's right edge to fully cover the series
|
|
@@ -647,6 +671,10 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
647
671
|
engine={engine}
|
|
648
672
|
padding={effectivePadding}
|
|
649
673
|
palette={palette}
|
|
674
|
+
selectionDot={selectionDot}
|
|
675
|
+
selectionY={crosshair.scrubDotY}
|
|
676
|
+
scrubActive={crosshair.scrubActive}
|
|
677
|
+
selectionColor={selectionColor}
|
|
650
678
|
dimOpacity={scrubCfg.dimOpacity}
|
|
651
679
|
liveDotExtent={liveDotExtent}
|
|
652
680
|
crosshairLineColor={scrubCfg.crosshairLineColor}
|
|
@@ -658,6 +686,18 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
658
686
|
clips them (they track each series' live value, not the scrub). */}
|
|
659
687
|
<SeriesValueLabelLayer model={model} />
|
|
660
688
|
</Canvas>
|
|
689
|
+
|
|
690
|
+
{/* RN labels floated over the canvas (sibling of <Canvas>, an RN
|
|
691
|
+
view). Inside the canvas wrapper so its top/bottom edges align
|
|
692
|
+
with the plot area, not the legend row. */}
|
|
693
|
+
<AxisLabelOverlay
|
|
694
|
+
topLabel={topLabelCfg}
|
|
695
|
+
bottomLabel={bottomLabelCfg}
|
|
696
|
+
engine={engine}
|
|
697
|
+
formatValue={formatValue}
|
|
698
|
+
defaultColor={palette.gridLabel}
|
|
699
|
+
padding={effectivePadding}
|
|
700
|
+
/>
|
|
661
701
|
</View>
|
|
662
702
|
</GestureDetector>
|
|
663
703
|
{legendCfg.position === "bottom" ? legend : null}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Circle, Group } from "@shopify/react-native-skia";
|
|
2
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
3
|
+
import type { ResolvedSelectionDotConfig } from "../core/resolveConfig";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Built-in selection dot: a filled circle at the scrub intersection plus an
|
|
7
|
+
* optional subtle outer ring (the `ring` config). Drawn only when `y >= 0` (the
|
|
8
|
+
* sentinel for "no dot") — an off-screen Y hides it naturally.
|
|
9
|
+
*/
|
|
10
|
+
export function DefaultSelectionDot({
|
|
11
|
+
x,
|
|
12
|
+
y,
|
|
13
|
+
opacity,
|
|
14
|
+
color,
|
|
15
|
+
size,
|
|
16
|
+
ring,
|
|
17
|
+
}: {
|
|
18
|
+
x: SharedValue<number>;
|
|
19
|
+
y: SharedValue<number>;
|
|
20
|
+
opacity: SharedValue<number>;
|
|
21
|
+
color: string;
|
|
22
|
+
size: number;
|
|
23
|
+
ring: ResolvedSelectionDotConfig["ring"];
|
|
24
|
+
}) {
|
|
25
|
+
// Push the dot off-screen when there's no value to mark (y < 0), so the
|
|
26
|
+
// sentinel never paints a stray dot at the top of the plot.
|
|
27
|
+
const cy = useDerivedValue(() => (y.get() < 0 ? -size * 4 : y.get()));
|
|
28
|
+
return (
|
|
29
|
+
<Group opacity={opacity}>
|
|
30
|
+
{ring && (
|
|
31
|
+
<Circle
|
|
32
|
+
cx={x}
|
|
33
|
+
cy={cy}
|
|
34
|
+
r={size + ring.width}
|
|
35
|
+
color={ring.color ?? color}
|
|
36
|
+
opacity={0.25}
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
<Circle cx={x} cy={cy} r={size} color={color} />
|
|
40
|
+
</Group>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the selection-dot slot inside a crosshair overlay: renders nothing
|
|
46
|
+
* when `config` is null or the position is unavailable, the consumer's custom
|
|
47
|
+
* `component` when one is given, and the built-in dot otherwise.
|
|
48
|
+
*
|
|
49
|
+
* `color` is the resolved fallback (line / leading-series color) used when the
|
|
50
|
+
* config's own `color` is unset.
|
|
51
|
+
*/
|
|
52
|
+
export function SelectionDotSlot({
|
|
53
|
+
config,
|
|
54
|
+
x,
|
|
55
|
+
y,
|
|
56
|
+
active,
|
|
57
|
+
opacity,
|
|
58
|
+
color,
|
|
59
|
+
}: {
|
|
60
|
+
config?: ResolvedSelectionDotConfig | null;
|
|
61
|
+
x: SharedValue<number>;
|
|
62
|
+
y?: SharedValue<number>;
|
|
63
|
+
active?: SharedValue<number> | SharedValue<boolean>;
|
|
64
|
+
opacity: SharedValue<number>;
|
|
65
|
+
color: string;
|
|
66
|
+
}) {
|
|
67
|
+
if (!config || y === undefined || !active) return null;
|
|
68
|
+
const dotColor = config.color ?? color;
|
|
69
|
+
if (config.component) {
|
|
70
|
+
const Custom = config.component;
|
|
71
|
+
return (
|
|
72
|
+
<Custom
|
|
73
|
+
x={x}
|
|
74
|
+
y={y}
|
|
75
|
+
active={active as SharedValue<boolean>}
|
|
76
|
+
opacity={opacity}
|
|
77
|
+
color={dotColor}
|
|
78
|
+
size={config.size}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
<DefaultSelectionDot
|
|
84
|
+
x={x}
|
|
85
|
+
y={y}
|
|
86
|
+
opacity={opacity}
|
|
87
|
+
color={dotColor}
|
|
88
|
+
size={config.size}
|
|
89
|
+
ring={config.ring}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|