react-native-livechart 1.0.0 → 2.0.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 +15 -3
- package/dist/components/CrosshairLine.d.ts +6 -1
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +7 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +12 -5
- package/dist/components/DotOverlay.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 +6 -2
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts +8 -4
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +8 -2
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +4 -2
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +16 -13
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +2 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts +2 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +34 -4
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +3 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +3 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/candle.d.ts +2 -2
- package/dist/draw/candle.d.ts.map +1 -1
- package/dist/draw/grid.d.ts +2 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/line.d.ts +8 -8
- package/dist/draw/line.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +44 -0
- package/dist/draw/markerAtlas.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +12 -4
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/resolveChartLayout.d.ts +3 -1
- package/dist/hooks/resolveChartLayout.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 +2 -2
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +3 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts +3 -1
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useYAxis.d.ts +2 -1
- package/dist/hooks/useYAxis.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 +136 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairLine.tsx +30 -10
- package/src/components/CrosshairOverlay.tsx +31 -10
- package/src/components/DotOverlay.tsx +29 -14
- package/src/components/LiveChart.tsx +67 -15
- package/src/components/LiveChartSeries.tsx +76 -17
- package/src/components/LoadingOverlay.tsx +20 -11
- package/src/components/MarkerOverlay.tsx +119 -164
- package/src/components/MultiSeriesDots.tsx +30 -2
- package/src/components/YAxisOverlay.tsx +8 -4
- package/src/constants.ts +54 -14
- package/src/core/liveChartEngineTick.ts +7 -2
- package/src/core/liveChartSeriesEngineTick.ts +8 -2
- package/src/core/resolveConfig.ts +137 -38
- package/src/core/useLiveChartEngine.ts +6 -0
- package/src/core/useLiveChartSeriesEngine.ts +6 -0
- package/src/draw/candle.ts +11 -6
- package/src/draw/grid.ts +5 -5
- package/src/draw/line.ts +106 -25
- package/src/draw/markerAtlas.ts +304 -0
- package/src/hooks/crosshairShared.ts +24 -3
- package/src/hooks/resolveChartLayout.ts +11 -2
- package/src/hooks/useBadge.ts +38 -24
- package/src/hooks/useCandlePaths.ts +4 -2
- package/src/hooks/useCrosshair.ts +15 -2
- package/src/hooks/useCrosshairSeries.ts +8 -2
- package/src/hooks/useYAxis.ts +4 -1
- package/src/index.ts +7 -0
- package/src/types.ts +147 -5
|
@@ -15,6 +15,7 @@ export function CrosshairLine({
|
|
|
15
15
|
padding,
|
|
16
16
|
palette,
|
|
17
17
|
dimOpacity = 0.3,
|
|
18
|
+
liveDotExtent = 0,
|
|
18
19
|
crosshairLineColor,
|
|
19
20
|
crosshairDimColor,
|
|
20
21
|
}: {
|
|
@@ -25,30 +26,49 @@ export function CrosshairLine({
|
|
|
25
26
|
palette: LiveChartPalette;
|
|
26
27
|
/** Opacity of content right of the crosshair (dstOut fade). Default 0.3. */
|
|
27
28
|
dimOpacity?: number;
|
|
29
|
+
/** How far the live series dots extend past the plot's right edge. The dim
|
|
30
|
+
* region extends by this much so it fully covers the dots — centered on that
|
|
31
|
+
* edge, otherwise only half-dimmed — while leaving the value/Y-axis labels
|
|
32
|
+
* the gutter reserves beyond them bright. Default 0. */
|
|
33
|
+
liveDotExtent?: number;
|
|
28
34
|
crosshairLineColor?: string;
|
|
29
35
|
crosshairDimColor?: string;
|
|
30
36
|
}) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Explicit dependency arrays: with React Compiler enabled, Reanimated's
|
|
38
|
+
// auto-detected worklet dependencies can change array size between renders
|
|
39
|
+
// (e.g. when `liveDotExtent` flips 0 → the live-dot extent), which trips
|
|
40
|
+
// React's "final argument changed size between renders" error. Listing the
|
|
41
|
+
// captured plain values keeps the dependency array a constant size. SharedValue
|
|
42
|
+
// reads stay reactive regardless of this list.
|
|
43
|
+
const p1 = useDerivedValue(
|
|
44
|
+
() => ({
|
|
45
|
+
x: scrubX.value,
|
|
46
|
+
y: padding.top,
|
|
47
|
+
}),
|
|
48
|
+
[scrubX, padding.top],
|
|
49
|
+
);
|
|
50
|
+
const p2 = useDerivedValue(
|
|
51
|
+
() => ({
|
|
52
|
+
x: scrubX.value,
|
|
53
|
+
y: engine.canvasHeight.value - padding.bottom,
|
|
54
|
+
}),
|
|
55
|
+
[scrubX, engine.canvasHeight, padding.bottom],
|
|
56
|
+
);
|
|
39
57
|
|
|
40
58
|
const dimWidth = useDerivedValue(() => {
|
|
41
|
-
const rightEdge = engine.canvasWidth.value - padding.right;
|
|
59
|
+
const rightEdge = engine.canvasWidth.value - padding.right + liveDotExtent;
|
|
42
60
|
return Math.max(0, rightEdge - scrubX.value);
|
|
43
|
-
});
|
|
61
|
+
}, [engine.canvasWidth, padding.right, liveDotExtent, scrubX]);
|
|
44
62
|
const dimHeight = useDerivedValue(
|
|
45
63
|
() => engine.canvasHeight.value - padding.top - padding.bottom,
|
|
64
|
+
[engine.canvasHeight, padding.top, padding.bottom],
|
|
46
65
|
);
|
|
47
66
|
|
|
48
67
|
// dstOut erase color: alpha = fraction of trailing content to remove, ramped
|
|
49
68
|
// by the crosshair fade-in. RGB is irrelevant for dstOut.
|
|
50
69
|
const dimErase = useDerivedValue(
|
|
51
70
|
() => `rgba(0,0,0,${(1 - dimOpacity) * crosshairOpacity.value})`,
|
|
71
|
+
[dimOpacity, crosshairOpacity],
|
|
52
72
|
);
|
|
53
73
|
|
|
54
74
|
return (
|
|
@@ -26,6 +26,7 @@ export function CrosshairOverlay({
|
|
|
26
26
|
showTooltip = true,
|
|
27
27
|
children,
|
|
28
28
|
dimOpacity = 0.3,
|
|
29
|
+
liveDotExtent = 0,
|
|
29
30
|
crosshairLineColor,
|
|
30
31
|
crosshairDimColor,
|
|
31
32
|
tooltipBackground,
|
|
@@ -46,27 +47,46 @@ export function CrosshairOverlay({
|
|
|
46
47
|
children?: ReactNode;
|
|
47
48
|
/** Opacity of content right of the crosshair (dstOut fade). Default 0.3. */
|
|
48
49
|
dimOpacity?: number;
|
|
50
|
+
/** How far the live dot (and its pulse ring) extends past the plot's right
|
|
51
|
+
* edge. The dim region extends by this much so it fully covers the live
|
|
52
|
+
* indicator — which is centered on that edge and would otherwise be only
|
|
53
|
+
* half-dimmed — while stopping short of the Y-axis labels the gutter
|
|
54
|
+
* reserves beyond it. Default 0. */
|
|
55
|
+
liveDotExtent?: number;
|
|
49
56
|
crosshairLineColor?: string;
|
|
50
57
|
crosshairDimColor?: string;
|
|
51
58
|
tooltipBackground?: string;
|
|
52
59
|
tooltipColor?: string;
|
|
53
60
|
tooltipBorderColor?: string;
|
|
54
61
|
}) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// Explicit dependency arrays: with React Compiler enabled, Reanimated's
|
|
63
|
+
// auto-detected worklet dependencies can change array size between renders
|
|
64
|
+
// (e.g. when `liveDotExtent` flips 0 → the live-dot extent), which trips
|
|
65
|
+
// React's "final argument changed size between renders" error. Listing the
|
|
66
|
+
// captured plain values keeps the dependency array a constant size. SharedValue
|
|
67
|
+
// reads stay reactive regardless of this list.
|
|
68
|
+
const p1 = useDerivedValue(
|
|
69
|
+
() => ({
|
|
70
|
+
x: scrubX.value,
|
|
71
|
+
y: padding.top,
|
|
72
|
+
}),
|
|
73
|
+
[scrubX, padding.top],
|
|
74
|
+
);
|
|
75
|
+
const p2 = useDerivedValue(
|
|
76
|
+
() => ({
|
|
77
|
+
x: scrubX.value,
|
|
78
|
+
y: engine.canvasHeight.value - padding.bottom,
|
|
79
|
+
}),
|
|
80
|
+
[scrubX, engine.canvasHeight, padding.bottom],
|
|
81
|
+
);
|
|
63
82
|
|
|
64
83
|
const dimWidth = useDerivedValue(() => {
|
|
65
|
-
const rightEdge = engine.canvasWidth.value - padding.right;
|
|
84
|
+
const rightEdge = engine.canvasWidth.value - padding.right + liveDotExtent;
|
|
66
85
|
return Math.max(0, rightEdge - scrubX.value);
|
|
67
|
-
});
|
|
86
|
+
}, [engine.canvasWidth, padding.right, liveDotExtent, scrubX]);
|
|
68
87
|
const dimHeight = useDerivedValue(
|
|
69
88
|
() => engine.canvasHeight.value - padding.top - padding.bottom,
|
|
89
|
+
[engine.canvasHeight, padding.top, padding.bottom],
|
|
70
90
|
);
|
|
71
91
|
|
|
72
92
|
const tipX = useDerivedValue(() => tooltipLayout.value.x);
|
|
@@ -85,6 +105,7 @@ export function CrosshairOverlay({
|
|
|
85
105
|
// ramped by the crosshair fade-in. Color RGB is irrelevant for dstOut.
|
|
86
106
|
const dimErase = useDerivedValue(
|
|
87
107
|
() => `rgba(0,0,0,${(1 - dimOpacity) * crosshairOpacity.value})`,
|
|
108
|
+
[dimOpacity, crosshairOpacity],
|
|
88
109
|
);
|
|
89
110
|
|
|
90
111
|
return (
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { Circle, Group } from "@shopify/react-native-skia";
|
|
2
2
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ResolvedDotRingConfig,
|
|
5
|
+
ResolvedPulseConfig,
|
|
6
|
+
} from "../core/resolveConfig";
|
|
4
7
|
import type { LiveChartPalette } from "../types";
|
|
5
8
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
6
9
|
|
|
7
10
|
const MIN_PULSE_RADIUS = 9;
|
|
8
|
-
const DOT_OUTER_RADIUS = 6.5;
|
|
9
|
-
const DOT_INNER_RADIUS = 3.5;
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Live dot + expanding pulse ring.
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
13
|
+
* Live dot + expanding pulse ring. The dot is a color-filled circle of `radius`
|
|
14
|
+
* with an optional contrasting outer `ring` (halo). Peak pulse size uses
|
|
15
|
+
* `pulse.maxRadius` / `pulse.strokeWidth`; chart padding reserves the same outer
|
|
16
|
+
* extent via `pulseRadialOutset` in `draw/line.ts` (see `resolveChartLayout`).
|
|
15
17
|
*/
|
|
16
18
|
export function DotOverlay({
|
|
17
19
|
dotX,
|
|
@@ -19,13 +21,24 @@ export function DotOverlay({
|
|
|
19
21
|
palette,
|
|
20
22
|
engine,
|
|
21
23
|
pulse,
|
|
24
|
+
radius,
|
|
25
|
+
ring,
|
|
26
|
+
color,
|
|
22
27
|
}: {
|
|
23
28
|
dotX: SharedValue<number>;
|
|
24
29
|
dotY: SharedValue<number>;
|
|
25
30
|
palette: LiveChartPalette;
|
|
26
31
|
engine: ChartEngineLayout;
|
|
27
32
|
pulse: ResolvedPulseConfig | null;
|
|
33
|
+
/** Radius of the color-filled dot in pixels. */
|
|
34
|
+
radius: number;
|
|
35
|
+
/** Outer halo ring, or `null` for a flat dot. */
|
|
36
|
+
ring: ResolvedDotRingConfig | null;
|
|
37
|
+
/** Dot (and pulse) fill color; falls back to the chart line color. */
|
|
38
|
+
color: string | undefined;
|
|
28
39
|
}) {
|
|
40
|
+
const dotColor = color ?? palette.line;
|
|
41
|
+
|
|
29
42
|
const pulseRadius = useDerivedValue(() => {
|
|
30
43
|
if (!pulse) return 0;
|
|
31
44
|
const nowMs = engine.timestamp.value * 1000;
|
|
@@ -49,21 +62,23 @@ export function DotOverlay({
|
|
|
49
62
|
cx={dotX}
|
|
50
63
|
cy={dotY}
|
|
51
64
|
r={pulseRadius}
|
|
52
|
-
color={
|
|
65
|
+
color={dotColor}
|
|
53
66
|
style="stroke"
|
|
54
67
|
strokeWidth={pulse.strokeWidth}
|
|
55
68
|
opacity={pulseOpacity}
|
|
56
69
|
/>
|
|
57
70
|
)}
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
{ring && (
|
|
73
|
+
<Circle
|
|
74
|
+
cx={dotX}
|
|
75
|
+
cy={dotY}
|
|
76
|
+
r={radius + ring.width}
|
|
77
|
+
color={ring.color ?? palette.badgeOuterBg}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
65
80
|
|
|
66
|
-
<Circle cx={dotX} cy={dotY} r={
|
|
81
|
+
<Circle cx={dotX} cy={dotY} r={radius} color={dotColor} />
|
|
67
82
|
</Group>
|
|
68
83
|
);
|
|
69
84
|
}
|
|
@@ -21,9 +21,11 @@ import { DEFAULT_ACCENT_COLOR } from "../constants";
|
|
|
21
21
|
import {
|
|
22
22
|
resolveBadge,
|
|
23
23
|
resolveDegen,
|
|
24
|
+
resolveDot,
|
|
24
25
|
resolveGradient,
|
|
25
26
|
resolveGridStyle,
|
|
26
27
|
resolveLeftEdgeFade,
|
|
28
|
+
resolveMetrics,
|
|
27
29
|
resolvePulse,
|
|
28
30
|
resolveScrub,
|
|
29
31
|
resolveTradeStream,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
resolveYAxis,
|
|
33
35
|
} from "../core/resolveConfig";
|
|
34
36
|
import { useLiveChartEngine } from "../core/useLiveChartEngine";
|
|
37
|
+
import { pulseRadialOutset } from "../draw/line";
|
|
35
38
|
import { resolveChartLayout } from "../hooks/resolveChartLayout";
|
|
36
39
|
import { useBadge } from "../hooks/useBadge";
|
|
37
40
|
import { useCandlePaths } from "../hooks/useCandlePaths";
|
|
@@ -126,12 +129,14 @@ function useLiveChartController({
|
|
|
126
129
|
badge = true,
|
|
127
130
|
momentum = true,
|
|
128
131
|
pulse = true,
|
|
132
|
+
dot,
|
|
129
133
|
valueLine = true,
|
|
130
134
|
showValue = false,
|
|
131
135
|
valueMomentumColor = false,
|
|
132
136
|
referenceLines,
|
|
133
137
|
gridStyle,
|
|
134
138
|
palette: paletteOverride,
|
|
139
|
+
metrics,
|
|
135
140
|
scrub = true,
|
|
136
141
|
tradeStream,
|
|
137
142
|
degen,
|
|
@@ -158,9 +163,13 @@ function useLiveChartController({
|
|
|
158
163
|
const gradientCfg = isCandle ? null : resolveGradient(gradient);
|
|
159
164
|
const valueLineCfg = resolveValueLine(valueLine);
|
|
160
165
|
const pulseCfg = resolvePulse(pulse);
|
|
166
|
+
const dotCfg = resolveDot(dot);
|
|
167
|
+
// Outer footprint of the dot (color-filled radius plus the halo ring).
|
|
168
|
+
const dotOuterRadius = dotCfg.radius + (dotCfg.ring?.width ?? 0);
|
|
161
169
|
const gridStyleCfg = resolveGridStyle(gridStyle);
|
|
162
170
|
const degenCfg = resolveDegen(degen);
|
|
163
171
|
const tradeStreamResolved = resolveTradeStream(tradeStream);
|
|
172
|
+
const metricsCfg = resolveMetrics(metrics);
|
|
164
173
|
|
|
165
174
|
const allRefLines = referenceLines ?? [];
|
|
166
175
|
const refValues = collectReferenceValues(allRefLines);
|
|
@@ -221,6 +230,7 @@ function useLiveChartController({
|
|
|
221
230
|
insetsOverride: insets,
|
|
222
231
|
yAxis: yAxisCfg !== null,
|
|
223
232
|
badge: badgeCfg !== null,
|
|
233
|
+
badgeMetrics: metricsCfg.badge,
|
|
224
234
|
badgeUsesRightGutter,
|
|
225
235
|
badgeShowTail: badgeCfg?.tail ?? true,
|
|
226
236
|
xAxis: xAxisCfg !== null,
|
|
@@ -260,6 +270,7 @@ function useLiveChartController({
|
|
|
260
270
|
timeWindow,
|
|
261
271
|
paused,
|
|
262
272
|
smoothing,
|
|
273
|
+
adaptiveSpeedBoost: metricsCfg.motion.adaptiveSpeedBoost,
|
|
263
274
|
exaggerate,
|
|
264
275
|
referenceValues: refValues,
|
|
265
276
|
nonNegative,
|
|
@@ -294,6 +305,7 @@ function useLiveChartController({
|
|
|
294
305
|
isCandle ? liveEngine : liveCandle,
|
|
295
306
|
candleWidth,
|
|
296
307
|
isCandle,
|
|
308
|
+
metricsCfg.candle,
|
|
297
309
|
);
|
|
298
310
|
const { dotX, dotY } = useLiveDot(engine, effectivePadding);
|
|
299
311
|
|
|
@@ -319,6 +331,7 @@ function useLiveChartController({
|
|
|
319
331
|
formatValue,
|
|
320
332
|
skiaFont,
|
|
321
333
|
yAxisCfg?.minGap ?? 36,
|
|
334
|
+
metricsCfg.grid,
|
|
322
335
|
);
|
|
323
336
|
|
|
324
337
|
const { xAxisEntries } = useXAxis(
|
|
@@ -339,6 +352,8 @@ function useLiveChartController({
|
|
|
339
352
|
momentumSV,
|
|
340
353
|
badgeCfg?.position ?? "right",
|
|
341
354
|
badgeCfg?.background,
|
|
355
|
+
metricsCfg.badge,
|
|
356
|
+
metricsCfg.motion.badgeColorSpeed,
|
|
342
357
|
);
|
|
343
358
|
|
|
344
359
|
// Scrub/crosshair must see the same stash-backed candles as the engine.
|
|
@@ -361,6 +376,7 @@ function useLiveChartController({
|
|
|
361
376
|
scrubCfg !== null,
|
|
362
377
|
onScrub,
|
|
363
378
|
candleOpts,
|
|
379
|
+
scrubCfg?.panGestureDelay ?? 0,
|
|
364
380
|
);
|
|
365
381
|
|
|
366
382
|
const markersActive = markers != null;
|
|
@@ -414,10 +430,13 @@ function useLiveChartController({
|
|
|
414
430
|
gradientCfg,
|
|
415
431
|
valueLineCfg,
|
|
416
432
|
pulseCfg,
|
|
433
|
+
dotCfg,
|
|
434
|
+
dotOuterRadius,
|
|
417
435
|
gridStyleCfg,
|
|
418
436
|
degenCfg,
|
|
419
437
|
tradeStreamResolved,
|
|
420
438
|
leftEdgeFadeCfg,
|
|
439
|
+
metricsCfg,
|
|
421
440
|
allRefLines,
|
|
422
441
|
badgeUsesRightGutter,
|
|
423
442
|
// theme / layout / fonts
|
|
@@ -498,15 +517,16 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
498
517
|
downBodiesPath,
|
|
499
518
|
xAxisCfg,
|
|
500
519
|
xAxisEntries,
|
|
501
|
-
badgeData,
|
|
502
520
|
dotX,
|
|
503
521
|
pulseCfg,
|
|
522
|
+
dotCfg,
|
|
504
523
|
degenCfg,
|
|
505
524
|
degenPack,
|
|
506
525
|
degenPackRevision,
|
|
507
526
|
markersActive,
|
|
508
527
|
markersSV,
|
|
509
528
|
emptyText,
|
|
529
|
+
metricsCfg,
|
|
510
530
|
} = model;
|
|
511
531
|
|
|
512
532
|
return (
|
|
@@ -522,6 +542,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
522
542
|
font={skiaFont}
|
|
523
543
|
badge={badgeUsesRightGutter}
|
|
524
544
|
badgeTail={badgeCfg?.tail ?? true}
|
|
545
|
+
badgeMetrics={metricsCfg.badge}
|
|
525
546
|
gridStyle={gridStyleCfg}
|
|
526
547
|
/>
|
|
527
548
|
</Group>
|
|
@@ -611,23 +632,23 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
611
632
|
/>
|
|
612
633
|
)}
|
|
613
634
|
|
|
614
|
-
{/*
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
635
|
+
{/* Live dot — the badge is drawn later (after the scrub layer) so the
|
|
636
|
+
scrub dim never clips the live-price badge's left edge. */}
|
|
637
|
+
{dotCfg.show && (
|
|
638
|
+
<Group opacity={reveal.dotOpacity}>
|
|
639
|
+
<DotOverlay
|
|
640
|
+
dotX={dotX}
|
|
641
|
+
dotY={dotY}
|
|
642
|
+
palette={palette}
|
|
643
|
+
engine={engine}
|
|
644
|
+
pulse={pulseCfg}
|
|
645
|
+
radius={dotCfg.radius}
|
|
646
|
+
ring={dotCfg.ring}
|
|
647
|
+
color={dotCfg.color}
|
|
648
|
+
/>
|
|
618
649
|
</Group>
|
|
619
650
|
)}
|
|
620
651
|
|
|
621
|
-
<Group opacity={reveal.dotOpacity}>
|
|
622
|
-
<DotOverlay
|
|
623
|
-
dotX={dotX}
|
|
624
|
-
dotY={dotY}
|
|
625
|
-
palette={palette}
|
|
626
|
-
engine={engine}
|
|
627
|
-
pulse={pulseCfg}
|
|
628
|
-
/>
|
|
629
|
-
</Group>
|
|
630
|
-
|
|
631
652
|
{degenCfg && (
|
|
632
653
|
<Group opacity={reveal.dotOpacity}>
|
|
633
654
|
<DegenParticlesOverlay
|
|
@@ -669,6 +690,8 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
669
690
|
strokeWidth={strokeWidth}
|
|
670
691
|
badge={badgeCfg !== null}
|
|
671
692
|
badgeTail={badgeCfg?.tail ?? true}
|
|
693
|
+
badgeMetrics={metricsCfg.badge}
|
|
694
|
+
emptyMetrics={metricsCfg.emptyState}
|
|
672
695
|
/>
|
|
673
696
|
</Group>
|
|
674
697
|
);
|
|
@@ -688,10 +711,20 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
|
|
|
688
711
|
reveal,
|
|
689
712
|
crosshair,
|
|
690
713
|
isCandle,
|
|
714
|
+
pulseCfg,
|
|
715
|
+
dotOuterRadius,
|
|
691
716
|
} = model;
|
|
692
717
|
|
|
693
718
|
if (!tradeStreamResolved && !scrubCfg) return null;
|
|
694
719
|
|
|
720
|
+
// Extend the scrub dim past the plot's right edge to fully cover the live dot
|
|
721
|
+
// (with its halo) and pulse ring, all centered on that edge. The gutter
|
|
722
|
+
// reserves an 8px gap beyond this for the Y-axis labels, so they stay readable.
|
|
723
|
+
const liveDotExtent = Math.max(
|
|
724
|
+
dotOuterRadius,
|
|
725
|
+
pulseCfg ? pulseRadialOutset(pulseCfg.maxRadius, pulseCfg.strokeWidth) : 0,
|
|
726
|
+
);
|
|
727
|
+
|
|
695
728
|
return (
|
|
696
729
|
<Group transform={degenShakeTransform}>
|
|
697
730
|
{tradeStreamResolved && (
|
|
@@ -716,6 +749,7 @@ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
|
|
|
716
749
|
font={skiaFont}
|
|
717
750
|
showTooltip={scrubCfg.tooltip}
|
|
718
751
|
dimOpacity={scrubCfg.dimOpacity}
|
|
752
|
+
liveDotExtent={liveDotExtent}
|
|
719
753
|
crosshairLineColor={scrubCfg.crosshairLineColor}
|
|
720
754
|
crosshairDimColor={scrubCfg.crosshairDimColor}
|
|
721
755
|
tooltipBackground={scrubCfg.tooltipBackground}
|
|
@@ -774,6 +808,20 @@ function ChartValueOverlay({ model }: { model: LiveChartModel }) {
|
|
|
774
808
|
);
|
|
775
809
|
}
|
|
776
810
|
|
|
811
|
+
/** Live-price badge, drawn above the scrub dim so the dim never clips its left
|
|
812
|
+
* edge. Shares the degen shake transform so it tracks the shaken stack. */
|
|
813
|
+
function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
814
|
+
const { badgeCfg, badgeData, skiaFont, reveal, degenShakeTransform } = model;
|
|
815
|
+
if (!badgeCfg) return null;
|
|
816
|
+
return (
|
|
817
|
+
<Group transform={degenShakeTransform}>
|
|
818
|
+
<Group opacity={reveal.badgeOpacity}>
|
|
819
|
+
<BadgeOverlay badge={badgeData} font={skiaFont} />
|
|
820
|
+
</Group>
|
|
821
|
+
</Group>
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
777
825
|
export function LiveChart(props: LiveChartProps) {
|
|
778
826
|
const model = useLiveChartController(props);
|
|
779
827
|
const {
|
|
@@ -814,6 +862,10 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
814
862
|
<ChartValueOverlay model={model} />
|
|
815
863
|
|
|
816
864
|
<ChartScrubLayer model={model} />
|
|
865
|
+
|
|
866
|
+
{/* Live-price badge on top of the scrub dim so the dim never clips
|
|
867
|
+
its left edge (the badge tracks the live value, not the scrub). */}
|
|
868
|
+
<ChartBadgeLayer model={model} />
|
|
817
869
|
</Canvas>
|
|
818
870
|
</View>
|
|
819
871
|
</GestureDetector>
|
|
@@ -28,12 +28,14 @@ import {
|
|
|
28
28
|
resolveGridStyle,
|
|
29
29
|
resolveLeftEdgeFade,
|
|
30
30
|
resolveLegend,
|
|
31
|
+
resolveMetrics,
|
|
31
32
|
resolveMultiSeriesDot,
|
|
32
33
|
resolveScrub,
|
|
33
34
|
resolveXAxis,
|
|
34
35
|
resolveYAxis,
|
|
35
36
|
} from "../core/resolveConfig";
|
|
36
37
|
import { useLiveChartSeriesEngine } from "../core/useLiveChartSeriesEngine";
|
|
38
|
+
import { pulseRadialOutset } from "../draw/line";
|
|
37
39
|
import { resolveChartLayout } from "../hooks/resolveChartLayout";
|
|
38
40
|
import { useCanvasLayout } from "../hooks/useCanvasLayout";
|
|
39
41
|
import { useChartReveal } from "../hooks/useChartReveal";
|
|
@@ -122,7 +124,8 @@ function useLiveChartSeriesController({
|
|
|
122
124
|
referenceLines,
|
|
123
125
|
gridStyle,
|
|
124
126
|
palette: paletteOverride,
|
|
125
|
-
|
|
127
|
+
metrics,
|
|
128
|
+
scrub = true,
|
|
126
129
|
onScrub,
|
|
127
130
|
onSeriesToggle,
|
|
128
131
|
dot: dotProp,
|
|
@@ -143,8 +146,12 @@ function useLiveChartSeriesController({
|
|
|
143
146
|
const scrubEnabled = scrubCfg !== null;
|
|
144
147
|
const gridStyleCfg = resolveGridStyle(gridStyle);
|
|
145
148
|
const dotCfg = resolveMultiSeriesDot(dotProp);
|
|
149
|
+
// Outer footprint of a dot (the color-filled radius plus the halo ring).
|
|
150
|
+
// Used to keep the gutter labels clear of the haloed dot.
|
|
151
|
+
const dotOuterRadius = dotCfg.radius + (dotCfg.ring?.width ?? 0);
|
|
146
152
|
const legendCfg = resolveLegend(legendProp);
|
|
147
153
|
const degenCfg = resolveDegen(degen);
|
|
154
|
+
const metricsCfg = resolveMetrics(metrics);
|
|
148
155
|
|
|
149
156
|
const allRefLines = referenceLines ?? [];
|
|
150
157
|
const refValues = collectReferenceValues(allRefLines);
|
|
@@ -190,7 +197,7 @@ function useLiveChartSeriesController({
|
|
|
190
197
|
: 0;
|
|
191
198
|
|
|
192
199
|
const seriesLabelInset = dotCfg.valueLabel
|
|
193
|
-
?
|
|
200
|
+
? dotOuterRadius + 8 + maxSeriesLabelWidth + 8
|
|
194
201
|
: 0;
|
|
195
202
|
|
|
196
203
|
const representativeValue =
|
|
@@ -204,12 +211,13 @@ function useLiveChartSeriesController({
|
|
|
204
211
|
insetsOverride: insets,
|
|
205
212
|
yAxis: yAxisCfg !== null,
|
|
206
213
|
badge: false,
|
|
214
|
+
badgeMetrics: metricsCfg.badge,
|
|
207
215
|
xAxis: xAxisCfg !== null,
|
|
208
216
|
font: skiaFont,
|
|
209
217
|
formatValue,
|
|
210
218
|
currentValue: representativeValue,
|
|
211
219
|
pulse: dotCfg.pulse,
|
|
212
|
-
multiSeriesDotRadius:
|
|
220
|
+
multiSeriesDotRadius: dotOuterRadius,
|
|
213
221
|
multiSeriesValueLabel: dotCfg.valueLabel,
|
|
214
222
|
multiSeriesMaxLabelWidth: maxSeriesLabelWidth,
|
|
215
223
|
});
|
|
@@ -236,6 +244,7 @@ function useLiveChartSeriesController({
|
|
|
236
244
|
timeWindow,
|
|
237
245
|
paused,
|
|
238
246
|
smoothing,
|
|
247
|
+
adaptiveSpeedBoost: metricsCfg.motion.adaptiveSpeedBoost,
|
|
239
248
|
exaggerate,
|
|
240
249
|
referenceValues: refValues,
|
|
241
250
|
nonNegative,
|
|
@@ -282,6 +291,7 @@ function useLiveChartSeriesController({
|
|
|
282
291
|
formatValue,
|
|
283
292
|
skiaFont,
|
|
284
293
|
yAxisCfg?.minGap ?? 36,
|
|
294
|
+
metricsCfg.grid,
|
|
285
295
|
);
|
|
286
296
|
|
|
287
297
|
const { xAxisEntries } = useXAxis(
|
|
@@ -296,6 +306,7 @@ function useLiveChartSeriesController({
|
|
|
296
306
|
effectivePadding,
|
|
297
307
|
scrubEnabled,
|
|
298
308
|
onScrub,
|
|
309
|
+
scrubCfg?.panGestureDelay ?? 0,
|
|
299
310
|
);
|
|
300
311
|
|
|
301
312
|
// `projected` is used internally by the hit-test gesture; the overlay
|
|
@@ -331,8 +342,10 @@ function useLiveChartSeriesController({
|
|
|
331
342
|
scrubCfg,
|
|
332
343
|
gridStyleCfg,
|
|
333
344
|
dotCfg,
|
|
345
|
+
dotOuterRadius,
|
|
334
346
|
legendCfg,
|
|
335
347
|
degenCfg,
|
|
348
|
+
metricsCfg,
|
|
336
349
|
allRefLines,
|
|
337
350
|
leftEdgeFadeCfg,
|
|
338
351
|
// theme / layout / fonts
|
|
@@ -396,6 +409,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
396
409
|
markersSV,
|
|
397
410
|
series,
|
|
398
411
|
emptyText,
|
|
412
|
+
metricsCfg,
|
|
399
413
|
} = model;
|
|
400
414
|
|
|
401
415
|
return (
|
|
@@ -462,28 +476,25 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
462
476
|
/>
|
|
463
477
|
)}
|
|
464
478
|
|
|
465
|
-
|
|
466
|
-
<MultiSeriesDots
|
|
467
|
-
engine={engine}
|
|
468
|
-
padding={effectivePadding}
|
|
469
|
-
colors={lineColors}
|
|
470
|
-
radius={dotCfg.radius}
|
|
471
|
-
pulse={dotCfg.pulse}
|
|
472
|
-
/>
|
|
473
|
-
</Group>
|
|
474
|
-
|
|
475
|
-
{dotCfg.valueLabel && (
|
|
479
|
+
{dotCfg.show && (
|
|
476
480
|
<Group opacity={reveal.dotOpacity}>
|
|
477
|
-
<
|
|
481
|
+
<MultiSeriesDots
|
|
478
482
|
engine={engine}
|
|
479
483
|
padding={effectivePadding}
|
|
480
484
|
colors={lineColors}
|
|
481
|
-
|
|
482
|
-
|
|
485
|
+
radius={dotCfg.radius}
|
|
486
|
+
ring={dotCfg.ring}
|
|
487
|
+
ringColor={palette.badgeOuterBg}
|
|
488
|
+
color={dotCfg.color}
|
|
489
|
+
pulse={dotCfg.pulse}
|
|
483
490
|
/>
|
|
484
491
|
</Group>
|
|
485
492
|
)}
|
|
486
493
|
|
|
494
|
+
{/* Value labels are drawn later (after the crosshair layer) so the scrub
|
|
495
|
+
dim — which now covers the dots + pulse rings — never clips them.
|
|
496
|
+
They track each series' live value, not the scrub point. */}
|
|
497
|
+
|
|
487
498
|
{degenCfg && (
|
|
488
499
|
<Group opacity={reveal.dotOpacity}>
|
|
489
500
|
<DegenParticlesOverlay
|
|
@@ -523,11 +534,42 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
523
534
|
emptyText={emptyText}
|
|
524
535
|
strokeWidth={strokeWidth}
|
|
525
536
|
badge={false}
|
|
537
|
+
emptyMetrics={metricsCfg.emptyState}
|
|
526
538
|
/>
|
|
527
539
|
</Group>
|
|
528
540
|
);
|
|
529
541
|
}
|
|
530
542
|
|
|
543
|
+
/** Per-series live-value labels, drawn above the scrub dim so the dim (which
|
|
544
|
+
* covers the dots + pulse rings) never clips them. Keeps the degen shake
|
|
545
|
+
* transform so they track the shaken stack. */
|
|
546
|
+
function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
547
|
+
const {
|
|
548
|
+
dotCfg,
|
|
549
|
+
dotOuterRadius,
|
|
550
|
+
engine,
|
|
551
|
+
effectivePadding,
|
|
552
|
+
lineColors,
|
|
553
|
+
skiaFont,
|
|
554
|
+
reveal,
|
|
555
|
+
degenShakeTransform,
|
|
556
|
+
} = model;
|
|
557
|
+
if (!dotCfg.valueLabel) return null;
|
|
558
|
+
return (
|
|
559
|
+
<Group transform={degenShakeTransform}>
|
|
560
|
+
<Group opacity={reveal.dotOpacity}>
|
|
561
|
+
<MultiSeriesValueLabels
|
|
562
|
+
engine={engine}
|
|
563
|
+
padding={effectivePadding}
|
|
564
|
+
colors={lineColors}
|
|
565
|
+
font={skiaFont}
|
|
566
|
+
dotRadius={dotOuterRadius}
|
|
567
|
+
/>
|
|
568
|
+
</Group>
|
|
569
|
+
</Group>
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
531
573
|
export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
532
574
|
const model = useLiveChartSeriesController(props);
|
|
533
575
|
const {
|
|
@@ -547,8 +589,20 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
547
589
|
scrubCfg,
|
|
548
590
|
crosshair,
|
|
549
591
|
palette,
|
|
592
|
+
dotCfg,
|
|
593
|
+
dotOuterRadius,
|
|
550
594
|
} = model;
|
|
551
595
|
|
|
596
|
+
// Extend the scrub dim past the plot's right edge to fully cover the series
|
|
597
|
+
// dots (with their halo) and pulse rings, all centered on that edge. The
|
|
598
|
+
// gutter reserves room beyond this for the value/Y-axis labels, drawn on top.
|
|
599
|
+
const liveDotExtent = Math.max(
|
|
600
|
+
dotOuterRadius,
|
|
601
|
+
dotCfg.pulse
|
|
602
|
+
? pulseRadialOutset(dotCfg.pulse.maxRadius, dotCfg.pulse.strokeWidth)
|
|
603
|
+
: 0,
|
|
604
|
+
);
|
|
605
|
+
|
|
552
606
|
const legend =
|
|
553
607
|
legendCfg.position === "top" || legendCfg.position === "bottom" ? (
|
|
554
608
|
<SeriesToggleChips
|
|
@@ -594,10 +648,15 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
594
648
|
padding={effectivePadding}
|
|
595
649
|
palette={palette}
|
|
596
650
|
dimOpacity={scrubCfg.dimOpacity}
|
|
651
|
+
liveDotExtent={liveDotExtent}
|
|
597
652
|
crosshairLineColor={scrubCfg.crosshairLineColor}
|
|
598
653
|
crosshairDimColor={scrubCfg.crosshairDimColor}
|
|
599
654
|
/>
|
|
600
655
|
)}
|
|
656
|
+
|
|
657
|
+
{/* Per-series value labels on top of the scrub dim so the dim never
|
|
658
|
+
clips them (they track each series' live value, not the scrub). */}
|
|
659
|
+
<SeriesValueLabelLayer model={model} />
|
|
601
660
|
</Canvas>
|
|
602
661
|
</View>
|
|
603
662
|
</GestureDetector>
|