react-native-livechart 2.0.1 → 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/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesValueLines.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/SelectionDot.d.ts +32 -0
- package/dist/components/SelectionDot.d.ts.map +1 -0
- package/dist/components/ValueLineOverlay.d.ts.map +1 -1
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- 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/useBadge.d.ts +2 -2
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +11 -11
- 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/useChartPaths.d.ts +8 -7
- package/dist/hooks/useChartPaths.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/useMultiSeriesLinePaths.d.ts +9 -8
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/hooks/usePathBuilder.d.ts +27 -0
- package/dist/hooks/usePathBuilder.d.ts.map +1 -0
- 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/math/spline.d.ts +10 -2
- package/dist/math/spline.d.ts.map +1 -1
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- 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/LoadingOverlay.tsx +15 -29
- package/src/components/MarkerOverlay.tsx +23 -28
- package/src/components/MultiSeriesValueLines.tsx +20 -40
- package/src/components/ReferenceLineOverlay.tsx +65 -107
- package/src/components/SelectionDot.tsx +92 -0
- package/src/components/ValueLineOverlay.tsx +9 -28
- package/src/components/XAxisOverlay.tsx +9 -29
- package/src/components/YAxisOverlay.tsx +6 -22
- package/src/core/resolveConfig.ts +98 -0
- package/src/core/useLiveChartEngine.ts +78 -27
- package/src/draw/markerAtlas.ts +9 -9
- package/src/draw/particleAtlas.ts +95 -0
- package/src/hooks/crosshairShared.ts +26 -0
- package/src/hooks/useBadge.ts +17 -32
- package/src/hooks/useCandlePaths.ts +27 -64
- package/src/hooks/useChartColors.ts +19 -0
- package/src/hooks/useChartPaths.ts +27 -39
- 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/useMultiSeriesLinePaths.ts +26 -28
- package/src/hooks/usePathBuilder.ts +42 -0
- package/src/hooks/useTradeStream.ts +3 -0
- package/src/index.ts +4 -0
- package/src/math/spline.ts +22 -2
- package/src/types.ts +101 -0
|
@@ -2,7 +2,9 @@ import { Group, Line, Rect } from "@shopify/react-native-skia";
|
|
|
2
2
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
3
3
|
import { type ChartPadding } from "../draw/line";
|
|
4
4
|
import type { LiveChartPalette } from "../types";
|
|
5
|
+
import type { ResolvedSelectionDotConfig } from "../core/resolveConfig";
|
|
5
6
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
7
|
+
import { SelectionDotSlot } from "./SelectionDot";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Crosshair vertical line + dim region to the right. No tooltip pill —
|
|
@@ -14,6 +16,10 @@ export function CrosshairLine({
|
|
|
14
16
|
engine,
|
|
15
17
|
padding,
|
|
16
18
|
palette,
|
|
19
|
+
selectionDot,
|
|
20
|
+
selectionY,
|
|
21
|
+
scrubActive,
|
|
22
|
+
selectionColor,
|
|
17
23
|
dimOpacity = 0.3,
|
|
18
24
|
liveDotExtent = 0,
|
|
19
25
|
crosshairLineColor,
|
|
@@ -24,6 +30,16 @@ export function CrosshairLine({
|
|
|
24
30
|
engine: ChartEngineLayout;
|
|
25
31
|
padding: ChartPadding;
|
|
26
32
|
palette: LiveChartPalette;
|
|
33
|
+
/** Resolved selection-dot config; `null` hides it, a `component` renders the
|
|
34
|
+
* consumer's dot, otherwise the built-in dot. */
|
|
35
|
+
selectionDot?: ResolvedSelectionDotConfig | null;
|
|
36
|
+
/** Scrub intersection Y in canvas px (the value the dot marks); -1 hides it. */
|
|
37
|
+
selectionY?: SharedValue<number>;
|
|
38
|
+
/** Whether scrubbing is active (passed through to a custom dot). */
|
|
39
|
+
scrubActive?: SharedValue<number> | SharedValue<boolean>;
|
|
40
|
+
/** Fallback selection-dot color (leading-series color), used when the config's
|
|
41
|
+
* own `color` is unset. */
|
|
42
|
+
selectionColor?: string;
|
|
27
43
|
/** Opacity of content right of the crosshair (dstOut fade). Default 0.3. */
|
|
28
44
|
dimOpacity?: number;
|
|
29
45
|
/** How far the live series dots extend past the plot's right edge. The dim
|
|
@@ -104,6 +120,17 @@ export function CrosshairLine({
|
|
|
104
120
|
color={crosshairLineColor ?? palette.crosshairLine}
|
|
105
121
|
strokeWidth={1}
|
|
106
122
|
/>
|
|
123
|
+
|
|
124
|
+
{/* Selection dot at the scrub intersection (leading series). `null`
|
|
125
|
+
hides it; a custom component renders the consumer's dot. */}
|
|
126
|
+
<SelectionDotSlot
|
|
127
|
+
config={selectionDot}
|
|
128
|
+
x={scrubX}
|
|
129
|
+
y={selectionY}
|
|
130
|
+
active={scrubActive}
|
|
131
|
+
opacity={crosshairOpacity}
|
|
132
|
+
color={selectionColor ?? palette.line}
|
|
133
|
+
/>
|
|
107
134
|
</Group>
|
|
108
135
|
</>
|
|
109
136
|
);
|
|
@@ -11,7 +11,9 @@ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
|
11
11
|
import { type ChartPadding } from "../draw/line";
|
|
12
12
|
import { type TooltipLayout } from "../hooks/crosshairShared";
|
|
13
13
|
import type { LiveChartPalette } from "../types";
|
|
14
|
+
import type { ResolvedSelectionDotConfig } from "../core/resolveConfig";
|
|
14
15
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
16
|
+
import { SelectionDotSlot } from "./SelectionDot";
|
|
15
17
|
|
|
16
18
|
const TOOLTIP_RADIUS = 5;
|
|
17
19
|
|
|
@@ -25,6 +27,11 @@ export function CrosshairOverlay({
|
|
|
25
27
|
font,
|
|
26
28
|
showTooltip = true,
|
|
27
29
|
children,
|
|
30
|
+
renderTooltip,
|
|
31
|
+
selectionDot,
|
|
32
|
+
selectionY,
|
|
33
|
+
scrubActive,
|
|
34
|
+
selectionColor,
|
|
28
35
|
dimOpacity = 0.3,
|
|
29
36
|
liveDotExtent = 0,
|
|
30
37
|
crosshairLineColor,
|
|
@@ -45,6 +52,19 @@ export function CrosshairOverlay({
|
|
|
45
52
|
* text (e.g. the multi-line candle stack). Passed as children so it composes
|
|
46
53
|
* instead of being threaded through as a JSX-valued prop. */
|
|
47
54
|
children?: ReactNode;
|
|
55
|
+
/** Public custom-tooltip hook for line mode: when provided (and no `children`
|
|
56
|
+
* are passed), its result replaces the default value/time tooltip body. */
|
|
57
|
+
renderTooltip?: () => ReactNode;
|
|
58
|
+
/** Resolved selection-dot config; `null` hides it, a `component` renders the
|
|
59
|
+
* consumer's dot, otherwise the built-in dot. */
|
|
60
|
+
selectionDot?: ResolvedSelectionDotConfig | null;
|
|
61
|
+
/** Scrub intersection Y in canvas px (the value the dot marks); -1 hides it. */
|
|
62
|
+
selectionY?: SharedValue<number>;
|
|
63
|
+
/** Whether scrubbing is active (passed through to a custom dot). */
|
|
64
|
+
scrubActive?: SharedValue<number> | SharedValue<boolean>;
|
|
65
|
+
/** Fallback selection-dot color (accent / leading-series color), used when the
|
|
66
|
+
* config's own `color` is unset. */
|
|
67
|
+
selectionColor?: string;
|
|
48
68
|
/** Opacity of content right of the crosshair (dstOut fade). Default 0.3. */
|
|
49
69
|
dimOpacity?: number;
|
|
50
70
|
/** How far the live dot (and its pulse ring) extends past the plot's right
|
|
@@ -143,6 +163,17 @@ export function CrosshairOverlay({
|
|
|
143
163
|
strokeWidth={1}
|
|
144
164
|
/>
|
|
145
165
|
|
|
166
|
+
{/* Selection dot at the scrub intersection. `null` hides it; a custom
|
|
167
|
+
component renders the consumer's dot; otherwise the built-in dot. */}
|
|
168
|
+
<SelectionDotSlot
|
|
169
|
+
config={selectionDot}
|
|
170
|
+
x={scrubX}
|
|
171
|
+
y={selectionY}
|
|
172
|
+
active={scrubActive}
|
|
173
|
+
opacity={crosshairOpacity}
|
|
174
|
+
color={selectionColor ?? palette.line}
|
|
175
|
+
/>
|
|
176
|
+
|
|
146
177
|
{showTooltip && (
|
|
147
178
|
<>
|
|
148
179
|
<RoundedRect
|
|
@@ -165,24 +196,25 @@ export function CrosshairOverlay({
|
|
|
165
196
|
strokeWidth={1}
|
|
166
197
|
/>
|
|
167
198
|
|
|
168
|
-
{children ??
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
{children ??
|
|
200
|
+
renderTooltip?.() ?? (
|
|
201
|
+
<Group>
|
|
202
|
+
<SkiaText
|
|
203
|
+
x={valueTextX}
|
|
204
|
+
y={line1Y}
|
|
205
|
+
text={valueStr}
|
|
206
|
+
font={font}
|
|
207
|
+
color={tooltipColor ?? palette.tooltipText}
|
|
208
|
+
/>
|
|
209
|
+
<SkiaText
|
|
210
|
+
x={timeTextX}
|
|
211
|
+
y={line2Y}
|
|
212
|
+
text={timeStr}
|
|
213
|
+
font={font}
|
|
214
|
+
color={palette.gridLabel}
|
|
215
|
+
/>
|
|
216
|
+
</Group>
|
|
217
|
+
)}
|
|
186
218
|
</>
|
|
187
219
|
)}
|
|
188
220
|
</Group>
|
|
@@ -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
|
);
|