react-native-livechart 1.1.0 → 2.0.1
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 +17 -4
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.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/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 +18 -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/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/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 +100 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairLine.tsx +23 -9
- package/src/components/CrosshairOverlay.tsx +23 -9
- package/src/components/LiveChart.tsx +14 -0
- package/src/components/LiveChartSeries.tsx +10 -1
- package/src/components/LoadingOverlay.tsx +20 -11
- package/src/components/MarkerOverlay.tsx +119 -164
- 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 +88 -32
- 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/resolveChartLayout.ts +11 -2
- package/src/hooks/useBadge.ts +38 -24
- package/src/hooks/useCandlePaths.ts +4 -2
- package/src/hooks/useYAxis.ts +4 -1
- package/src/index.ts +7 -0
- package/src/types.ts +109 -5
|
@@ -13,10 +13,8 @@ import {
|
|
|
13
13
|
import { useRef } from "react";
|
|
14
14
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
EMPTY_STATE_LABEL_ALPHA,
|
|
19
|
-
EMPTY_TEXT_GAP_PAD,
|
|
16
|
+
BADGE_METRICS_DEFAULTS,
|
|
17
|
+
EMPTY_STATE_METRICS_DEFAULTS,
|
|
20
18
|
} from "../constants";
|
|
21
19
|
import {
|
|
22
20
|
badgeTailAndCap,
|
|
@@ -26,7 +24,11 @@ import {
|
|
|
26
24
|
} from "../draw/line";
|
|
27
25
|
import { drawSpline } from "../math/spline";
|
|
28
26
|
import { buildSquigglyPts } from "../math/squiggly";
|
|
29
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
BadgeMetrics,
|
|
29
|
+
EmptyStateMetrics,
|
|
30
|
+
LiveChartPalette,
|
|
31
|
+
} from "../types";
|
|
30
32
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
31
33
|
|
|
32
34
|
const PLACEHOLDER_LABEL_COUNT = 4;
|
|
@@ -57,6 +59,8 @@ export function LoadingOverlay({
|
|
|
57
59
|
strokeWidth,
|
|
58
60
|
badge = false,
|
|
59
61
|
badgeTail = true,
|
|
62
|
+
badgeMetrics = BADGE_METRICS_DEFAULTS,
|
|
63
|
+
emptyMetrics = EMPTY_STATE_METRICS_DEFAULTS,
|
|
60
64
|
}: {
|
|
61
65
|
engine: ChartEngineLayout;
|
|
62
66
|
padding: ChartPadding;
|
|
@@ -72,9 +76,14 @@ export function LoadingOverlay({
|
|
|
72
76
|
badge?: boolean;
|
|
73
77
|
/** Whether the badge tail spike is shown; affects the left inset used for skeleton alignment. */
|
|
74
78
|
badgeTail?: boolean;
|
|
79
|
+
/** Badge pill geometry tokens (kept in sync with GridOverlay/useBadge). */
|
|
80
|
+
badgeMetrics?: BadgeMetrics;
|
|
81
|
+
/** Empty-state layout tokens. */
|
|
82
|
+
emptyMetrics?: EmptyStateMetrics;
|
|
75
83
|
}) {
|
|
76
84
|
// Same left-inset formula as GridOverlay (only used when badge=true)
|
|
77
|
-
const leftInset =
|
|
85
|
+
const leftInset =
|
|
86
|
+
badgeMetrics.dotGap + badgeTailAndCap(font.getSize(), badgeTail, badgeMetrics);
|
|
78
87
|
|
|
79
88
|
// Ping-pong persistent paths — avoid allocating a JSI-backed SkPath per frame
|
|
80
89
|
// while the squiggly loading/empty-state animation is running.
|
|
@@ -127,7 +136,7 @@ export function LoadingOverlay({
|
|
|
127
136
|
const chartH = h - padding.top - padding.bottom;
|
|
128
137
|
// Mirror GridOverlay: pillTextLeftX when badge is on, gutterCenteredTextLeftX otherwise
|
|
129
138
|
const x = badge
|
|
130
|
-
? pillTextLeftX(w, padding.right, leftInset, RECT_W)
|
|
139
|
+
? pillTextLeftX(w, padding.right, leftInset, RECT_W, badgeMetrics)
|
|
131
140
|
: gutterCenteredTextLeftX(w, padding.right, RECT_W);
|
|
132
141
|
// Centre the group of rects around the chart's vertical midpoint
|
|
133
142
|
const groupH = (PLACEHOLDER_LABEL_COUNT - 1) * RECT_SPACING;
|
|
@@ -157,7 +166,7 @@ export function LoadingOverlay({
|
|
|
157
166
|
gapRight: 0,
|
|
158
167
|
centerY: 0,
|
|
159
168
|
eraseH: 0,
|
|
160
|
-
fadeW:
|
|
169
|
+
fadeW: emptyMetrics.gapFadeWidth,
|
|
161
170
|
textX: 0,
|
|
162
171
|
textY: 0,
|
|
163
172
|
textW: 0,
|
|
@@ -168,8 +177,8 @@ export function LoadingOverlay({
|
|
|
168
177
|
const centerY = padding.top + chartH / 2;
|
|
169
178
|
const chartCentreX = (padding.left + w - padding.right) / 2;
|
|
170
179
|
const textW = font.measureText(emptyText).width;
|
|
171
|
-
const gapHalf = textW / 2 +
|
|
172
|
-
const fadeW =
|
|
180
|
+
const gapHalf = textW / 2 + emptyMetrics.gapPad;
|
|
181
|
+
const fadeW = emptyMetrics.gapFadeWidth;
|
|
173
182
|
const gapLeft = chartCentreX - gapHalf - fadeW;
|
|
174
183
|
const gapRight = chartCentreX + gapHalf + fadeW;
|
|
175
184
|
const eraseH = Math.max(56, chartH * 0.28) + strokeWidth;
|
|
@@ -221,7 +230,7 @@ export function LoadingOverlay({
|
|
|
221
230
|
isEmpty.value ? emptyText : "",
|
|
222
231
|
);
|
|
223
232
|
const emptyLabelOpacity = useDerivedValue(() =>
|
|
224
|
-
isEmpty.value ?
|
|
233
|
+
isEmpty.value ? emptyMetrics.labelOpacity : 0,
|
|
225
234
|
);
|
|
226
235
|
|
|
227
236
|
return (
|
|
@@ -1,87 +1,70 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
Atlas,
|
|
3
3
|
Group,
|
|
4
|
-
Image as SkiaImage,
|
|
5
4
|
Path,
|
|
6
5
|
Skia,
|
|
7
|
-
Text as SkiaText,
|
|
8
6
|
type SkFont,
|
|
9
7
|
type SkPath,
|
|
10
8
|
} from "@shopify/react-native-skia";
|
|
11
|
-
import { useRef, useState } from "react";
|
|
9
|
+
import { useMemo, useRef, useState } from "react";
|
|
12
10
|
import {
|
|
13
|
-
useAnimatedReaction,
|
|
14
11
|
useDerivedValue,
|
|
12
|
+
useAnimatedReaction,
|
|
15
13
|
type SharedValue,
|
|
16
14
|
} from "react-native-reanimated";
|
|
17
15
|
import { scheduleOnRN } from "react-native-worklets";
|
|
18
16
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
19
17
|
import type { ChartPadding } from "../draw/line";
|
|
20
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
buildMarkerAtlas,
|
|
20
|
+
defaultMarkerColor,
|
|
21
|
+
isConnectorMarker,
|
|
22
|
+
markerAppearanceSig,
|
|
23
|
+
type AtlasCell,
|
|
24
|
+
} from "../draw/markerAtlas";
|
|
25
|
+
import {
|
|
26
|
+
markersSignature,
|
|
27
|
+
projectMarkers,
|
|
28
|
+
projectPoint,
|
|
29
|
+
type ProjectedMarker,
|
|
30
|
+
} from "../math/markers";
|
|
21
31
|
import type {
|
|
22
32
|
LiveChartPalette,
|
|
23
33
|
LiveChartPoint,
|
|
24
34
|
Marker,
|
|
25
|
-
MarkerKind,
|
|
26
35
|
SeriesConfig,
|
|
27
36
|
} from "../types";
|
|
28
37
|
|
|
29
|
-
/** Default glyph color per kind when `marker.color` is unset. */
|
|
30
|
-
function defaultMarkerColor(kind: MarkerKind, palette: LiveChartPalette): string {
|
|
31
|
-
switch (kind) {
|
|
32
|
-
case "trade":
|
|
33
|
-
return palette.line;
|
|
34
|
-
case "boost":
|
|
35
|
-
return palette.refLine;
|
|
36
|
-
case "graduation":
|
|
37
|
-
return palette.dotUp;
|
|
38
|
-
case "winner":
|
|
39
|
-
return palette.dotUp;
|
|
40
|
-
case "clawback":
|
|
41
|
-
return palette.refLabel;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
38
|
const OFF = -9999;
|
|
46
|
-
const DEFAULT_ICON_SIZE = 16;
|
|
47
|
-
/** Circular badge: padding around the icon glyph, background-colored ring width,
|
|
48
|
-
* and the icon color drawn on the fill. */
|
|
49
|
-
const PILL_PAD = 2;
|
|
50
|
-
const PILL_BORDER = 2;
|
|
51
|
-
const PILL_TEXT_COLOR = "#ffffff";
|
|
52
39
|
|
|
53
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Self-projecting fallback for the axis-anchored kinds (graduation stem + flag,
|
|
42
|
+
* clawback box) that can't be fixed-size atlas sprites. Rare, so the per-glyph
|
|
43
|
+
* worklet cost here is negligible; each glyph still projects its OWN marker so
|
|
44
|
+
* reordering the array can't make it flash another marker's position.
|
|
45
|
+
*/
|
|
46
|
+
function ConnectorGlyph({
|
|
54
47
|
marker,
|
|
55
48
|
color,
|
|
56
|
-
bgColor,
|
|
57
49
|
engine,
|
|
58
50
|
padding,
|
|
59
51
|
seriesSV,
|
|
60
52
|
lineDataSV,
|
|
61
|
-
font,
|
|
62
53
|
axisY,
|
|
63
54
|
}: {
|
|
64
55
|
marker: Marker;
|
|
65
56
|
color: string;
|
|
66
|
-
/** Chart background color, drawn as a ring behind a `pill` badge. */
|
|
67
|
-
bgColor?: string;
|
|
68
57
|
engine: ChartEngineLayout;
|
|
69
58
|
padding: ChartPadding;
|
|
70
59
|
seriesSV?: SharedValue<SeriesConfig[]>;
|
|
71
60
|
lineDataSV?: SharedValue<LiveChartPoint[]>;
|
|
72
|
-
font: SkFont;
|
|
73
61
|
axisY: SharedValue<number>;
|
|
74
62
|
}) {
|
|
75
|
-
|
|
76
|
-
// carry non-serializable `data` / `image`) — in the worklet closure.
|
|
77
|
-
const { kind, icon, image, pill } = marker;
|
|
63
|
+
const { kind } = marker;
|
|
78
64
|
const time = marker.time;
|
|
79
65
|
const value = marker.value;
|
|
80
66
|
const seriesId = marker.seriesId;
|
|
81
|
-
const size = marker.size ?? DEFAULT_ICON_SIZE;
|
|
82
67
|
|
|
83
|
-
// Each glyph projects its OWN marker — no shared index buffer, so reordering
|
|
84
|
-
// the marker array can't make a glyph flash another marker's position.
|
|
85
68
|
const layout = useDerivedValue(() =>
|
|
86
69
|
projectPoint(time, value, seriesId, {
|
|
87
70
|
canvasWidth: engine.canvasWidth.get(),
|
|
@@ -99,12 +82,6 @@ function MarkerGlyph({
|
|
|
99
82
|
}),
|
|
100
83
|
);
|
|
101
84
|
|
|
102
|
-
const cx = useDerivedValue(() =>
|
|
103
|
-
layout.get().visible ? layout.get().x : OFF,
|
|
104
|
-
);
|
|
105
|
-
const cy = useDerivedValue(() =>
|
|
106
|
-
layout.get().visible ? layout.get().y : OFF,
|
|
107
|
-
);
|
|
108
85
|
const opacity = useDerivedValue(() => (layout.get().visible ? 1 : 0));
|
|
109
86
|
|
|
110
87
|
const cacheRef = useRef<{ a: SkPath; b: SkPath; tick: boolean } | null>(null);
|
|
@@ -119,30 +96,9 @@ function MarkerGlyph({
|
|
|
119
96
|
p.reset();
|
|
120
97
|
const l = layout.get();
|
|
121
98
|
if (!l.visible) return p;
|
|
122
|
-
const x = l.x;
|
|
99
|
+
const x = l.visible ? l.x : OFF;
|
|
123
100
|
const y = l.y;
|
|
124
|
-
if (kind === "
|
|
125
|
-
const outer = 7;
|
|
126
|
-
const inner = 3;
|
|
127
|
-
for (let k = 0; k < 10; k++) {
|
|
128
|
-
const ang = -Math.PI / 2 + (k * Math.PI) / 5;
|
|
129
|
-
const rad = k % 2 === 0 ? outer : inner;
|
|
130
|
-
const px = x + rad * Math.cos(ang);
|
|
131
|
-
const py = y + rad * Math.sin(ang);
|
|
132
|
-
if (k === 0) p.moveTo(px, py);
|
|
133
|
-
else p.lineTo(px, py);
|
|
134
|
-
}
|
|
135
|
-
p.close();
|
|
136
|
-
} else if (kind === "boost") {
|
|
137
|
-
const L = 6;
|
|
138
|
-
for (let k = 0; k < 4; k++) {
|
|
139
|
-
const ang = (k * Math.PI) / 4;
|
|
140
|
-
const dx = L * Math.cos(ang);
|
|
141
|
-
const dy = L * Math.sin(ang);
|
|
142
|
-
p.moveTo(x - dx, y - dy);
|
|
143
|
-
p.lineTo(x + dx, y + dy);
|
|
144
|
-
}
|
|
145
|
-
} else if (kind === "graduation") {
|
|
101
|
+
if (kind === "graduation") {
|
|
146
102
|
p.moveTo(x, axisY.get());
|
|
147
103
|
p.lineTo(x, y);
|
|
148
104
|
p.moveTo(x, y - 7);
|
|
@@ -161,88 +117,7 @@ function MarkerGlyph({
|
|
|
161
117
|
return p;
|
|
162
118
|
});
|
|
163
119
|
|
|
164
|
-
|
|
165
|
-
const imgX = useDerivedValue(() =>
|
|
166
|
-
layout.get().visible ? layout.get().x - size / 2 : OFF,
|
|
167
|
-
);
|
|
168
|
-
const imgY = useDerivedValue(() =>
|
|
169
|
-
layout.get().visible ? layout.get().y - size / 2 : OFF,
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
// Text / emoji icon centering — width + baseline shift depend only on the
|
|
173
|
-
// (stable) font and icon, so measure once instead of every frame. Skia
|
|
174
|
-
// `measureText`/`getMetrics` both allocate, so hoisting them out of the
|
|
175
|
-
// per-frame worklet removes one measure + one metrics call per glyph/frame.
|
|
176
|
-
// Center the icon glyph on the marker using its measured bounds — tighter and
|
|
177
|
-
// more accurate than font ascent/descent, so `+` / `−` etc. sit centered both
|
|
178
|
-
// horizontally and vertically in the badge. Stable per render, not per frame.
|
|
179
|
-
const iconBounds = font.measureText(icon ?? "");
|
|
180
|
-
const iconDX = iconBounds.x + iconBounds.width / 2;
|
|
181
|
-
const iconDY = iconBounds.y + iconBounds.height / 2;
|
|
182
|
-
|
|
183
|
-
const iconX = useDerivedValue(() =>
|
|
184
|
-
layout.get().visible ? layout.get().x - iconDX : OFF,
|
|
185
|
-
);
|
|
186
|
-
const iconY = useDerivedValue(() =>
|
|
187
|
-
layout.get().visible ? layout.get().y - iconDY : OFF,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
// Circular badge radius — fits the glyph's larger dimension + padding.
|
|
191
|
-
const pillR = Math.max(iconBounds.width, iconBounds.height) / 2 + PILL_PAD;
|
|
192
|
-
|
|
193
|
-
if (image) {
|
|
194
|
-
return (
|
|
195
|
-
<Group opacity={opacity}>
|
|
196
|
-
<SkiaImage
|
|
197
|
-
image={image}
|
|
198
|
-
x={imgX}
|
|
199
|
-
y={imgY}
|
|
200
|
-
width={size}
|
|
201
|
-
height={size}
|
|
202
|
-
fit="contain"
|
|
203
|
-
/>
|
|
204
|
-
</Group>
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (icon) {
|
|
209
|
-
if (pill) {
|
|
210
|
-
// Filled circular badge in the marker color with the icon in white — e.g.
|
|
211
|
-
// a green `+` buy / red `−` sell tag. A background-colored ring underneath
|
|
212
|
-
// separates it from the line passing behind.
|
|
213
|
-
return (
|
|
214
|
-
<Group opacity={opacity}>
|
|
215
|
-
{bgColor !== undefined && (
|
|
216
|
-
<Circle cx={cx} cy={cy} r={pillR + PILL_BORDER} color={bgColor} />
|
|
217
|
-
)}
|
|
218
|
-
<Circle cx={cx} cy={cy} r={pillR} color={color} />
|
|
219
|
-
<SkiaText
|
|
220
|
-
x={iconX}
|
|
221
|
-
y={iconY}
|
|
222
|
-
text={icon}
|
|
223
|
-
font={font}
|
|
224
|
-
color={PILL_TEXT_COLOR}
|
|
225
|
-
/>
|
|
226
|
-
</Group>
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
return (
|
|
230
|
-
<Group opacity={opacity}>
|
|
231
|
-
<SkiaText x={iconX} y={iconY} text={icon} font={font} color={color} />
|
|
232
|
-
</Group>
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (kind === "trade") {
|
|
237
|
-
return (
|
|
238
|
-
<Group opacity={opacity}>
|
|
239
|
-
<Circle cx={cx} cy={cy} r={5} color={color} style="stroke" strokeWidth={2} />
|
|
240
|
-
<Circle cx={cx} cy={cy} r={2} color={color} />
|
|
241
|
-
</Group>
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const fill = kind === "winner" || kind === "clawback";
|
|
120
|
+
const fill = kind === "clawback";
|
|
246
121
|
return (
|
|
247
122
|
<Group opacity={opacity}>
|
|
248
123
|
<Path
|
|
@@ -258,10 +133,14 @@ function MarkerGlyph({
|
|
|
258
133
|
}
|
|
259
134
|
|
|
260
135
|
/**
|
|
261
|
-
* Renders the `markers` SharedValue as canvas glyphs.
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
136
|
+
* Renders the `markers` SharedValue as canvas glyphs.
|
|
137
|
+
*
|
|
138
|
+
* Every "stamp" glyph (icon, pill, image, and the trade/winner/boost shapes) is
|
|
139
|
+
* pre-rasterized once per distinct appearance into a single packed atlas image,
|
|
140
|
+
* then drawn each frame with ONE `drawAtlas` call driven by a single worklet —
|
|
141
|
+
* so per-frame UI-thread cost is O(1) mappers + one draw, not O(markers × ~9
|
|
142
|
+
* derived values + N Skia subtrees). Axis-anchored kinds (graduation/clawback)
|
|
143
|
+
* fall back to `ConnectorGlyph`.
|
|
265
144
|
*
|
|
266
145
|
* Glyph precedence per marker: `image` → `icon` (text) → built-in `kind` shape.
|
|
267
146
|
*/
|
|
@@ -288,9 +167,7 @@ export function MarkerOverlay({
|
|
|
288
167
|
const [snapshot, setSnapshot] = useState<Marker[]>(() => markers.get().slice());
|
|
289
168
|
|
|
290
169
|
// Read the `markers` prop from closure rather than a SharedValue passed
|
|
291
|
-
// through `scheduleOnRN
|
|
292
|
-
// boundary exposes the native `.value` accessor but NOT the `.get()` method,
|
|
293
|
-
// so calling `.get()` on it throws ("sv.get is not a function").
|
|
170
|
+
// through `scheduleOnRN` (which loses `.get()`); mirrors the data model.
|
|
294
171
|
const pull = () => {
|
|
295
172
|
setSnapshot(markers.get().slice());
|
|
296
173
|
};
|
|
@@ -304,25 +181,103 @@ export function MarkerOverlay({
|
|
|
304
181
|
[markers, pull],
|
|
305
182
|
);
|
|
306
183
|
|
|
184
|
+
// Rebuild the atlas image only when the set of distinct appearances or the
|
|
185
|
+
// theme/font changes — never per frame. `markersSignature` already drives the
|
|
186
|
+
// snapshot, so this useMemo re-runs at most at the snapshot cadence.
|
|
187
|
+
const appearanceKey = useMemo(() => {
|
|
188
|
+
const sigs = new Set<string>();
|
|
189
|
+
for (let i = 0; i < snapshot.length; i++) {
|
|
190
|
+
const m = snapshot[i];
|
|
191
|
+
if (!isConnectorMarker(m)) sigs.add(markerAppearanceSig(m));
|
|
192
|
+
}
|
|
193
|
+
return Array.from(sigs).sort().join("\x1e");
|
|
194
|
+
}, [snapshot]);
|
|
195
|
+
// Cells bake in resolved colors; include the palette fields they depend on.
|
|
196
|
+
const paletteKey = `${palette.bgRgb.join(",")}|${palette.line}|${palette.refLine}|${palette.dotUp}|${palette.refLabel}`;
|
|
197
|
+
const atlas = useMemo(
|
|
198
|
+
() => buildMarkerAtlas(snapshot, palette, font),
|
|
199
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- appearanceKey/paletteKey capture the inputs that change cell pixels
|
|
200
|
+
[appearanceKey, paletteKey, font],
|
|
201
|
+
);
|
|
202
|
+
const cells: Record<string, AtlasCell> = atlas.cells;
|
|
203
|
+
const atlasImage = atlas.image;
|
|
204
|
+
|
|
205
|
+
// One pooled, ping-ponged projection buffer reused every frame (no per-frame
|
|
206
|
+
// array allocation for the projection itself).
|
|
207
|
+
const projRef = useRef<{
|
|
208
|
+
a: ProjectedMarker[];
|
|
209
|
+
b: ProjectedMarker[];
|
|
210
|
+
tick: boolean;
|
|
211
|
+
} | null>(null);
|
|
212
|
+
if (projRef.current === null) {
|
|
213
|
+
projRef.current = { a: [], b: [], tick: false };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Single per-frame worklet: project all markers, then emit a transform +
|
|
217
|
+
// source rect for each visible atlas marker. Three mappers total regardless
|
|
218
|
+
// of marker count (this + the two thin readers below).
|
|
219
|
+
const atlasData = useDerivedValue(
|
|
220
|
+
() => {
|
|
221
|
+
const ms = markers.get();
|
|
222
|
+
const proj = projRef.current!;
|
|
223
|
+
proj.tick = !proj.tick;
|
|
224
|
+
const buf = proj.tick ? proj.a : proj.b;
|
|
225
|
+
projectMarkers(ms, buf, {
|
|
226
|
+
canvasWidth: engine.canvasWidth.get(),
|
|
227
|
+
canvasHeight: engine.canvasHeight.get(),
|
|
228
|
+
padTop: padding.top,
|
|
229
|
+
padBottom: padding.bottom,
|
|
230
|
+
padLeft: padding.left,
|
|
231
|
+
padRight: padding.right,
|
|
232
|
+
timestamp: engine.timestamp.get(),
|
|
233
|
+
displayWindow: engine.displayWindow.get(),
|
|
234
|
+
displayMin: engine.displayMin.get(),
|
|
235
|
+
displayMax: engine.displayMax.get(),
|
|
236
|
+
series: series?.get(),
|
|
237
|
+
lineData: lineData?.get(),
|
|
238
|
+
});
|
|
239
|
+
const transforms = [];
|
|
240
|
+
const sprites = [];
|
|
241
|
+
for (let i = 0; i < ms.length; i++) {
|
|
242
|
+
const pt = buf[i];
|
|
243
|
+
if (!pt.visible) continue;
|
|
244
|
+
const m = ms[i];
|
|
245
|
+
if (isConnectorMarker(m)) continue;
|
|
246
|
+
const cell = cells[markerAppearanceSig(m)];
|
|
247
|
+
if (!cell) continue;
|
|
248
|
+
// Center the cell on the projected point (RSXform: scale 1, no rotation).
|
|
249
|
+
transforms.push(
|
|
250
|
+
Skia.RSXform(1, 0, pt.x - cell.w / 2, pt.y - cell.h / 2),
|
|
251
|
+
);
|
|
252
|
+
sprites.push(cell.rect);
|
|
253
|
+
}
|
|
254
|
+
return { transforms, sprites };
|
|
255
|
+
},
|
|
256
|
+
[cells, markers, engine, padding, series, lineData],
|
|
257
|
+
);
|
|
258
|
+
const transforms = useDerivedValue(() => atlasData.get().transforms, [atlasData]);
|
|
259
|
+
const sprites = useDerivedValue(() => atlasData.get().sprites, [atlasData]);
|
|
260
|
+
|
|
307
261
|
const axisY = useDerivedValue(
|
|
308
262
|
() => engine.canvasHeight.get() - padding.bottom,
|
|
309
263
|
);
|
|
310
264
|
|
|
311
|
-
const
|
|
265
|
+
const connectors = snapshot.filter(isConnectorMarker);
|
|
312
266
|
|
|
313
267
|
return (
|
|
314
268
|
<Group>
|
|
315
|
-
{
|
|
316
|
-
<
|
|
269
|
+
{atlasImage && (
|
|
270
|
+
<Atlas image={atlasImage} sprites={sprites} transforms={transforms} />
|
|
271
|
+
)}
|
|
272
|
+
{connectors.map((m) => (
|
|
273
|
+
<ConnectorGlyph
|
|
317
274
|
key={m.id}
|
|
318
275
|
marker={m}
|
|
319
276
|
color={m.color ?? defaultMarkerColor(m.kind, palette)}
|
|
320
|
-
bgColor={bgColor}
|
|
321
277
|
engine={engine}
|
|
322
278
|
padding={padding}
|
|
323
279
|
seriesSV={series}
|
|
324
280
|
lineDataSV={lineData}
|
|
325
|
-
font={font}
|
|
326
281
|
axisY={axisY}
|
|
327
282
|
/>
|
|
328
283
|
))}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "@shopify/react-native-skia";
|
|
9
9
|
import { useRef } from "react";
|
|
10
10
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
11
|
-
import {
|
|
11
|
+
import { BADGE_METRICS_DEFAULTS } from "../constants";
|
|
12
12
|
import type { ResolvedGridStyleConfig } from "../core/resolveConfig";
|
|
13
13
|
import type { YAxisEntry } from "../draw/grid";
|
|
14
14
|
import {
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type ChartPadding,
|
|
20
20
|
} from "../draw/line";
|
|
21
21
|
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
22
|
-
import type { LiveChartPalette } from "../types";
|
|
22
|
+
import type { BadgeMetrics, LiveChartPalette } from "../types";
|
|
23
23
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
24
24
|
import { AnimatedLabel } from "./AnimatedLabel";
|
|
25
25
|
|
|
@@ -33,6 +33,7 @@ export function YAxisOverlay({
|
|
|
33
33
|
font,
|
|
34
34
|
badge = false,
|
|
35
35
|
badgeTail = true,
|
|
36
|
+
badgeMetrics = BADGE_METRICS_DEFAULTS,
|
|
36
37
|
seriesLabelInset = 0,
|
|
37
38
|
gridStyle,
|
|
38
39
|
}: {
|
|
@@ -45,6 +46,8 @@ export function YAxisOverlay({
|
|
|
45
46
|
badge?: boolean;
|
|
46
47
|
/** Whether the badge tail spike is shown; affects the left inset used for label alignment. */
|
|
47
48
|
badgeTail?: boolean;
|
|
49
|
+
/** Badge pill geometry tokens (kept in sync with useBadge). */
|
|
50
|
+
badgeMetrics?: BadgeMetrics;
|
|
48
51
|
/** When > 0, series labels occupy the left portion of the gutter; Y-axis labels right-align. */
|
|
49
52
|
seriesLabelInset?: number;
|
|
50
53
|
/** Grid-line styling overrides. Omit for the legacy solid 1px line. */
|
|
@@ -81,7 +84,8 @@ export function YAxisOverlay({
|
|
|
81
84
|
return path;
|
|
82
85
|
});
|
|
83
86
|
|
|
84
|
-
const leftInset =
|
|
87
|
+
const leftInset =
|
|
88
|
+
badgeMetrics.dotGap + badgeTailAndCap(font.getSize(), badgeTail, badgeMetrics);
|
|
85
89
|
|
|
86
90
|
const labelEntries = useDerivedValue(() => {
|
|
87
91
|
const items = entries.get();
|
|
@@ -93,7 +97,7 @@ export function YAxisOverlay({
|
|
|
93
97
|
const e = items[i];
|
|
94
98
|
const textW = measureFontTextWidth(font, e.label);
|
|
95
99
|
const x = badge
|
|
96
|
-
? pillTextLeftX(w, padding.right, leftInset, textW)
|
|
100
|
+
? pillTextLeftX(w, padding.right, leftInset, textW, badgeMetrics)
|
|
97
101
|
: seriesLabelInset > 0
|
|
98
102
|
? gutterRightAlignedTextLeftX(w, textW)
|
|
99
103
|
: gutterCenteredTextLeftX(w, padding.right, textW);
|
package/src/constants.ts
CHANGED
|
@@ -1,19 +1,66 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BadgeMetrics,
|
|
3
|
+
CandleMetrics,
|
|
4
|
+
EmptyStateMetrics,
|
|
5
|
+
GridMetrics,
|
|
6
|
+
MotionMetrics,
|
|
7
|
+
} from "./types";
|
|
8
|
+
|
|
1
9
|
/** Milliseconds per frame at 60 fps — baseline for frame-rate-independent lerp. */
|
|
2
10
|
export const MS_PER_FRAME_60FPS = 16.67;
|
|
3
11
|
|
|
4
|
-
|
|
5
|
-
|
|
12
|
+
// ─── Metric default tokens (single source of truth for LiveChartMetrics) ─────
|
|
13
|
+
// The resolved `metrics` config (see resolveMetrics) is assembled from these
|
|
14
|
+
// objects. Draw/worklet helpers default their metric params to the matching
|
|
15
|
+
// object so call sites that don't thread metrics keep the built-in behavior.
|
|
16
|
+
|
|
17
|
+
/** Default value-badge pill geometry. */
|
|
18
|
+
export const BADGE_METRICS_DEFAULTS: BadgeMetrics = {
|
|
19
|
+
padX: 10,
|
|
20
|
+
padY: 3,
|
|
21
|
+
tailLength: 5,
|
|
22
|
+
marginEdge: 4,
|
|
23
|
+
dotGap: 12,
|
|
24
|
+
tailSpread: 2.5,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Default candlestick body/wick geometry. */
|
|
28
|
+
export const CANDLE_METRICS_DEFAULTS: CandleMetrics = {
|
|
29
|
+
minBodyPx: 1,
|
|
30
|
+
maxBodyPx: 40,
|
|
31
|
+
bodyWidthRatio: 0.8,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Default grid + axis-label fade speeds. */
|
|
35
|
+
export const GRID_METRICS_DEFAULTS: GridMetrics = {
|
|
36
|
+
fadeInSpeed: 0.18,
|
|
37
|
+
fadeOutSpeed: 0.12,
|
|
38
|
+
};
|
|
6
39
|
|
|
40
|
+
/** Default per-frame lerp speeds for value/color transitions. */
|
|
41
|
+
export const MOTION_METRICS_DEFAULTS: MotionMetrics = {
|
|
42
|
+
badgeColorSpeed: 0.08,
|
|
43
|
+
adaptiveSpeedBoost: 0.12,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Default empty-state (no-data) layout. */
|
|
47
|
+
export const EMPTY_STATE_METRICS_DEFAULTS: EmptyStateMetrics = {
|
|
48
|
+
labelOpacity: 0.35,
|
|
49
|
+
gapPad: 20,
|
|
50
|
+
gapFadeWidth: 30,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Back-compat scalar aliases — canonical source is BADGE_METRICS_DEFAULTS above.
|
|
7
54
|
/** Horizontal padding inside the badge pill, on each side of the label. */
|
|
8
|
-
export const BADGE_PILL_PAD_X =
|
|
55
|
+
export const BADGE_PILL_PAD_X = BADGE_METRICS_DEFAULTS.padX;
|
|
9
56
|
/** Vertical padding above and below the label inside the badge pill. */
|
|
10
|
-
export const BADGE_PILL_PAD_Y =
|
|
57
|
+
export const BADGE_PILL_PAD_Y = BADGE_METRICS_DEFAULTS.padY;
|
|
11
58
|
/** Length of the badge tail (the pointed spike toward the dot). */
|
|
12
|
-
export const BADGE_TAIL_LEN =
|
|
59
|
+
export const BADGE_TAIL_LEN = BADGE_METRICS_DEFAULTS.tailLength;
|
|
13
60
|
/** Gap between the pill's right edge and the canvas right edge. */
|
|
14
|
-
export const BADGE_MARGIN_RIGHT =
|
|
61
|
+
export const BADGE_MARGIN_RIGHT = BADGE_METRICS_DEFAULTS.marginEdge;
|
|
15
62
|
/** Gap between the live dot and the badge tail tip. */
|
|
16
|
-
export const BADGE_DOT_GAP =
|
|
63
|
+
export const BADGE_DOT_GAP = BADGE_METRICS_DEFAULTS.dotGap;
|
|
17
64
|
|
|
18
65
|
/** Maximum simultaneous series rendered (paths/dots slots). */
|
|
19
66
|
export const MAX_MULTI_SERIES = 12;
|
|
@@ -21,13 +68,6 @@ export const MAX_MULTI_SERIES = 12;
|
|
|
21
68
|
/** Default width (px) of the left-edge fade band */
|
|
22
69
|
export const FADE_EDGE_WIDTH = 40;
|
|
23
70
|
|
|
24
|
-
/** Empty-state label opacity. */
|
|
25
|
-
export const EMPTY_STATE_LABEL_ALPHA = 0.35;
|
|
26
|
-
/** Half-padding (px) around empty text for the squiggle “gap” erase band. */
|
|
27
|
-
export const EMPTY_TEXT_GAP_PAD = 20;
|
|
28
|
-
/** Horizontal fade width (px) on each side of the empty-text gap. */
|
|
29
|
-
export const EMPTY_GAP_FADE_WIDTH = 30;
|
|
30
|
-
|
|
31
71
|
/**
|
|
32
72
|
* Floats per degen particle slot: `x, y, vx, vy, t0, active, size, colorIndex`.
|
|
33
73
|
* `colorIndex` selects which entry of the renderer's color list a particle uses
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CandlePoint, LiveChartPoint } from "../types";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { MOTION_METRICS_DEFAULTS } from "../constants";
|
|
4
4
|
import { lerp } from "../math/lerp";
|
|
5
5
|
|
|
6
6
|
export interface EngineTickMutable {
|
|
@@ -18,6 +18,8 @@ export interface EngineTickInput {
|
|
|
18
18
|
timeWindow: number;
|
|
19
19
|
smoothing: number;
|
|
20
20
|
exaggerate: boolean;
|
|
21
|
+
/** Extra catch-up speed added to `smoothing` when the live value lags. Default `0.12`. */
|
|
22
|
+
adaptiveSpeedBoost?: number;
|
|
21
23
|
referenceValue: number | undefined;
|
|
22
24
|
/** Additional reference values (lines + bands) folded into the Y range. */
|
|
23
25
|
referenceValues?: number[];
|
|
@@ -68,7 +70,10 @@ export function tickLiveChartEngineFrame(
|
|
|
68
70
|
const range = state.displayMax - state.displayMin;
|
|
69
71
|
const gapRatio =
|
|
70
72
|
range > 0 ? Math.min(Math.abs(target - state.displayValue) / range, 1) : 0;
|
|
71
|
-
const adaptiveSpeed =
|
|
73
|
+
const adaptiveSpeed =
|
|
74
|
+
speed +
|
|
75
|
+
(1 - gapRatio) *
|
|
76
|
+
(input.adaptiveSpeedBoost ?? MOTION_METRICS_DEFAULTS.adaptiveSpeedBoost);
|
|
72
77
|
|
|
73
78
|
state.displayValue = lerp(
|
|
74
79
|
state.displayValue,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MOTION_METRICS_DEFAULTS } from "../constants";
|
|
2
2
|
import { lerp } from "../math/lerp";
|
|
3
3
|
import type { SeriesConfig } from "../types";
|
|
4
4
|
|
|
@@ -18,6 +18,8 @@ export interface MultiEngineTickInput {
|
|
|
18
18
|
timeWindow: number;
|
|
19
19
|
smoothing: number;
|
|
20
20
|
exaggerate: boolean;
|
|
21
|
+
/** Extra catch-up speed added to `smoothing` when a series tip lags. Default `0.12`. */
|
|
22
|
+
adaptiveSpeedBoost?: number;
|
|
21
23
|
referenceValue: number | undefined;
|
|
22
24
|
/** Additional reference values (lines + bands) folded into the Y range. */
|
|
23
25
|
referenceValues?: number[];
|
|
@@ -80,7 +82,11 @@ export function tickLiveChartSeriesEngineFrame(
|
|
|
80
82
|
const cur = state.displayValues[i];
|
|
81
83
|
const gapRatio =
|
|
82
84
|
range > 0 ? Math.min(Math.abs(target - cur) / range, 1) : 0;
|
|
83
|
-
const adaptiveSpeed =
|
|
85
|
+
const adaptiveSpeed =
|
|
86
|
+
speed +
|
|
87
|
+
(1 - gapRatio) *
|
|
88
|
+
(input.adaptiveSpeedBoost ??
|
|
89
|
+
MOTION_METRICS_DEFAULTS.adaptiveSpeedBoost);
|
|
84
90
|
state.displayValues[i] = lerp(cur, target, adaptiveSpeed, input.dt);
|
|
85
91
|
|
|
86
92
|
const targetOp = series[i].visible !== false ? 1 : 0;
|