react-native-livechart 4.10.0 → 4.12.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 +24 -0
- package/dist/components/CrosshairLine.d.ts +5 -1
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +3 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +19 -12
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +3 -5
- package/dist/components/DotOverlay.d.ts.map +1 -1
- package/dist/components/LeftEdgeFade.d.ts +3 -1
- package/dist/components/LeftEdgeFade.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 +3 -1
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +3 -1
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -1
- package/dist/components/MultiSeriesValueLabels.d.ts +3 -1
- package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -1
- package/dist/components/MultiSeriesValueLines.d.ts +3 -1
- package/dist/components/MultiSeriesValueLines.d.ts.map +1 -1
- package/dist/components/PerSeriesTooltipOverlay.d.ts +19 -0
- package/dist/components/PerSeriesTooltipOverlay.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +20 -6
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ThresholdSplitShader.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +3 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/components/thresholdSplitShaderSource.d.ts +13 -0
- package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -0
- package/dist/core/liveIndicatorVisibility.d.ts +9 -0
- package/dist/core/liveIndicatorVisibility.d.ts.map +1 -0
- package/dist/core/resolveConfig.d.ts +29 -0
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +9 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +10 -5
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/particleAtlas.d.ts +3 -2
- package/dist/draw/particleAtlas.d.ts.map +1 -1
- package/dist/hooks/crosshairSeries.d.ts +10 -2
- package/dist/hooks/crosshairSeries.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +37 -2
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/delayedPanGuard.d.ts +66 -8
- package/dist/hooks/delayedPanGuard.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +5 -9
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +7 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts +22 -3
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useMomentum.d.ts +1 -1
- package/dist/hooks/useMomentum.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +4 -4
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +15 -1
- package/dist/hooks/usePanScroll.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/lerp.d.ts.map +1 -1
- package/dist/math/momentum.d.ts +1 -1
- package/dist/math/momentum.d.ts.map +1 -1
- package/dist/math/referenceLines.d.ts +7 -0
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/math/squiggly.d.ts +6 -3
- package/dist/math/squiggly.d.ts.map +1 -1
- package/dist/math/threshold.d.ts +2 -2
- package/dist/types.d.ts +184 -16
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairLine.tsx +21 -1
- package/src/components/CrosshairOverlay.tsx +48 -35
- package/src/components/CustomReferenceLineOverlay.tsx +58 -20
- package/src/components/DegenParticlesOverlay.tsx +40 -5
- package/src/components/DotOverlay.tsx +24 -8
- package/src/components/LeftEdgeFade.tsx +12 -6
- package/src/components/LiveChart.tsx +186 -53
- package/src/components/LiveChartSeries.tsx +214 -29
- package/src/components/LoadingOverlay.tsx +26 -12
- package/src/components/MarkerOverlay.tsx +20 -3
- package/src/components/MultiSeriesDots.tsx +4 -2
- package/src/components/MultiSeriesTooltipStack.tsx +4 -2
- package/src/components/MultiSeriesValueLabels.tsx +4 -2
- package/src/components/MultiSeriesValueLines.tsx +4 -2
- package/src/components/PerSeriesTooltipOverlay.tsx +287 -0
- package/src/components/ReferenceLineOverlay.tsx +274 -141
- package/src/components/ThresholdSplitShader.tsx +5 -37
- package/src/components/YAxisOverlay.tsx +5 -1
- package/src/components/thresholdSplitShaderSource.ts +70 -0
- package/src/core/liveIndicatorVisibility.ts +25 -0
- package/src/core/resolveConfig.ts +86 -0
- package/src/core/useLiveChartEngine.ts +110 -42
- package/src/core/useLiveChartSeriesEngine.ts +137 -70
- package/src/draw/particleAtlas.ts +21 -10
- package/src/hooks/crosshairSeries.ts +262 -33
- package/src/hooks/crosshairShared.ts +41 -2
- package/src/hooks/delayedPanGuard.ts +111 -6
- package/src/hooks/useChartPaths.ts +36 -15
- package/src/hooks/useCrosshair.ts +73 -14
- package/src/hooks/useCrosshairSeries.ts +106 -13
- package/src/hooks/useMomentum.ts +8 -1
- package/src/hooks/useMultiSeriesLinePaths.ts +15 -9
- package/src/hooks/usePanScroll.ts +46 -5
- package/src/index.ts +2 -0
- package/src/math/lerp.ts +7 -0
- package/src/math/momentum.ts +22 -5
- package/src/math/referenceLines.ts +38 -0
- package/src/math/squiggly.ts +28 -11
- package/src/math/threshold.ts +2 -2
- package/src/types.ts +197 -17
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a balanced constant-index lookup for the threshold sample array.
|
|
3
|
+
*
|
|
4
|
+
* SkSL does not allow a runtime value to index a uniform array. A linear loop
|
|
5
|
+
* works around that restriction, but makes every covered fragment visit every
|
|
6
|
+
* segment. This tree keeps every array index constant while selecting the same
|
|
7
|
+
* segment in at most `ceil(log2(sampleCount - 1))` comparisons (six for the
|
|
8
|
+
* production 64-sample shader).
|
|
9
|
+
*/
|
|
10
|
+
export function buildThresholdLookupTree(sampleCount: number): string {
|
|
11
|
+
if (!Number.isInteger(sampleCount) || sampleCount < 2) {
|
|
12
|
+
throw new Error("Threshold shader needs at least two samples");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const build = (first: number, end: number, indent: string): string => {
|
|
16
|
+
if (end - first === 1) {
|
|
17
|
+
return `${indent}return mix(samples[${first}], samples[${first + 1}], clamp(u - float(${first}), 0.0, 1.0));`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const middle = Math.floor((first + end) / 2);
|
|
21
|
+
const childIndent = `${indent} `;
|
|
22
|
+
return `${indent}if (u < float(${middle})) {
|
|
23
|
+
${build(first, middle, childIndent)}
|
|
24
|
+
${indent}} else {
|
|
25
|
+
${build(middle, end, childIndent)}
|
|
26
|
+
${indent}}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return build(0, sampleCount - 1, " ");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Build the complete time-varying threshold split shader. */
|
|
33
|
+
export function buildThresholdSplitShaderSource(sampleCount: number): string {
|
|
34
|
+
const lookup = buildThresholdLookupTree(sampleCount);
|
|
35
|
+
return `
|
|
36
|
+
// Pixel-X span of the sample grid (thresholdSampleSpanX) — overhangs the plot
|
|
37
|
+
// and glides with the window so features move fluidly, frame to frame.
|
|
38
|
+
uniform float sampleLeft;
|
|
39
|
+
uniform float sampleRight;
|
|
40
|
+
// X where the threshold ends (extendToNow: false → the last point's pixel-X,
|
|
41
|
+
// else a huge sentinel). Fragments right of it paint restColor: the plain line
|
|
42
|
+
// color for the stroke, transparent for the band.
|
|
43
|
+
uniform float clipRight;
|
|
44
|
+
uniform vec4 aboveColor; // straight-alpha rgba, 0..1
|
|
45
|
+
uniform vec4 belowColor; // straight-alpha rgba, 0..1
|
|
46
|
+
uniform vec4 restColor; // straight-alpha rgba, 0..1
|
|
47
|
+
uniform float samples[${sampleCount}];
|
|
48
|
+
|
|
49
|
+
float thresholdAt(float u) {
|
|
50
|
+
${lookup}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
half4 main(vec2 xy) {
|
|
54
|
+
if (xy.x > clipRight) {
|
|
55
|
+
return half4(half3(restColor.rgb) * restColor.a, restColor.a);
|
|
56
|
+
}
|
|
57
|
+
float span = sampleRight - sampleLeft;
|
|
58
|
+
float u = span > 0.0 ? (xy.x - sampleLeft) / span : 0.0;
|
|
59
|
+
u = clamp(u, 0.0, 1.0) * float(${sampleCount - 1});
|
|
60
|
+
// Uniform-array indices must stay compile-time constants in SkSL. The
|
|
61
|
+
// generated branch tree selects the same segment as floor(u), including the
|
|
62
|
+
// clamped final endpoint, in logarithmic comparisons instead of walking every
|
|
63
|
+
// segment for every fragment.
|
|
64
|
+
float thrY = thresholdAt(u);
|
|
65
|
+
// 1 above the boundary, 0 below, with ~1px antialiasing across it.
|
|
66
|
+
float above = clamp(thrY - xy.y + 0.5, 0.0, 1.0);
|
|
67
|
+
vec4 c = mix(belowColor, aboveColor, above);
|
|
68
|
+
return half4(half3(c.rgb) * c.a, c.a);
|
|
69
|
+
}`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TimeScrollConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve whether historical time-scroll views should suppress indicators that
|
|
5
|
+
* still point at the live price.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveHideLiveOnScrollBack(
|
|
8
|
+
timeScroll: boolean | TimeScrollConfig | undefined,
|
|
9
|
+
followViewEdge: boolean,
|
|
10
|
+
): boolean {
|
|
11
|
+
const hide =
|
|
12
|
+
typeof timeScroll === "object"
|
|
13
|
+
? (timeScroll.hideLiveOnScrollBack ?? true)
|
|
14
|
+
: true;
|
|
15
|
+
return hide && !followViewEdge;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Worklet-safe opacity gate shared by the live badge, dot, and dashed value line. */
|
|
19
|
+
export function liveIndicatorScrollOpacity(
|
|
20
|
+
hideLiveOnScrollBack: boolean,
|
|
21
|
+
viewEnd: number | null,
|
|
22
|
+
): 0 | 1 {
|
|
23
|
+
"worklet";
|
|
24
|
+
return hideLiveOnScrollBack && viewEnd != null ? 0 : 1;
|
|
25
|
+
}
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
ReferenceLine,
|
|
25
25
|
ScrubActionConfig,
|
|
26
26
|
ScrubConfig,
|
|
27
|
+
PerSeriesTooltipConfig,
|
|
27
28
|
SelectionDotConfig,
|
|
28
29
|
SelectionDotProps,
|
|
29
30
|
SelectionDotRingConfig,
|
|
@@ -144,6 +145,8 @@ export interface ResolvedXAxisConfig {
|
|
|
144
145
|
|
|
145
146
|
export interface ResolvedScrubConfig {
|
|
146
147
|
tooltip: boolean;
|
|
148
|
+
/** Opt-in per-series pill tooltip for LiveChartSeries; null keeps guide-only behavior. */
|
|
149
|
+
seriesTooltip: ResolvedPerSeriesTooltipConfig | null;
|
|
147
150
|
/** Opacity of content right of the crosshair while scrubbing (dstOut fade). */
|
|
148
151
|
dimOpacity: number;
|
|
149
152
|
/** undefined → palette.crosshairLine */
|
|
@@ -174,6 +177,36 @@ export interface ResolvedScrubConfig {
|
|
|
174
177
|
hideOverlaysOnScrub: boolean;
|
|
175
178
|
}
|
|
176
179
|
|
|
180
|
+
export interface ResolvedPerSeriesTooltipConfig {
|
|
181
|
+
alwaysShow: boolean;
|
|
182
|
+
bucketSeconds: number | undefined;
|
|
183
|
+
formatSeriesValue:
|
|
184
|
+
| ((value: number, seriesId: string) => string)
|
|
185
|
+
| undefined;
|
|
186
|
+
formatTimeRange: ((from: number, to: number) => string) | undefined;
|
|
187
|
+
maxLabelChars: number;
|
|
188
|
+
guideColor: string | undefined;
|
|
189
|
+
guideWidth: number;
|
|
190
|
+
guideDashPattern: number[] | undefined;
|
|
191
|
+
timePillBackground: string | undefined;
|
|
192
|
+
timePillColor: string | undefined;
|
|
193
|
+
timePillBorderColor: string | undefined;
|
|
194
|
+
timePillRadius: number;
|
|
195
|
+
timePillPaddingX: number;
|
|
196
|
+
timePillPaddingY: number;
|
|
197
|
+
seriesPillBackground: string | undefined;
|
|
198
|
+
seriesPillLabelColor: string | undefined;
|
|
199
|
+
seriesPillValueColor: string | undefined;
|
|
200
|
+
seriesPillBorderColor: string | undefined;
|
|
201
|
+
seriesPillRadius: number;
|
|
202
|
+
seriesPillPaddingX: number;
|
|
203
|
+
seriesPillPaddingY: number;
|
|
204
|
+
seriesPillDotSize: number;
|
|
205
|
+
seriesPillDotGap: number;
|
|
206
|
+
seriesPillLabelValueGap: number;
|
|
207
|
+
intersectionDotSize: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
177
210
|
export interface ResolvedScrubActionConfig {
|
|
178
211
|
/** Glyph drawn in the action badge. */
|
|
179
212
|
icon: string;
|
|
@@ -628,6 +661,7 @@ export function resolveXAxis(
|
|
|
628
661
|
|
|
629
662
|
const SCRUB_DEFAULTS: ResolvedScrubConfig = {
|
|
630
663
|
tooltip: true,
|
|
664
|
+
seriesTooltip: null,
|
|
631
665
|
dimOpacity: 0.3,
|
|
632
666
|
crosshairLineColor: undefined,
|
|
633
667
|
crosshairDash: undefined,
|
|
@@ -644,6 +678,55 @@ const SCRUB_DEFAULTS: ResolvedScrubConfig = {
|
|
|
644
678
|
hideOverlaysOnScrub: false,
|
|
645
679
|
};
|
|
646
680
|
|
|
681
|
+
const PER_SERIES_TOOLTIP_DEFAULTS: ResolvedPerSeriesTooltipConfig = {
|
|
682
|
+
alwaysShow: false,
|
|
683
|
+
bucketSeconds: undefined,
|
|
684
|
+
formatSeriesValue: undefined,
|
|
685
|
+
formatTimeRange: undefined,
|
|
686
|
+
maxLabelChars: 14,
|
|
687
|
+
guideColor: undefined,
|
|
688
|
+
guideWidth: 1,
|
|
689
|
+
guideDashPattern: [3, 3],
|
|
690
|
+
timePillBackground: undefined,
|
|
691
|
+
timePillColor: undefined,
|
|
692
|
+
timePillBorderColor: undefined,
|
|
693
|
+
timePillRadius: 6,
|
|
694
|
+
timePillPaddingX: 8,
|
|
695
|
+
timePillPaddingY: 4,
|
|
696
|
+
seriesPillBackground: undefined,
|
|
697
|
+
seriesPillLabelColor: undefined,
|
|
698
|
+
seriesPillValueColor: undefined,
|
|
699
|
+
seriesPillBorderColor: undefined,
|
|
700
|
+
seriesPillRadius: 6,
|
|
701
|
+
seriesPillPaddingX: 8,
|
|
702
|
+
seriesPillPaddingY: 4,
|
|
703
|
+
seriesPillDotSize: 8,
|
|
704
|
+
seriesPillDotGap: 6,
|
|
705
|
+
seriesPillLabelValueGap: 6,
|
|
706
|
+
intersectionDotSize: 8,
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
function resolvePerSeriesTooltip(
|
|
710
|
+
prop: boolean | PerSeriesTooltipConfig | undefined,
|
|
711
|
+
): ResolvedPerSeriesTooltipConfig | null {
|
|
712
|
+
const resolved = resolveToggle(
|
|
713
|
+
prop,
|
|
714
|
+
PER_SERIES_TOOLTIP_DEFAULTS,
|
|
715
|
+
false,
|
|
716
|
+
);
|
|
717
|
+
if (!resolved) return null;
|
|
718
|
+
const dash = typeof prop === "object" ? prop.guideDashPattern : undefined;
|
|
719
|
+
if (dash !== undefined) {
|
|
720
|
+
resolved.guideDashPattern = dash === true ? [3, 3] : dash || undefined;
|
|
721
|
+
}
|
|
722
|
+
resolved.maxLabelChars = Math.max(1, Math.floor(resolved.maxLabelChars));
|
|
723
|
+
resolved.bucketSeconds =
|
|
724
|
+
resolved.bucketSeconds !== undefined && resolved.bucketSeconds > 0
|
|
725
|
+
? resolved.bucketSeconds
|
|
726
|
+
: undefined;
|
|
727
|
+
return resolved;
|
|
728
|
+
}
|
|
729
|
+
|
|
647
730
|
/**
|
|
648
731
|
* Resolves `scrub` prop to a fully-typed config or null (disabled).
|
|
649
732
|
* `true` → defaults, object → merged with defaults, falsy → null.
|
|
@@ -657,6 +740,9 @@ export function resolveScrub(
|
|
|
657
740
|
// through, anything falsy → solid (undefined).
|
|
658
741
|
const dash = typeof prop === "object" ? prop.crosshairDash : undefined;
|
|
659
742
|
resolved.crosshairDash = dash === true ? [4, 4] : dash || undefined;
|
|
743
|
+
const seriesTooltip =
|
|
744
|
+
typeof prop === "object" ? prop.seriesTooltip : undefined;
|
|
745
|
+
resolved.seriesTooltip = resolvePerSeriesTooltip(seriesTooltip);
|
|
660
746
|
}
|
|
661
747
|
return resolved;
|
|
662
748
|
}
|
|
@@ -18,7 +18,11 @@ import {
|
|
|
18
18
|
} from "react-native-reanimated";
|
|
19
19
|
import { MS_PER_FRAME_60FPS, RETURN_TO_LIVE_MS } from "../constants";
|
|
20
20
|
import type { CandlePoint, LiveChartPoint, SeriesConfig } from "../types";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
tickLiveChartEngineFrame,
|
|
23
|
+
type EngineTickInput,
|
|
24
|
+
type EngineTickMutable,
|
|
25
|
+
} from "./liveChartEngineTick";
|
|
22
26
|
|
|
23
27
|
export interface EngineConfig {
|
|
24
28
|
data: SharedValue<LiveChartPoint[]>;
|
|
@@ -229,6 +233,42 @@ export interface EngineFrameRefs {
|
|
|
229
233
|
extremaMaxTime: SharedValue<number>;
|
|
230
234
|
}
|
|
231
235
|
|
|
236
|
+
/** Stable state/input containers reused by the UI-thread frame callback. */
|
|
237
|
+
export interface EngineFrameScratch {
|
|
238
|
+
state: EngineTickMutable;
|
|
239
|
+
input: EngineTickInput;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Allocate one single-series frame scratch. Call once per engine instance. */
|
|
243
|
+
export function makeEngineFrameScratch(): EngineFrameScratch {
|
|
244
|
+
return {
|
|
245
|
+
state: {
|
|
246
|
+
displayValue: 0,
|
|
247
|
+
displayMin: 0,
|
|
248
|
+
displayMax: 1,
|
|
249
|
+
displayWindow: 0,
|
|
250
|
+
timestamp: 0,
|
|
251
|
+
liveEdge: 0,
|
|
252
|
+
edgeValue: 0,
|
|
253
|
+
extremaMinValue: NaN,
|
|
254
|
+
extremaMaxValue: NaN,
|
|
255
|
+
extremaMinTime: NaN,
|
|
256
|
+
extremaMaxTime: NaN,
|
|
257
|
+
},
|
|
258
|
+
input: {
|
|
259
|
+
dt: MS_PER_FRAME_60FPS,
|
|
260
|
+
canvasWidth: 0,
|
|
261
|
+
canvasHeight: 0,
|
|
262
|
+
timeWindow: 0,
|
|
263
|
+
smoothing: 0,
|
|
264
|
+
exaggerate: false,
|
|
265
|
+
referenceValue: undefined,
|
|
266
|
+
targetValue: 0,
|
|
267
|
+
points: [],
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
232
272
|
/**
|
|
233
273
|
* Shared between the `useFrameCallback` worklet and unit tests.
|
|
234
274
|
* Mutates shared values from a snapshot tick (`tickLiveChartEngineFrame`).
|
|
@@ -236,54 +276,77 @@ export interface EngineFrameRefs {
|
|
|
236
276
|
export function applyLiveChartEngineFrame(
|
|
237
277
|
frameInfo: { timeSincePreviousFrame?: number | null },
|
|
238
278
|
sv: EngineFrameRefs,
|
|
279
|
+
scratch?: EngineFrameScratch,
|
|
239
280
|
): void {
|
|
240
281
|
"worklet";
|
|
241
282
|
const dt = frameInfo.timeSincePreviousFrame ?? MS_PER_FRAME_60FPS;
|
|
242
283
|
// One-shot settle: a `snapKey` change set this flag; consume it for this tick
|
|
243
284
|
// and clear it below so only this frame snaps (live ticks stay smoothed).
|
|
244
285
|
const snap = sv.snapSV?.value ?? false;
|
|
245
|
-
const state = {
|
|
246
|
-
displayValue:
|
|
247
|
-
displayMin:
|
|
248
|
-
displayMax:
|
|
249
|
-
displayWindow:
|
|
250
|
-
timestamp:
|
|
251
|
-
liveEdge:
|
|
252
|
-
edgeValue:
|
|
253
|
-
extremaMinValue:
|
|
254
|
-
extremaMaxValue:
|
|
255
|
-
extremaMinTime:
|
|
256
|
-
extremaMaxTime:
|
|
286
|
+
const state: EngineTickMutable = scratch?.state ?? {
|
|
287
|
+
displayValue: 0,
|
|
288
|
+
displayMin: 0,
|
|
289
|
+
displayMax: 1,
|
|
290
|
+
displayWindow: 0,
|
|
291
|
+
timestamp: 0,
|
|
292
|
+
liveEdge: 0,
|
|
293
|
+
edgeValue: 0,
|
|
294
|
+
extremaMinValue: NaN,
|
|
295
|
+
extremaMaxValue: NaN,
|
|
296
|
+
extremaMinTime: NaN,
|
|
297
|
+
extremaMaxTime: NaN,
|
|
257
298
|
};
|
|
258
|
-
|
|
299
|
+
state.displayValue = sv.displayValue.value;
|
|
300
|
+
state.displayMin = sv.displayMin.value;
|
|
301
|
+
state.displayMax = sv.displayMax.value;
|
|
302
|
+
state.displayWindow = sv.displayWindow.value;
|
|
303
|
+
state.timestamp = sv.timestamp.value;
|
|
304
|
+
state.liveEdge = sv.liveEdgeSV?.value ?? 0;
|
|
305
|
+
state.edgeValue = sv.edgeValueSV?.value ?? 0;
|
|
306
|
+
state.extremaMinValue = sv.extremaMinValue.value;
|
|
307
|
+
state.extremaMaxValue = sv.extremaMaxValue.value;
|
|
308
|
+
state.extremaMinTime = sv.extremaMinTime.value;
|
|
309
|
+
state.extremaMaxTime = sv.extremaMaxTime.value;
|
|
310
|
+
|
|
311
|
+
const input: EngineTickInput = scratch?.input ?? {
|
|
259
312
|
dt,
|
|
260
|
-
canvasWidth:
|
|
261
|
-
canvasHeight:
|
|
262
|
-
timeWindow:
|
|
263
|
-
smoothing:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
313
|
+
canvasWidth: 0,
|
|
314
|
+
canvasHeight: 0,
|
|
315
|
+
timeWindow: 0,
|
|
316
|
+
smoothing: 0,
|
|
317
|
+
exaggerate: false,
|
|
318
|
+
referenceValue: undefined,
|
|
319
|
+
targetValue: 0,
|
|
320
|
+
points: [],
|
|
321
|
+
};
|
|
322
|
+
input.dt = dt;
|
|
323
|
+
input.canvasWidth = sv.canvasWidth.value;
|
|
324
|
+
input.canvasHeight = sv.canvasHeight.value;
|
|
325
|
+
input.timeWindow = sv.timeWindow.value;
|
|
326
|
+
input.smoothing = sv.smoothing.value;
|
|
327
|
+
input.adaptiveSpeedBoost = sv.adaptiveSpeedBoostSV?.value;
|
|
328
|
+
input.exaggerate = sv.exaggerateSV.value;
|
|
329
|
+
input.referenceValue = sv.referenceValue.value;
|
|
330
|
+
input.referenceValues = sv.referenceValues?.value;
|
|
331
|
+
input.thresholdRangePoints = sv.thresholdRangePoints?.value;
|
|
332
|
+
input.thresholdRangeExtendToNow = sv.thresholdRangeExtendToNow?.value ?? true;
|
|
333
|
+
input.nonNegative = sv.nonNegativeSV?.value ?? false;
|
|
334
|
+
input.maxValue = sv.maxValueSV?.value;
|
|
335
|
+
input.nowOverride = sv.nowOverrideSV?.value;
|
|
336
|
+
input.windowBuffer = sv.windowBufferSV?.value ?? 0;
|
|
337
|
+
input.targetValue = sv.value.value;
|
|
338
|
+
input.points = sv.data.value;
|
|
339
|
+
input.nowSeconds = Date.now() / 1000;
|
|
340
|
+
input.paused = sv.pausedSV.value;
|
|
341
|
+
input.snap = snap;
|
|
342
|
+
input.viewEnd = sv.viewEndSV?.value;
|
|
343
|
+
input.returnT = sv.returnTSV?.value;
|
|
344
|
+
input.returnFrom = sv.returnFromSV?.value;
|
|
345
|
+
input.viewWindow = sv.viewWindowSV?.value;
|
|
346
|
+
input.mode = sv.modeSV.value;
|
|
347
|
+
input.candles = sv.candles?.value;
|
|
348
|
+
input.liveCandle = sv.liveCandle?.value;
|
|
349
|
+
tickLiveChartEngineFrame(state, input);
|
|
287
350
|
sv.displayValue.value = state.displayValue;
|
|
288
351
|
sv.displayMin.value = state.displayMin;
|
|
289
352
|
sv.displayMax.value = state.displayMax;
|
|
@@ -467,12 +530,16 @@ export function useLiveChartEngine(
|
|
|
467
530
|
extremaMinTime,
|
|
468
531
|
extremaMaxTime,
|
|
469
532
|
};
|
|
533
|
+
const frameScratchRef = useRef<EngineFrameScratch | null>(null);
|
|
534
|
+
if (frameScratchRef.current === null) {
|
|
535
|
+
frameScratchRef.current = makeEngineFrameScratch();
|
|
536
|
+
}
|
|
470
537
|
|
|
471
538
|
// `autostart=false` registers the frame callback without running it — the live
|
|
472
539
|
// loop is fully inert in static mode (the invariant that makes this worth it).
|
|
473
540
|
useFrameCallback((frameInfo) => {
|
|
474
541
|
"worklet";
|
|
475
|
-
applyLiveChartEngineFrame(frameInfo, frameRefs);
|
|
542
|
+
applyLiveChartEngineFrame(frameInfo, frameRefs, frameScratchRef.current!);
|
|
476
543
|
}, !config.static);
|
|
477
544
|
|
|
478
545
|
// When time-scroll is disabled while scrolled back, return the window to the
|
|
@@ -532,6 +599,7 @@ export function useLiveChartEngine(
|
|
|
532
599
|
applyLiveChartEngineFrame(
|
|
533
600
|
{ timeSincePreviousFrame: MS_PER_FRAME_60FPS },
|
|
534
601
|
frameRefs,
|
|
602
|
+
frameScratchRef.current!,
|
|
535
603
|
);
|
|
536
604
|
},
|
|
537
605
|
);
|
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
} from "react-native-reanimated";
|
|
12
12
|
import { MS_PER_FRAME_60FPS, RETURN_TO_LIVE_MS } from "../constants";
|
|
13
13
|
import type { LiveChartPoint, SeriesConfig } from "../types";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
tickLiveChartSeriesEngineFrame,
|
|
16
|
+
type MultiEngineTickInput,
|
|
17
|
+
type MultiEngineTickMutable,
|
|
18
|
+
} from "./liveChartSeriesEngineTick";
|
|
15
19
|
import type { ChartEngineScroll, MultiEngineState } from "./useLiveChartEngine";
|
|
16
20
|
|
|
17
21
|
export interface MultiSeriesEngineConfig {
|
|
@@ -94,10 +98,10 @@ export interface MultiEngineFrameRefs {
|
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
/**
|
|
97
|
-
* Reusable per-frame scratch for {@link applyLiveChartSeriesEngineFrame}. The
|
|
101
|
+
* Reusable per-frame scratch for {@link applyLiveChartSeriesEngineFrame}. The
|
|
98
102
|
* output arrays ping-pong so the assigned reference still changes each frame
|
|
99
|
-
* (Reanimated propagates on reference change)
|
|
100
|
-
*
|
|
103
|
+
* (Reanimated propagates on reference change), while the state/input containers
|
|
104
|
+
* are mutated in place. Create one per engine.
|
|
101
105
|
*/
|
|
102
106
|
export interface MultiSeriesEngineScratch {
|
|
103
107
|
dvA: number[];
|
|
@@ -105,6 +109,46 @@ export interface MultiSeriesEngineScratch {
|
|
|
105
109
|
opA: number[];
|
|
106
110
|
opB: number[];
|
|
107
111
|
tick: boolean;
|
|
112
|
+
state: MultiEngineTickMutable;
|
|
113
|
+
input: MultiEngineTickInput;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Allocate one multi-series frame scratch. Call once per engine instance. */
|
|
117
|
+
export function makeMultiSeriesEngineScratch(): MultiSeriesEngineScratch {
|
|
118
|
+
const dvA: number[] = [];
|
|
119
|
+
const dvB: number[] = [];
|
|
120
|
+
const opA: number[] = [];
|
|
121
|
+
const opB: number[] = [];
|
|
122
|
+
return {
|
|
123
|
+
dvA,
|
|
124
|
+
dvB,
|
|
125
|
+
opA,
|
|
126
|
+
opB,
|
|
127
|
+
tick: false,
|
|
128
|
+
state: {
|
|
129
|
+
displayMin: 0,
|
|
130
|
+
displayMax: 1,
|
|
131
|
+
displayWindow: 0,
|
|
132
|
+
timestamp: 0,
|
|
133
|
+
liveEdge: 0,
|
|
134
|
+
displayValues: dvA,
|
|
135
|
+
opacities: opA,
|
|
136
|
+
extremaMinValue: NaN,
|
|
137
|
+
extremaMaxValue: NaN,
|
|
138
|
+
extremaMinTime: NaN,
|
|
139
|
+
extremaMaxTime: NaN,
|
|
140
|
+
},
|
|
141
|
+
input: {
|
|
142
|
+
dt: MS_PER_FRAME_60FPS,
|
|
143
|
+
canvasWidth: 0,
|
|
144
|
+
canvasHeight: 0,
|
|
145
|
+
timeWindow: 0,
|
|
146
|
+
smoothing: 0,
|
|
147
|
+
exaggerate: false,
|
|
148
|
+
referenceValue: undefined,
|
|
149
|
+
series: [],
|
|
150
|
+
},
|
|
151
|
+
};
|
|
108
152
|
}
|
|
109
153
|
|
|
110
154
|
/**
|
|
@@ -113,8 +157,8 @@ export interface MultiSeriesEngineScratch {
|
|
|
113
157
|
* to lerp per-series display values and opacities. Extracted as a
|
|
114
158
|
* pure function so it can be called from both `useFrameCallback` and tests.
|
|
115
159
|
*
|
|
116
|
-
* Pass `scratch` to reuse the output arrays
|
|
117
|
-
*
|
|
160
|
+
* Pass `scratch` to reuse the output arrays plus state/input containers across
|
|
161
|
+
* frames; omit it (tests) to retain the standalone allocation fallback.
|
|
118
162
|
*/
|
|
119
163
|
export function applyLiveChartSeriesEngineFrame(
|
|
120
164
|
frameInfo: { timeSincePreviousFrame?: number | null },
|
|
@@ -143,42 +187,63 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
143
187
|
displayValues = curDv.slice();
|
|
144
188
|
opacities = curOp.slice();
|
|
145
189
|
}
|
|
146
|
-
const state = {
|
|
147
|
-
displayMin:
|
|
148
|
-
displayMax:
|
|
149
|
-
displayWindow:
|
|
150
|
-
timestamp:
|
|
151
|
-
liveEdge:
|
|
190
|
+
const state: MultiEngineTickMutable = scratch?.state ?? {
|
|
191
|
+
displayMin: 0,
|
|
192
|
+
displayMax: 1,
|
|
193
|
+
displayWindow: 0,
|
|
194
|
+
timestamp: 0,
|
|
195
|
+
liveEdge: 0,
|
|
152
196
|
displayValues,
|
|
153
197
|
opacities,
|
|
154
|
-
extremaMinValue:
|
|
155
|
-
extremaMaxValue:
|
|
156
|
-
extremaMinTime:
|
|
157
|
-
extremaMaxTime:
|
|
198
|
+
extremaMinValue: NaN,
|
|
199
|
+
extremaMaxValue: NaN,
|
|
200
|
+
extremaMinTime: NaN,
|
|
201
|
+
extremaMaxTime: NaN,
|
|
158
202
|
};
|
|
159
|
-
|
|
203
|
+
state.displayMin = sv.displayMin.value;
|
|
204
|
+
state.displayMax = sv.displayMax.value;
|
|
205
|
+
state.displayWindow = sv.displayWindow.value;
|
|
206
|
+
state.timestamp = sv.timestamp.value;
|
|
207
|
+
state.liveEdge = sv.liveEdgeSV?.value ?? 0;
|
|
208
|
+
state.displayValues = displayValues;
|
|
209
|
+
state.opacities = opacities;
|
|
210
|
+
state.extremaMinValue = sv.extremaMinValue.value;
|
|
211
|
+
state.extremaMaxValue = sv.extremaMaxValue.value;
|
|
212
|
+
state.extremaMinTime = sv.extremaMinTime.value;
|
|
213
|
+
state.extremaMaxTime = sv.extremaMaxTime.value;
|
|
214
|
+
|
|
215
|
+
const input: MultiEngineTickInput = scratch?.input ?? {
|
|
160
216
|
dt,
|
|
161
|
-
canvasWidth:
|
|
162
|
-
canvasHeight:
|
|
163
|
-
timeWindow:
|
|
164
|
-
smoothing:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
referenceValue: sv.referenceValue.value,
|
|
168
|
-
referenceValues: sv.referenceValues?.value,
|
|
169
|
-
nonNegative: sv.nonNegativeSV?.value ?? false,
|
|
170
|
-
maxValue: sv.maxValueSV?.value,
|
|
171
|
-
nowOverride: sv.nowOverrideSV?.value,
|
|
172
|
-
windowBuffer: sv.windowBufferSV?.value ?? 0,
|
|
217
|
+
canvasWidth: 0,
|
|
218
|
+
canvasHeight: 0,
|
|
219
|
+
timeWindow: 0,
|
|
220
|
+
smoothing: 0,
|
|
221
|
+
exaggerate: false,
|
|
222
|
+
referenceValue: undefined,
|
|
173
223
|
series: seriesSnap,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
224
|
+
};
|
|
225
|
+
input.dt = dt;
|
|
226
|
+
input.canvasWidth = sv.canvasWidth.value;
|
|
227
|
+
input.canvasHeight = sv.canvasHeight.value;
|
|
228
|
+
input.timeWindow = sv.timeWindow.value;
|
|
229
|
+
input.smoothing = sv.smoothing.value;
|
|
230
|
+
input.adaptiveSpeedBoost = sv.adaptiveSpeedBoostSV?.value;
|
|
231
|
+
input.exaggerate = sv.exaggerateSV.value;
|
|
232
|
+
input.referenceValue = sv.referenceValue.value;
|
|
233
|
+
input.referenceValues = sv.referenceValues?.value;
|
|
234
|
+
input.nonNegative = sv.nonNegativeSV?.value ?? false;
|
|
235
|
+
input.maxValue = sv.maxValueSV?.value;
|
|
236
|
+
input.nowOverride = sv.nowOverrideSV?.value;
|
|
237
|
+
input.windowBuffer = sv.windowBufferSV?.value ?? 0;
|
|
238
|
+
input.series = seriesSnap;
|
|
239
|
+
input.nowSeconds = Date.now() / 1000;
|
|
240
|
+
input.paused = sv.pausedSV.value;
|
|
241
|
+
input.snap = snap;
|
|
242
|
+
input.viewEnd = sv.viewEndSV?.value;
|
|
243
|
+
input.returnT = sv.returnTSV?.value;
|
|
244
|
+
input.returnFrom = sv.returnFromSV?.value;
|
|
245
|
+
input.viewWindow = sv.viewWindowSV?.value;
|
|
246
|
+
tickLiveChartSeriesEngineFrame(state, input);
|
|
182
247
|
sv.displayMin.value = state.displayMin;
|
|
183
248
|
sv.displayMax.value = state.displayMax;
|
|
184
249
|
sv.displayWindow.value = state.displayWindow;
|
|
@@ -272,46 +337,48 @@ export function useLiveChartSeriesEngine(
|
|
|
272
337
|
// Reused per-frame output buffers (ping-ponged) — see applyLiveChartSeriesEngineFrame.
|
|
273
338
|
const scratchRef = useRef<MultiSeriesEngineScratch | null>(null);
|
|
274
339
|
if (scratchRef.current === null) {
|
|
275
|
-
scratchRef.current =
|
|
340
|
+
scratchRef.current = makeMultiSeriesEngineScratch();
|
|
276
341
|
}
|
|
277
342
|
|
|
343
|
+
const frameRefs: MultiEngineFrameRefs = {
|
|
344
|
+
series,
|
|
345
|
+
displaySeriesValues,
|
|
346
|
+
seriesOpacities,
|
|
347
|
+
displayMin,
|
|
348
|
+
displayMax,
|
|
349
|
+
displayWindow,
|
|
350
|
+
timestamp,
|
|
351
|
+
canvasWidth,
|
|
352
|
+
canvasHeight,
|
|
353
|
+
timeWindow,
|
|
354
|
+
smoothing,
|
|
355
|
+
adaptiveSpeedBoostSV,
|
|
356
|
+
exaggerateSV,
|
|
357
|
+
referenceValue,
|
|
358
|
+
referenceValues,
|
|
359
|
+
nonNegativeSV,
|
|
360
|
+
maxValueSV,
|
|
361
|
+
nowOverrideSV,
|
|
362
|
+
windowBufferSV,
|
|
363
|
+
pausedSV,
|
|
364
|
+
viewEndSV: viewEnd,
|
|
365
|
+
returnTSV: returnT,
|
|
366
|
+
returnFromSV: returnFrom,
|
|
367
|
+
viewWindowSV: viewWindow,
|
|
368
|
+
liveEdgeSV: liveEdge,
|
|
369
|
+
snapSV,
|
|
370
|
+
extremaMinValue,
|
|
371
|
+
extremaMaxValue,
|
|
372
|
+
extremaMinTime,
|
|
373
|
+
extremaMaxTime,
|
|
374
|
+
};
|
|
375
|
+
|
|
278
376
|
useFrameCallback((frameInfo) => {
|
|
279
377
|
"worklet";
|
|
280
378
|
const scratch = scratchRef.current!;
|
|
281
379
|
applyLiveChartSeriesEngineFrame(
|
|
282
380
|
frameInfo,
|
|
283
|
-
|
|
284
|
-
series,
|
|
285
|
-
displaySeriesValues,
|
|
286
|
-
seriesOpacities,
|
|
287
|
-
displayMin,
|
|
288
|
-
displayMax,
|
|
289
|
-
displayWindow,
|
|
290
|
-
timestamp,
|
|
291
|
-
canvasWidth,
|
|
292
|
-
canvasHeight,
|
|
293
|
-
timeWindow,
|
|
294
|
-
smoothing,
|
|
295
|
-
adaptiveSpeedBoostSV,
|
|
296
|
-
exaggerateSV,
|
|
297
|
-
referenceValue,
|
|
298
|
-
referenceValues,
|
|
299
|
-
nonNegativeSV,
|
|
300
|
-
maxValueSV,
|
|
301
|
-
nowOverrideSV,
|
|
302
|
-
windowBufferSV,
|
|
303
|
-
pausedSV,
|
|
304
|
-
viewEndSV: viewEnd,
|
|
305
|
-
returnTSV: returnT,
|
|
306
|
-
returnFromSV: returnFrom,
|
|
307
|
-
viewWindowSV: viewWindow,
|
|
308
|
-
liveEdgeSV: liveEdge,
|
|
309
|
-
snapSV,
|
|
310
|
-
extremaMinValue,
|
|
311
|
-
extremaMaxValue,
|
|
312
|
-
extremaMinTime,
|
|
313
|
-
extremaMaxTime,
|
|
314
|
-
},
|
|
381
|
+
frameRefs,
|
|
315
382
|
scratch,
|
|
316
383
|
);
|
|
317
384
|
});
|