react-native-livechart 4.4.0 → 4.6.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/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/LoadingOverlay.d.ts +9 -1
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +11 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts +11 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +35 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +13 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +13 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +5 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useChartReveal.d.ts +3 -1
- package/dist/hooks/useChartReveal.d.ts.map +1 -1
- package/dist/hooks/useModeBlend.d.ts +4 -1
- package/dist/hooks/useModeBlend.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/squiggly.d.ts +8 -5
- package/dist/math/squiggly.d.ts.map +1 -1
- package/dist/types.d.ts +110 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/LiveChart.tsx +128 -42
- package/src/components/LiveChartSeries.tsx +97 -26
- package/src/components/LoadingOverlay.tsx +25 -6
- package/src/constants.ts +17 -0
- package/src/core/liveChartEngineTick.ts +26 -10
- package/src/core/liveChartSeriesEngineTick.ts +22 -4
- package/src/core/resolveConfig.ts +63 -0
- package/src/core/useLiveChartEngine.ts +40 -1
- package/src/core/useLiveChartSeriesEngine.ts +35 -1
- package/src/hooks/useChartPaths.ts +11 -1
- package/src/hooks/useChartReveal.ts +7 -2
- package/src/hooks/useModeBlend.ts +5 -3
- package/src/index.ts +2 -0
- package/src/math/squiggly.ts +21 -7
- package/src/types.ts +112 -1
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Canvas, Group } from "@shopify/react-native-skia";
|
|
8
8
|
import { useLayoutEffect, useState } from "react";
|
|
9
|
-
import { View } from "react-native";
|
|
9
|
+
import { StyleSheet, View } from "react-native";
|
|
10
10
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
11
|
-
import {
|
|
11
|
+
import Animated, {
|
|
12
12
|
useAnimatedReaction,
|
|
13
|
+
useAnimatedStyle,
|
|
13
14
|
useDerivedValue,
|
|
14
15
|
useSharedValue,
|
|
16
|
+
withTiming,
|
|
15
17
|
type SharedValue,
|
|
16
18
|
} from "react-native-reanimated";
|
|
17
19
|
import { scheduleOnRN } from "react-native-worklets";
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
DEFAULT_ACCENT_COLOR,
|
|
20
22
|
HOLD_TO_SCRUB_MS,
|
|
21
23
|
MAX_MULTI_SERIES,
|
|
24
|
+
SCRUB_OVERLAY_FADE_MS,
|
|
22
25
|
} from "../constants";
|
|
23
26
|
import {
|
|
24
27
|
lineColorsSignatureFromArray,
|
|
@@ -33,12 +36,14 @@ import {
|
|
|
33
36
|
resolveGridStyle,
|
|
34
37
|
resolveLeftEdgeFade,
|
|
35
38
|
resolveLegend,
|
|
39
|
+
resolveLoading,
|
|
36
40
|
resolveMarkerCluster,
|
|
37
41
|
resolveMetrics,
|
|
38
42
|
resolveMultiSeriesDot,
|
|
39
43
|
resolveReturnToLiveMs,
|
|
40
44
|
resolveScrub,
|
|
41
45
|
resolveSelectionDot,
|
|
46
|
+
resolveTransitions,
|
|
42
47
|
resolveXAxis,
|
|
43
48
|
resolveYAxis,
|
|
44
49
|
resolveZoom,
|
|
@@ -126,6 +131,8 @@ function useLiveChartSeriesController({
|
|
|
126
131
|
timeWindow = 30,
|
|
127
132
|
paused = false,
|
|
128
133
|
loading = false,
|
|
134
|
+
transitions,
|
|
135
|
+
snapKey,
|
|
129
136
|
smoothing = 0.08,
|
|
130
137
|
exaggerate = false,
|
|
131
138
|
nonNegative = false,
|
|
@@ -289,7 +296,18 @@ function useLiveChartSeriesController({
|
|
|
289
296
|
return false;
|
|
290
297
|
});
|
|
291
298
|
|
|
292
|
-
|
|
299
|
+
// Resolve the loading shell: null = not loading, else the styled config.
|
|
300
|
+
const loadingCfg = resolveLoading(loading);
|
|
301
|
+
const loadingActive = loadingCfg !== null;
|
|
302
|
+
// Multi-series is always lines, so only the reveal transition applies (no
|
|
303
|
+
// candle↔line crossfade); `transitions.mode` is accepted but inert here.
|
|
304
|
+
const transitionsCfg = resolveTransitions(transitions);
|
|
305
|
+
const reveal = useChartReveal(
|
|
306
|
+
loadingActive,
|
|
307
|
+
hasData,
|
|
308
|
+
false,
|
|
309
|
+
transitionsCfg.reveal,
|
|
310
|
+
);
|
|
293
311
|
|
|
294
312
|
const effectiveSeries = useMultiSeriesReverseMorphInputs({
|
|
295
313
|
series,
|
|
@@ -301,6 +319,7 @@ function useLiveChartSeriesController({
|
|
|
301
319
|
series: effectiveSeries,
|
|
302
320
|
timeWindow,
|
|
303
321
|
paused,
|
|
322
|
+
snapKey,
|
|
304
323
|
scrollEnabled: timeScrollEnabled,
|
|
305
324
|
returnToLiveMs,
|
|
306
325
|
smoothing,
|
|
@@ -453,6 +472,23 @@ function useLiveChartSeriesController({
|
|
|
453
472
|
|
|
454
473
|
const backgroundColor = `rgb(${palette.bgRgb[0]}, ${palette.bgRgb[1]}, ${palette.bgRgb[2]})`;
|
|
455
474
|
|
|
475
|
+
// Fade markers + reference lines out while scrubbing when
|
|
476
|
+
// `scrub.hideOverlaysOnScrub` is set. Eased off the scrub-ACTIVE flag (not the
|
|
477
|
+
// crosshair edge fade, which would resurface them near the live dot); only a
|
|
478
|
+
// group opacity animates — the overlay draws stay intact. See `LiveChart`.
|
|
479
|
+
const fadeOverlaysOnScrub =
|
|
480
|
+
scrubCfg !== null && scrubCfg.hideOverlaysOnScrub === true;
|
|
481
|
+
const overlayScrubFade = useDerivedValue(() =>
|
|
482
|
+
fadeOverlaysOnScrub
|
|
483
|
+
? withTiming(crosshair.scrubActive.get() ? 0 : 1, {
|
|
484
|
+
duration: SCRUB_OVERLAY_FADE_MS,
|
|
485
|
+
})
|
|
486
|
+
: 1,
|
|
487
|
+
);
|
|
488
|
+
const markerGroupOpacity = useDerivedValue(
|
|
489
|
+
() => reveal.dotOpacity.get() * overlayScrubFade.get(),
|
|
490
|
+
);
|
|
491
|
+
|
|
456
492
|
return {
|
|
457
493
|
// passthrough props the render needs
|
|
458
494
|
series,
|
|
@@ -484,6 +520,11 @@ function useLiveChartSeriesController({
|
|
|
484
520
|
// engine + reveal
|
|
485
521
|
engine,
|
|
486
522
|
reveal,
|
|
523
|
+
// loading shell styling (null → not loading)
|
|
524
|
+
loadingLineColor: loadingCfg?.color,
|
|
525
|
+
loadingStrokeWidth: loadingCfg?.strokeWidth,
|
|
526
|
+
loadingAmplitude: loadingCfg?.amplitude,
|
|
527
|
+
loadingSpeed: loadingCfg?.speed,
|
|
487
528
|
effectiveSeries,
|
|
488
529
|
layoutHeight,
|
|
489
530
|
onLayout,
|
|
@@ -500,6 +541,8 @@ function useLiveChartSeriesController({
|
|
|
500
541
|
markersActive,
|
|
501
542
|
markersSV,
|
|
502
543
|
markerClusterCfg,
|
|
544
|
+
markerGroupOpacity,
|
|
545
|
+
overlayScrubFade,
|
|
503
546
|
renderMarker,
|
|
504
547
|
// selection dot: resolved config + fallback color (the leading series' color)
|
|
505
548
|
selectionDot: selectionDotCfg,
|
|
@@ -545,10 +588,16 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
545
588
|
markersActive,
|
|
546
589
|
markersSV,
|
|
547
590
|
markerClusterCfg,
|
|
591
|
+
markerGroupOpacity,
|
|
592
|
+
overlayScrubFade,
|
|
548
593
|
renderMarker,
|
|
549
594
|
series,
|
|
550
595
|
emptyText,
|
|
551
596
|
metricsCfg,
|
|
597
|
+
loadingLineColor,
|
|
598
|
+
loadingStrokeWidth,
|
|
599
|
+
loadingAmplitude,
|
|
600
|
+
loadingSpeed,
|
|
552
601
|
} = model;
|
|
553
602
|
|
|
554
603
|
return (
|
|
@@ -570,18 +619,21 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
570
619
|
|
|
571
620
|
{/* Index keys: reference lines are a positional array and two may share
|
|
572
621
|
value + label (e.g. duplicate working orders at the same price), which a
|
|
573
|
-
content-derived key would collapse to one.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
622
|
+
content-derived key would collapse to one. Fade group lets
|
|
623
|
+
`scrub.hideOverlaysOnScrub` ease the lines out while scrubbing. */}
|
|
624
|
+
<Group opacity={overlayScrubFade}>
|
|
625
|
+
{allRefLines.map((rl, i) => (
|
|
626
|
+
<ReferenceLineOverlay
|
|
627
|
+
key={i}
|
|
628
|
+
engine={engine}
|
|
629
|
+
padding={effectivePadding}
|
|
630
|
+
line={rl}
|
|
631
|
+
palette={palette}
|
|
632
|
+
formatValue={formatValue}
|
|
633
|
+
font={skiaFont}
|
|
634
|
+
/>
|
|
635
|
+
))}
|
|
636
|
+
</Group>
|
|
585
637
|
|
|
586
638
|
{dotCfg.valueLine && (
|
|
587
639
|
<Group opacity={reveal.lineOpacity}>
|
|
@@ -654,7 +706,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
654
706
|
)}
|
|
655
707
|
|
|
656
708
|
{markersActive && (
|
|
657
|
-
<Group opacity={
|
|
709
|
+
<Group opacity={markerGroupOpacity}>
|
|
658
710
|
<MarkerOverlay
|
|
659
711
|
markers={markersSV}
|
|
660
712
|
engine={engine}
|
|
@@ -680,6 +732,10 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
680
732
|
strokeWidth={strokeWidth}
|
|
681
733
|
badge={false}
|
|
682
734
|
emptyMetrics={metricsCfg.emptyState}
|
|
735
|
+
lineColor={loadingLineColor}
|
|
736
|
+
lineStrokeWidth={loadingStrokeWidth}
|
|
737
|
+
waveAmplitude={loadingAmplitude}
|
|
738
|
+
waveSpeed={loadingSpeed}
|
|
683
739
|
/>
|
|
684
740
|
</Group>
|
|
685
741
|
);
|
|
@@ -726,10 +782,11 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
726
782
|
formatValue,
|
|
727
783
|
skiaFont,
|
|
728
784
|
degenShakeTransform,
|
|
785
|
+
overlayScrubFade,
|
|
729
786
|
} = model;
|
|
730
787
|
if (allRefLines.length === 0) return null;
|
|
731
788
|
return (
|
|
732
|
-
<Group transform={degenShakeTransform}>
|
|
789
|
+
<Group transform={degenShakeTransform} opacity={overlayScrubFade}>
|
|
733
790
|
{allRefLines.map((rl, i) => (
|
|
734
791
|
<ReferenceLineOverlay
|
|
735
792
|
key={i}
|
|
@@ -778,8 +835,15 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
778
835
|
markersSV,
|
|
779
836
|
markerClusterCfg,
|
|
780
837
|
renderMarker,
|
|
838
|
+
overlayScrubFade,
|
|
781
839
|
} = model;
|
|
782
840
|
|
|
841
|
+
// Mirror the Skia overlay fade onto the RN custom-marker sibling so
|
|
842
|
+
// `scrub.hideOverlaysOnScrub` hides it with the Skia markers.
|
|
843
|
+
const overlayFadeStyle = useAnimatedStyle(() => ({
|
|
844
|
+
opacity: overlayScrubFade.get(),
|
|
845
|
+
}));
|
|
846
|
+
|
|
783
847
|
// Extend the scrub dim past the plot's right edge to fully cover the series
|
|
784
848
|
// dots (with their halo) and pulse rings, all centered on that edge. The
|
|
785
849
|
// gutter reserves room beyond this for the value/Y-axis labels, drawn on top.
|
|
@@ -876,16 +940,23 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
876
940
|
/>
|
|
877
941
|
|
|
878
942
|
{/* Custom-rendered markers — RN views floated over the canvas
|
|
879
|
-
(non-Skia), pinned to each marker's live position.
|
|
943
|
+
(non-Skia), pinned to each marker's live position. Box-none fade
|
|
944
|
+
wrapper so `scrub.hideOverlaysOnScrub` hides them with the Skia
|
|
945
|
+
markers (full-bleed; children keep their own absolute positions). */}
|
|
880
946
|
{markersActive && renderMarker && (
|
|
881
|
-
<
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
947
|
+
<Animated.View
|
|
948
|
+
pointerEvents="box-none"
|
|
949
|
+
style={[StyleSheet.absoluteFill, overlayFadeStyle]}
|
|
950
|
+
>
|
|
951
|
+
<CustomMarkerOverlay
|
|
952
|
+
markers={markersSV}
|
|
953
|
+
renderMarker={renderMarker}
|
|
954
|
+
engine={engine}
|
|
955
|
+
padding={effectivePadding}
|
|
956
|
+
series={series}
|
|
957
|
+
cluster={markerClusterCfg}
|
|
958
|
+
/>
|
|
959
|
+
</Animated.View>
|
|
889
960
|
)}
|
|
890
961
|
</View>
|
|
891
962
|
</GestureDetector>
|
|
@@ -62,6 +62,10 @@ export function LoadingOverlay({
|
|
|
62
62
|
badgeTail = true,
|
|
63
63
|
badgeMetrics = BADGE_METRICS_DEFAULTS,
|
|
64
64
|
emptyMetrics = EMPTY_STATE_METRICS_DEFAULTS,
|
|
65
|
+
lineColor,
|
|
66
|
+
lineStrokeWidth,
|
|
67
|
+
waveAmplitude = 14,
|
|
68
|
+
waveSpeed = 1,
|
|
65
69
|
}: {
|
|
66
70
|
engine: ChartEngineLayout;
|
|
67
71
|
padding: ChartPadding;
|
|
@@ -73,6 +77,14 @@ export function LoadingOverlay({
|
|
|
73
77
|
isEmpty: SharedValue<boolean> | { value: boolean };
|
|
74
78
|
emptyText: string;
|
|
75
79
|
strokeWidth: number;
|
|
80
|
+
/** Loading squiggle + skeleton color. Omit → theme `gridLine`. */
|
|
81
|
+
lineColor?: string;
|
|
82
|
+
/** Loading squiggle stroke width. Omit → `strokeWidth`. */
|
|
83
|
+
lineStrokeWidth?: number;
|
|
84
|
+
/** Breathing-wave base amplitude (px). */
|
|
85
|
+
waveAmplitude?: number;
|
|
86
|
+
/** Breathing-wave speed multiplier. */
|
|
87
|
+
waveSpeed?: number;
|
|
76
88
|
/** Mirror the badge prop so labels align with GridOverlay's label positions. */
|
|
77
89
|
badge?: boolean;
|
|
78
90
|
/** Whether the badge tail spike is shown; affects the left inset used for skeleton alignment. */
|
|
@@ -86,6 +98,11 @@ export function LoadingOverlay({
|
|
|
86
98
|
const leftInset =
|
|
87
99
|
badgeMetrics.dotGap + badgeTailAndCap(font.getSize(), badgeTail, badgeMetrics);
|
|
88
100
|
|
|
101
|
+
// Loading-shell color (squiggle + skeleton placeholders) and squiggle stroke,
|
|
102
|
+
// both overridable via `loading={{ color, strokeWidth }}`.
|
|
103
|
+
const loadingColor = lineColor ?? palette.gridLine;
|
|
104
|
+
const loadingStroke = lineStrokeWidth ?? strokeWidth;
|
|
105
|
+
|
|
89
106
|
// Squiggly path — built into a reused PathBuilder and detach()-ed each frame.
|
|
90
107
|
const squigglyBuilder = usePathBuilder();
|
|
91
108
|
|
|
@@ -100,6 +117,8 @@ export function LoadingOverlay({
|
|
|
100
117
|
engine.canvasHeight.get(),
|
|
101
118
|
padding,
|
|
102
119
|
engine.timestamp.get(),
|
|
120
|
+
waveAmplitude,
|
|
121
|
+
waveSpeed,
|
|
103
122
|
);
|
|
104
123
|
return buildSplineDetached(b, pts);
|
|
105
124
|
});
|
|
@@ -225,8 +244,8 @@ export function LoadingOverlay({
|
|
|
225
244
|
<Path
|
|
226
245
|
path={squigglyPath}
|
|
227
246
|
style="stroke"
|
|
228
|
-
strokeWidth={
|
|
229
|
-
color={
|
|
247
|
+
strokeWidth={loadingStroke}
|
|
248
|
+
color={loadingColor}
|
|
230
249
|
strokeCap="round"
|
|
231
250
|
strokeJoin="round"
|
|
232
251
|
/>
|
|
@@ -255,7 +274,7 @@ export function LoadingOverlay({
|
|
|
255
274
|
width={RECT_W}
|
|
256
275
|
height={RECT_H}
|
|
257
276
|
r={RECT_R}
|
|
258
|
-
color={
|
|
277
|
+
color={loadingColor}
|
|
259
278
|
/>
|
|
260
279
|
<RoundedRect
|
|
261
280
|
x={lx}
|
|
@@ -263,7 +282,7 @@ export function LoadingOverlay({
|
|
|
263
282
|
width={RECT_W}
|
|
264
283
|
height={RECT_H}
|
|
265
284
|
r={RECT_R}
|
|
266
|
-
color={
|
|
285
|
+
color={loadingColor}
|
|
267
286
|
/>
|
|
268
287
|
<RoundedRect
|
|
269
288
|
x={lx}
|
|
@@ -271,7 +290,7 @@ export function LoadingOverlay({
|
|
|
271
290
|
width={RECT_W}
|
|
272
291
|
height={RECT_H}
|
|
273
292
|
r={RECT_R}
|
|
274
|
-
color={
|
|
293
|
+
color={loadingColor}
|
|
275
294
|
/>
|
|
276
295
|
<RoundedRect
|
|
277
296
|
x={lx}
|
|
@@ -279,7 +298,7 @@ export function LoadingOverlay({
|
|
|
279
298
|
width={RECT_W}
|
|
280
299
|
height={RECT_H}
|
|
281
300
|
r={RECT_R}
|
|
282
|
-
color={
|
|
301
|
+
color={loadingColor}
|
|
283
302
|
/>
|
|
284
303
|
|
|
285
304
|
{/* Empty state label */}
|
package/src/constants.ts
CHANGED
|
@@ -31,6 +31,23 @@ export const HOLD_TO_SCRUB_MS = 500;
|
|
|
31
31
|
*/
|
|
32
32
|
export const RETURN_TO_LIVE_MS = 450;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Duration (ms) of the fade applied to annotation overlays (markers + reference
|
|
36
|
+
* lines) when `scrub.hideOverlaysOnScrub` is on — how long they take to ease out as a
|
|
37
|
+
* scrub starts and back in on release. Driven by the scrub-active flag, eased on
|
|
38
|
+
* the UI thread. Shared by `LiveChart` and `LiveChartSeries`.
|
|
39
|
+
*/
|
|
40
|
+
export const SCRUB_OVERLAY_FADE_MS = 150;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Base wave amplitude (px) of the breathing loading squiggle — it breathes
|
|
44
|
+
* between `0.4×` and `1.0×` this. Overridable via `loading={{ amplitude }}`.
|
|
45
|
+
*/
|
|
46
|
+
export const LOADING_WAVE_AMPLITUDE = 14;
|
|
47
|
+
|
|
48
|
+
/** Default breathing-wave speed multiplier (`1` = built-in cadence). */
|
|
49
|
+
export const LOADING_WAVE_SPEED = 1;
|
|
50
|
+
|
|
34
51
|
// ─── Metric default tokens (single source of truth for LiveChartMetrics) ─────
|
|
35
52
|
// The resolved `metrics` config (see resolveMetrics) is assembled from these
|
|
36
53
|
// objects. Draw/worklet helpers default their metric params to the matching
|
|
@@ -62,6 +62,17 @@ export interface EngineTickInput {
|
|
|
62
62
|
windowBuffer?: number;
|
|
63
63
|
/** When true, freeze the viewport timestamp and skip displayWindow lerp */
|
|
64
64
|
paused?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Settle the framing in this one frame instead of easing into it: the time
|
|
67
|
+
* window ({@link EngineTickMutable.displayWindow}), Y-range
|
|
68
|
+
* ({@link EngineTickMutable.displayMin}/{@link EngineTickMutable.displayMax}),
|
|
69
|
+
* and value ({@link EngineTickMutable.displayValue}/{@link EngineTickMutable.edgeValue})
|
|
70
|
+
* jump straight to their targets — `smoothing` is bypassed for this tick only.
|
|
71
|
+
* Set for the single frame after a `snapKey` change so a timeframe / dataset
|
|
72
|
+
* switch lands instantly while live ticks keep their normal smoothing. Leaves
|
|
73
|
+
* the timestamp / pan-scroll state untouched. See {@link LiveChartProps.snapKey}.
|
|
74
|
+
*/
|
|
75
|
+
snap?: boolean;
|
|
65
76
|
/**
|
|
66
77
|
* Absolute right-edge time (unix seconds) to freeze the window at, or
|
|
67
78
|
* `null`/`undefined` to follow the live edge. Set by the pan-scroll gesture
|
|
@@ -147,6 +158,10 @@ export function tickLiveChartEngineFrame(
|
|
|
147
158
|
if (input.canvasWidth === 0 || input.canvasHeight === 0) return;
|
|
148
159
|
|
|
149
160
|
const speed = input.smoothing;
|
|
161
|
+
// One-shot settle (snapKey change): collapse this frame's easing so the
|
|
162
|
+
// window / range / value land on target instantly, then normal smoothing
|
|
163
|
+
// resumes next frame. See `EngineTickInput.snap`.
|
|
164
|
+
const snap = input.snap === true;
|
|
150
165
|
let target = input.targetValue;
|
|
151
166
|
if (input.mode === "candle" && input.liveCandle) {
|
|
152
167
|
target = input.liveCandle.close;
|
|
@@ -160,17 +175,16 @@ export function tickLiveChartEngineFrame(
|
|
|
160
175
|
(1 - gapRatio) *
|
|
161
176
|
(input.adaptiveSpeedBoost ?? MOTION_METRICS_DEFAULTS.adaptiveSpeedBoost);
|
|
162
177
|
|
|
163
|
-
state.displayValue =
|
|
164
|
-
|
|
165
|
-
target,
|
|
166
|
-
adaptiveSpeed,
|
|
167
|
-
input.dt,
|
|
168
|
-
);
|
|
178
|
+
state.displayValue = snap
|
|
179
|
+
? target
|
|
180
|
+
: lerp(state.displayValue, target, adaptiveSpeed, input.dt);
|
|
169
181
|
|
|
170
182
|
// Pinch-zoom: ease toward the zoom override when set, else the configured
|
|
171
183
|
// window. Mirrors the viewEnd freeze above (width vs. right edge).
|
|
172
184
|
const targetWindow = input.viewWindow ?? input.timeWindow;
|
|
173
|
-
state.displayWindow =
|
|
185
|
+
state.displayWindow = snap
|
|
186
|
+
? targetWindow
|
|
187
|
+
: lerp(state.displayWindow, targetWindow, speed, input.dt);
|
|
174
188
|
|
|
175
189
|
const winStart = state.timestamp - state.displayWindow;
|
|
176
190
|
|
|
@@ -292,13 +306,13 @@ export function tickLiveChartEngineFrame(
|
|
|
292
306
|
const maxV = input.maxValue;
|
|
293
307
|
if (maxV !== undefined && tMax > maxV) tMax = maxV;
|
|
294
308
|
|
|
295
|
-
if (tMin < state.displayMin) {
|
|
309
|
+
if (snap || tMin < state.displayMin) {
|
|
296
310
|
state.displayMin = tMin;
|
|
297
311
|
} else {
|
|
298
312
|
state.displayMin = lerp(state.displayMin, tMin, speed, input.dt);
|
|
299
313
|
}
|
|
300
314
|
|
|
301
|
-
if (tMax > state.displayMax) {
|
|
315
|
+
if (snap || tMax > state.displayMax) {
|
|
302
316
|
state.displayMax = tMax;
|
|
303
317
|
} else {
|
|
304
318
|
state.displayMax = lerp(state.displayMax, tMax, speed, input.dt);
|
|
@@ -338,6 +352,8 @@ export function tickLiveChartEngineFrame(
|
|
|
338
352
|
}
|
|
339
353
|
if (elo > 0) edgeTarget = pts[elo - 1].value;
|
|
340
354
|
}
|
|
341
|
-
state.edgeValue =
|
|
355
|
+
state.edgeValue = snap
|
|
356
|
+
? edgeTarget
|
|
357
|
+
: lerp(state.edgeValue, edgeTarget, speed, input.dt);
|
|
342
358
|
}
|
|
343
359
|
}
|
|
@@ -50,6 +50,17 @@ export interface MultiEngineTickInput {
|
|
|
50
50
|
/** Right-edge buffer as a fraction of the time window. */
|
|
51
51
|
windowBuffer?: number;
|
|
52
52
|
paused?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Settle the framing in this one frame instead of easing into it: the time
|
|
55
|
+
* window, Y-range ({@link MultiEngineTickMutable.displayMin}/{@link MultiEngineTickMutable.displayMax}),
|
|
56
|
+
* and per-series tips ({@link MultiEngineTickMutable.displayValues}) jump
|
|
57
|
+
* straight to their targets — `smoothing` is bypassed for this tick only. Set
|
|
58
|
+
* for the single frame after a `snapKey` change so a timeframe / dataset switch
|
|
59
|
+
* lands instantly while live ticks keep their normal smoothing. Series-toggle
|
|
60
|
+
* opacities and the timestamp / pan-scroll state are left untouched. See
|
|
61
|
+
* {@link LiveChartSeriesProps.snapKey}.
|
|
62
|
+
*/
|
|
63
|
+
snap?: boolean;
|
|
53
64
|
/**
|
|
54
65
|
* Absolute right-edge time (unix seconds) to freeze the window at, or
|
|
55
66
|
* `null`/`undefined` to follow the live edge. Drives pan-scroll; takes
|
|
@@ -122,6 +133,9 @@ export function tickLiveChartSeriesEngineFrame(
|
|
|
122
133
|
if (input.canvasWidth === 0 || input.canvasHeight === 0) return;
|
|
123
134
|
|
|
124
135
|
const speed = input.smoothing;
|
|
136
|
+
// One-shot settle (snapKey change): collapse this frame's easing so the
|
|
137
|
+
// window / range / tips land on target instantly. See `MultiEngineTickInput.snap`.
|
|
138
|
+
const snap = input.snap === true;
|
|
125
139
|
const series = input.series;
|
|
126
140
|
const n = series.length;
|
|
127
141
|
|
|
@@ -138,7 +152,9 @@ export function tickLiveChartSeriesEngineFrame(
|
|
|
138
152
|
// Pinch-zoom: ease toward the zoom override when set, else the configured
|
|
139
153
|
// window (mirrors the single-series tick).
|
|
140
154
|
const targetWindow = input.viewWindow ?? input.timeWindow;
|
|
141
|
-
state.displayWindow =
|
|
155
|
+
state.displayWindow = snap
|
|
156
|
+
? targetWindow
|
|
157
|
+
: lerp(state.displayWindow, targetWindow, speed, input.dt);
|
|
142
158
|
|
|
143
159
|
const winStart = state.timestamp - state.displayWindow;
|
|
144
160
|
const range = state.displayMax - state.displayMin;
|
|
@@ -170,7 +186,9 @@ export function tickLiveChartSeriesEngineFrame(
|
|
|
170
186
|
(1 - gapRatio) *
|
|
171
187
|
(input.adaptiveSpeedBoost ??
|
|
172
188
|
MOTION_METRICS_DEFAULTS.adaptiveSpeedBoost);
|
|
173
|
-
state.displayValues[i] =
|
|
189
|
+
state.displayValues[i] = snap
|
|
190
|
+
? target
|
|
191
|
+
: lerp(cur, target, adaptiveSpeed, input.dt);
|
|
174
192
|
|
|
175
193
|
const targetOp = series[i].visible !== false ? 1 : 0;
|
|
176
194
|
state.opacities[i] = lerp(state.opacities[i], targetOp, speed, input.dt);
|
|
@@ -261,13 +279,13 @@ export function tickLiveChartSeriesEngineFrame(
|
|
|
261
279
|
const maxV = input.maxValue;
|
|
262
280
|
if (maxV !== undefined && tMax > maxV) tMax = maxV;
|
|
263
281
|
|
|
264
|
-
if (tMin < state.displayMin) {
|
|
282
|
+
if (snap || tMin < state.displayMin) {
|
|
265
283
|
state.displayMin = tMin;
|
|
266
284
|
} else {
|
|
267
285
|
state.displayMin = lerp(state.displayMin, tMin, speed, input.dt);
|
|
268
286
|
}
|
|
269
287
|
|
|
270
|
-
if (tMax > state.displayMax) {
|
|
288
|
+
if (snap || tMax > state.displayMax) {
|
|
271
289
|
state.displayMax = tMax;
|
|
272
290
|
} else {
|
|
273
291
|
state.displayMax = lerp(state.displayMax, tMax, speed, input.dt);
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
LineStyleConfig,
|
|
15
15
|
LiveChartMetrics,
|
|
16
16
|
LiveChartMetricsOverride,
|
|
17
|
+
LoadingConfig,
|
|
17
18
|
MarkerClusterConfig,
|
|
18
19
|
DotConfig,
|
|
19
20
|
DotRingConfig,
|
|
@@ -29,6 +30,7 @@ import type {
|
|
|
29
30
|
ThresholdConfig,
|
|
30
31
|
ThresholdLineConfig,
|
|
31
32
|
TradeEvent,
|
|
33
|
+
TransitionConfig,
|
|
32
34
|
ValueLineConfig,
|
|
33
35
|
VolumeConfig,
|
|
34
36
|
XAxisConfig,
|
|
@@ -45,6 +47,8 @@ import {
|
|
|
45
47
|
EMPTY_STATE_METRICS_DEFAULTS,
|
|
46
48
|
FADE_EDGE_WIDTH,
|
|
47
49
|
GRID_METRICS_DEFAULTS,
|
|
50
|
+
LOADING_WAVE_AMPLITUDE,
|
|
51
|
+
LOADING_WAVE_SPEED,
|
|
48
52
|
MOTION_METRICS_DEFAULTS,
|
|
49
53
|
RETURN_TO_LIVE_MS,
|
|
50
54
|
} from "../constants";
|
|
@@ -165,6 +169,8 @@ export interface ResolvedScrubConfig {
|
|
|
165
169
|
tooltipShowTime: boolean;
|
|
166
170
|
/** Press-and-hold delay (ms) before scrubbing activates. 0 = immediate. */
|
|
167
171
|
panGestureDelay: number;
|
|
172
|
+
/** Fade markers + reference lines out while scrubbing. */
|
|
173
|
+
hideOverlaysOnScrub: boolean;
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
export interface ResolvedScrubActionConfig {
|
|
@@ -477,6 +483,32 @@ export function resolveReturnToLiveMs(
|
|
|
477
483
|
return d > 0 ? d : 0;
|
|
478
484
|
}
|
|
479
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Resolved transition durations. `undefined` for a field means "use the
|
|
488
|
+
* component's built-in default" (so we don't duplicate the default constants
|
|
489
|
+
* here); a number is an explicit duration in ms (clamped to ≥ 0).
|
|
490
|
+
*/
|
|
491
|
+
export interface ResolvedTransitionConfig {
|
|
492
|
+
reveal: number | undefined;
|
|
493
|
+
mode: number | undefined;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const clampMs = (v: number | undefined): number | undefined =>
|
|
497
|
+
v == null ? undefined : v > 0 ? v : 0;
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Resolves the `transitions` prop. `false` → all transitions instant (`0`);
|
|
501
|
+
* `true` / omitted → defaults (both `undefined` = use the built-in durations);
|
|
502
|
+
* an object → per-transition overrides (an omitted field keeps its default).
|
|
503
|
+
*/
|
|
504
|
+
export function resolveTransitions(
|
|
505
|
+
prop: boolean | TransitionConfig | undefined,
|
|
506
|
+
): ResolvedTransitionConfig {
|
|
507
|
+
if (prop === false) return { reveal: 0, mode: 0 };
|
|
508
|
+
if (prop == null || prop === true) return { reveal: undefined, mode: undefined };
|
|
509
|
+
return { reveal: clampMs(prop.reveal), mode: clampMs(prop.mode) };
|
|
510
|
+
}
|
|
511
|
+
|
|
480
512
|
const AXIS_LABEL_DEFAULTS: ResolvedAxisLabelConfig = {
|
|
481
513
|
format: undefined,
|
|
482
514
|
color: undefined,
|
|
@@ -571,6 +603,7 @@ const SCRUB_DEFAULTS: ResolvedScrubConfig = {
|
|
|
571
603
|
tooltipShowValue: true,
|
|
572
604
|
tooltipShowTime: true,
|
|
573
605
|
panGestureDelay: 0,
|
|
606
|
+
hideOverlaysOnScrub: false,
|
|
574
607
|
};
|
|
575
608
|
|
|
576
609
|
/**
|
|
@@ -590,6 +623,36 @@ export function resolveScrub(
|
|
|
590
623
|
return resolved;
|
|
591
624
|
}
|
|
592
625
|
|
|
626
|
+
export interface ResolvedLoadingConfig {
|
|
627
|
+
/** undefined → palette.gridLine */
|
|
628
|
+
color: string | undefined;
|
|
629
|
+
/** undefined → the chart's line strokeWidth */
|
|
630
|
+
strokeWidth: number | undefined;
|
|
631
|
+
/** Base breathing-wave amplitude (px). */
|
|
632
|
+
amplitude: number;
|
|
633
|
+
/** Breathing-wave speed multiplier. */
|
|
634
|
+
speed: number;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const LOADING_DEFAULTS: ResolvedLoadingConfig = {
|
|
638
|
+
color: undefined,
|
|
639
|
+
strokeWidth: undefined,
|
|
640
|
+
amplitude: LOADING_WAVE_AMPLITUDE,
|
|
641
|
+
speed: LOADING_WAVE_SPEED,
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Resolves the `loading` prop to a fully-typed config or null (not loading).
|
|
646
|
+
* `true` → defaults (loading on, built-in look), object → merged with defaults
|
|
647
|
+
* (loading on, restyled), `false` / omitted → null. So a non-null result is the
|
|
648
|
+
* "is loading" flag and carries the resolved styling.
|
|
649
|
+
*/
|
|
650
|
+
export function resolveLoading(
|
|
651
|
+
prop: boolean | LoadingConfig | undefined,
|
|
652
|
+
): ResolvedLoadingConfig | null {
|
|
653
|
+
return resolveToggle(prop, LOADING_DEFAULTS, false);
|
|
654
|
+
}
|
|
655
|
+
|
|
593
656
|
const SCRUB_ACTION_DEFAULTS: ResolvedScrubActionConfig = {
|
|
594
657
|
icon: "+",
|
|
595
658
|
background: undefined,
|