react-native-livechart 3.0.0 → 3.2.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/useCanvasLayout.d.ts +3 -2
- package/dist/hooks/useCanvasLayout.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 +102 -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 +79 -13
- 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/useCanvasLayout.ts +5 -3
- 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 +106 -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,
|
|
@@ -289,7 +307,7 @@ function useLiveChartController({
|
|
|
289
307
|
);
|
|
290
308
|
|
|
291
309
|
// ── Per-frame derived values ───────────────────────────────────────────
|
|
292
|
-
const { layoutHeight, onLayout } = useCanvasLayout(engine);
|
|
310
|
+
const { layoutWidth, layoutHeight, onLayout } = useCanvasLayout(engine);
|
|
293
311
|
|
|
294
312
|
const { linePath, fillPath } = useChartPaths(
|
|
295
313
|
engine,
|
|
@@ -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,8 +479,11 @@ function useLiveChartController({
|
|
|
453
479
|
gradientEnd,
|
|
454
480
|
gradientTopColor,
|
|
455
481
|
gradientBottomColor,
|
|
482
|
+
gradientColors,
|
|
483
|
+
gradientPositions,
|
|
456
484
|
lineGroupOpacity,
|
|
457
485
|
candleGroupOpacity,
|
|
486
|
+
layoutWidth,
|
|
458
487
|
onLayout,
|
|
459
488
|
linePath,
|
|
460
489
|
fillPath,
|
|
@@ -476,6 +505,12 @@ function useLiveChartController({
|
|
|
476
505
|
rootGesture,
|
|
477
506
|
markersActive,
|
|
478
507
|
markersSV,
|
|
508
|
+
// selection dot: resolved config + fallback color (the chart line/accent color)
|
|
509
|
+
selectionDot: selectionDotCfg,
|
|
510
|
+
selectionColor: lineProp?.color ?? palette.line,
|
|
511
|
+
// RN axis edge labels (floated over the canvas as a sibling layer)
|
|
512
|
+
topLabelCfg,
|
|
513
|
+
bottomLabelCfg,
|
|
479
514
|
};
|
|
480
515
|
}
|
|
481
516
|
|
|
@@ -500,8 +535,8 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
500
535
|
gradientCfg,
|
|
501
536
|
fillPath,
|
|
502
537
|
gradientEnd,
|
|
503
|
-
|
|
504
|
-
|
|
538
|
+
gradientColors,
|
|
539
|
+
gradientPositions,
|
|
505
540
|
valueLineCfg,
|
|
506
541
|
dotY,
|
|
507
542
|
allRefLines,
|
|
@@ -527,6 +562,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
527
562
|
markersSV,
|
|
528
563
|
emptyText,
|
|
529
564
|
metricsCfg,
|
|
565
|
+
layoutWidth,
|
|
530
566
|
} = model;
|
|
531
567
|
|
|
532
568
|
return (
|
|
@@ -555,7 +591,8 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
555
591
|
<LinearGradient
|
|
556
592
|
start={vec(0, effectivePadding.top)}
|
|
557
593
|
end={vec(0, gradientEnd)}
|
|
558
|
-
colors={
|
|
594
|
+
colors={gradientColors}
|
|
595
|
+
positions={gradientPositions}
|
|
559
596
|
/>
|
|
560
597
|
</Path>
|
|
561
598
|
</Group>
|
|
@@ -593,10 +630,18 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
593
630
|
path={linePath}
|
|
594
631
|
style="stroke"
|
|
595
632
|
strokeWidth={strokeWidth}
|
|
596
|
-
color={lineProp?.color ?? palette.line}
|
|
597
633
|
strokeCap="round"
|
|
598
634
|
strokeJoin="round"
|
|
599
|
-
|
|
635
|
+
color={lineProp?.color ?? palette.line}
|
|
636
|
+
>
|
|
637
|
+
{lineProp?.colors?.length ? (
|
|
638
|
+
<LinearGradient
|
|
639
|
+
start={vec(0, 0)}
|
|
640
|
+
end={vec(layoutWidth, 0)}
|
|
641
|
+
colors={lineProp.colors}
|
|
642
|
+
/>
|
|
643
|
+
) : null}
|
|
644
|
+
</Path>
|
|
600
645
|
</Group>
|
|
601
646
|
|
|
602
647
|
{/* Candle bodies/wicks (fades in in candle mode) */}
|
|
@@ -713,6 +758,8 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
|
|
|
713
758
|
isCandle,
|
|
714
759
|
pulseCfg,
|
|
715
760
|
dotOuterRadius,
|
|
761
|
+
selectionDot,
|
|
762
|
+
selectionColor,
|
|
716
763
|
} = model;
|
|
717
764
|
|
|
718
765
|
if (!tradeStreamResolved && !scrubCfg) return null;
|
|
@@ -748,6 +795,10 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
|
|
|
748
795
|
palette={palette}
|
|
749
796
|
font={skiaFont}
|
|
750
797
|
showTooltip={scrubCfg.tooltip}
|
|
798
|
+
selectionDot={selectionDot}
|
|
799
|
+
selectionY={crosshair.scrubDotY}
|
|
800
|
+
scrubActive={crosshair.scrubActive}
|
|
801
|
+
selectionColor={selectionColor}
|
|
751
802
|
dimOpacity={scrubCfg.dimOpacity}
|
|
752
803
|
liveDotExtent={liveDotExtent}
|
|
753
804
|
crosshairLineColor={scrubCfg.crosshairLineColor}
|
|
@@ -834,6 +885,10 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
834
885
|
leftEdgeFadeCfg,
|
|
835
886
|
effectivePadding,
|
|
836
887
|
engine,
|
|
888
|
+
palette,
|
|
889
|
+
formatValue,
|
|
890
|
+
topLabelCfg,
|
|
891
|
+
bottomLabelCfg,
|
|
837
892
|
} = model;
|
|
838
893
|
|
|
839
894
|
return (
|
|
@@ -867,6 +922,17 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
867
922
|
its left edge (the badge tracks the live value, not the scrub). */}
|
|
868
923
|
<ChartBadgeLayer model={model} />
|
|
869
924
|
</Canvas>
|
|
925
|
+
|
|
926
|
+
{/* RN labels floated over the canvas (sibling of <Canvas>, an RN view).
|
|
927
|
+
Pinned to the plot's top/bottom edges via the resolved padding. */}
|
|
928
|
+
<AxisLabelOverlay
|
|
929
|
+
topLabel={topLabelCfg}
|
|
930
|
+
bottomLabel={bottomLabelCfg}
|
|
931
|
+
engine={engine}
|
|
932
|
+
formatValue={formatValue}
|
|
933
|
+
defaultColor={palette.gridLabel}
|
|
934
|
+
padding={effectivePadding}
|
|
935
|
+
/>
|
|
870
936
|
</View>
|
|
871
937
|
</GestureDetector>
|
|
872
938
|
);
|
|
@@ -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}
|