react-native-livechart 4.15.1 → 4.17.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/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +6 -0
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +22 -11
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartReveal.d.ts +3 -2
- package/dist/hooks/useChartReveal.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +8 -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/markerCluster.d.ts +3 -0
- package/dist/math/markerCluster.d.ts.map +1 -1
- package/dist/types.d.ts +50 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/LiveChart.tsx +236 -123
- package/src/components/LiveChartSeries.tsx +60 -44
- package/src/core/liveChartEngineTick.ts +5 -1
- package/src/core/liveChartSeriesEngineTick.ts +5 -1
- package/src/core/resolveConfig.ts +14 -0
- package/src/core/useLiveChartEngine.ts +6 -2
- package/src/core/useLiveChartSeriesEngine.ts +4 -2
- package/src/hooks/useCandlePaths.ts +41 -27
- package/src/hooks/useChartReveal.ts +11 -3
- package/src/hooks/useMarkers.ts +1 -0
- package/src/hooks/usePanScroll.ts +12 -1
- package/src/index.ts +1 -0
- package/src/math/markerCluster.ts +14 -0
- package/src/types.ts +51 -1
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
resolveMarkerCluster,
|
|
40
40
|
resolveMetrics,
|
|
41
41
|
resolveMultiSeriesDot,
|
|
42
|
+
resolveFling,
|
|
42
43
|
resolveOverscroll,
|
|
43
44
|
resolveReturnToLiveMs,
|
|
44
45
|
resolveScrub,
|
|
@@ -139,6 +140,7 @@ function useLiveChartSeriesController({
|
|
|
139
140
|
font: fontProp,
|
|
140
141
|
insets,
|
|
141
142
|
style,
|
|
143
|
+
seriesOpacity,
|
|
142
144
|
canvasMode = "transparent",
|
|
143
145
|
timeWindow = 30,
|
|
144
146
|
paused = false,
|
|
@@ -190,6 +192,8 @@ function useLiveChartSeriesController({
|
|
|
190
192
|
renderOffAxisReferenceLine,
|
|
191
193
|
leftEdgeFade = true,
|
|
192
194
|
}: LiveChartSeriesProps) {
|
|
195
|
+
const fullSeriesOpacity = useSharedValue(1);
|
|
196
|
+
const resolvedSeriesOpacity = seriesOpacity ?? fullSeriesOpacity;
|
|
193
197
|
const emptyMarkers = useSharedValue<Marker[]>([]);
|
|
194
198
|
const markersSV = markers ?? emptyMarkers;
|
|
195
199
|
const markerClusterCfg = resolveMarkerCluster(markerCluster);
|
|
@@ -218,6 +222,8 @@ function useLiveChartSeriesController({
|
|
|
218
222
|
const timeScrollOverscroll = timeScrollEnabled
|
|
219
223
|
? resolveOverscroll(timeScroll)
|
|
220
224
|
: 0;
|
|
225
|
+
// Release inertia (fling) — `timeScroll.fling: false` stops the pan dead.
|
|
226
|
+
const timeScrollFling = resolveFling(timeScroll);
|
|
221
227
|
const zoomCfg = resolveZoom(zoom);
|
|
222
228
|
const zoomEnabled = zoomCfg !== null;
|
|
223
229
|
const scrollGestureMode =
|
|
@@ -518,6 +524,7 @@ function useLiveChartSeriesController({
|
|
|
518
524
|
enabled: timeScrollEnabled,
|
|
519
525
|
mode: scrollGestureMode,
|
|
520
526
|
overscroll: timeScrollOverscroll,
|
|
527
|
+
fling: timeScrollFling,
|
|
521
528
|
scrollActive,
|
|
522
529
|
// Once a scrub is engaged the chart is locked: scrolling goes inert so the
|
|
523
530
|
// finger only moves the price indicator across a fixed window.
|
|
@@ -589,6 +596,7 @@ function useLiveChartSeriesController({
|
|
|
589
596
|
return {
|
|
590
597
|
// passthrough props the render needs
|
|
591
598
|
series,
|
|
599
|
+
seriesOpacity: resolvedSeriesOpacity,
|
|
592
600
|
style,
|
|
593
601
|
canvasMode,
|
|
594
602
|
accessibilityLabel,
|
|
@@ -700,6 +708,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
700
708
|
overlayScrubFade,
|
|
701
709
|
renderMarker,
|
|
702
710
|
series,
|
|
711
|
+
seriesOpacity,
|
|
703
712
|
emptyText,
|
|
704
713
|
loadingAxisLabels,
|
|
705
714
|
metricsCfg,
|
|
@@ -750,30 +759,32 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
750
759
|
))}
|
|
751
760
|
</Group>
|
|
752
761
|
|
|
753
|
-
{
|
|
762
|
+
<Group opacity={seriesOpacity}>
|
|
763
|
+
{dotCfg.valueLine && (
|
|
764
|
+
<Group opacity={reveal.lineOpacity}>
|
|
765
|
+
<MultiSeriesValueLines
|
|
766
|
+
engine={engine}
|
|
767
|
+
padding={effectivePadding}
|
|
768
|
+
colors={lineColors}
|
|
769
|
+
config={dotCfg.valueLine}
|
|
770
|
+
seriesCount={activeSeriesCount}
|
|
771
|
+
/>
|
|
772
|
+
</Group>
|
|
773
|
+
)}
|
|
774
|
+
|
|
754
775
|
<Group opacity={reveal.lineOpacity}>
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
776
|
+
{Array.from({ length: activeSeriesCount }, (_, i) => (
|
|
777
|
+
<MultiSeriesStroke
|
|
778
|
+
key={i}
|
|
779
|
+
index={i}
|
|
780
|
+
paths={linePaths}
|
|
781
|
+
opacities={engine.seriesOpacities}
|
|
782
|
+
series={effectiveSeries}
|
|
783
|
+
strokeWidth={strokeWidth}
|
|
784
|
+
lineStyle={lineStyles[i]}
|
|
785
|
+
/>
|
|
786
|
+
))}
|
|
762
787
|
</Group>
|
|
763
|
-
)}
|
|
764
|
-
|
|
765
|
-
<Group opacity={reveal.lineOpacity}>
|
|
766
|
-
{Array.from({ length: activeSeriesCount }, (_, i) => (
|
|
767
|
-
<MultiSeriesStroke
|
|
768
|
-
key={i}
|
|
769
|
-
index={i}
|
|
770
|
-
paths={linePaths}
|
|
771
|
-
opacities={engine.seriesOpacities}
|
|
772
|
-
series={effectiveSeries}
|
|
773
|
-
strokeWidth={strokeWidth}
|
|
774
|
-
lineStyle={lineStyles[i]}
|
|
775
|
-
/>
|
|
776
|
-
))}
|
|
777
788
|
</Group>
|
|
778
789
|
|
|
779
790
|
{xAxisCfg && (
|
|
@@ -787,19 +798,21 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
787
798
|
)}
|
|
788
799
|
|
|
789
800
|
{dotCfg.show && (
|
|
790
|
-
<Group opacity={
|
|
791
|
-
<
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
801
|
+
<Group opacity={seriesOpacity}>
|
|
802
|
+
<Group opacity={reveal.dotOpacity}>
|
|
803
|
+
<MultiSeriesDots
|
|
804
|
+
engine={engine}
|
|
805
|
+
padding={effectivePadding}
|
|
806
|
+
colors={lineColors}
|
|
807
|
+
radius={dotCfg.radius}
|
|
808
|
+
ring={dotCfg.ring}
|
|
809
|
+
ringColor={palette.badgeOuterBg}
|
|
810
|
+
color={dotCfg.color}
|
|
811
|
+
pulse={dotCfg.pulse}
|
|
812
|
+
viewEnd={engine.viewEnd}
|
|
813
|
+
seriesCount={activeSeriesCount}
|
|
814
|
+
/>
|
|
815
|
+
</Group>
|
|
803
816
|
</Group>
|
|
804
817
|
)}
|
|
805
818
|
|
|
@@ -872,21 +885,24 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
872
885
|
lineColors,
|
|
873
886
|
skiaFont,
|
|
874
887
|
reveal,
|
|
888
|
+
seriesOpacity,
|
|
875
889
|
degenShakeTransform,
|
|
876
890
|
activeSeriesCount,
|
|
877
891
|
} = model;
|
|
878
892
|
if (!dotCfg.valueLabel) return null;
|
|
879
893
|
return (
|
|
880
894
|
<Group transform={degenShakeTransform}>
|
|
881
|
-
<Group opacity={
|
|
882
|
-
<
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
895
|
+
<Group opacity={seriesOpacity}>
|
|
896
|
+
<Group opacity={reveal.dotOpacity}>
|
|
897
|
+
<MultiSeriesValueLabels
|
|
898
|
+
engine={engine}
|
|
899
|
+
padding={effectivePadding}
|
|
900
|
+
colors={lineColors}
|
|
901
|
+
font={skiaFont}
|
|
902
|
+
dotRadius={dotOuterRadius}
|
|
903
|
+
seriesCount={activeSeriesCount}
|
|
904
|
+
/>
|
|
905
|
+
</Group>
|
|
890
906
|
</Group>
|
|
891
907
|
</Group>
|
|
892
908
|
);
|
|
@@ -371,7 +371,11 @@ export function tickLiveChartEngineFrame(
|
|
|
371
371
|
: 1;
|
|
372
372
|
if (yScale !== 1) {
|
|
373
373
|
const scaledMid = (tMin + tMax) / 2;
|
|
374
|
-
|
|
374
|
+
let scaledHalf = ((tMax - tMin) / 2) * yScale;
|
|
375
|
+
// On non-negative charts, cap the zoom-out at the point where the floor
|
|
376
|
+
// reaches 0. Letting it grow and clamping min afterwards keeps only the
|
|
377
|
+
// top expanding, which pins the data to the bottom of the plot.
|
|
378
|
+
if (input.nonNegative && scaledHalf > scaledMid) scaledHalf = scaledMid;
|
|
375
379
|
const scaledMin = scaledMid - scaledHalf;
|
|
376
380
|
const scaledMax = scaledMid + scaledHalf;
|
|
377
381
|
// Reject multiplication overflow and subnormal scales that round the two
|
|
@@ -305,7 +305,11 @@ export function tickLiveChartSeriesEngineFrame(
|
|
|
305
305
|
: 1;
|
|
306
306
|
if (yScale !== 1) {
|
|
307
307
|
const scaledMid = (tMin + tMax) / 2;
|
|
308
|
-
|
|
308
|
+
let scaledHalf = ((tMax - tMin) / 2) * yScale;
|
|
309
|
+
// On non-negative charts, cap the zoom-out at the point where the floor
|
|
310
|
+
// reaches 0. Letting it grow and clamping min afterwards keeps only the
|
|
311
|
+
// top expanding, which pins the data to the bottom of the plot.
|
|
312
|
+
if (input.nonNegative && scaledHalf > scaledMid) scaledHalf = scaledMid;
|
|
309
313
|
const scaledMin = scaledMid - scaledHalf;
|
|
310
314
|
const scaledMax = scaledMid + scaledHalf;
|
|
311
315
|
if (
|
|
@@ -582,6 +582,18 @@ export function resolveOverscroll(
|
|
|
582
582
|
return v < MAX_OVERSCROLL ? v : MAX_OVERSCROLL;
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
+
/**
|
|
586
|
+
* Resolves `timeScroll.fling`: `false` disables release inertia so the window
|
|
587
|
+
* stops dead where the finger lifts. Booleans / omitted keep the default (on).
|
|
588
|
+
* See {@link TimeScrollConfig}.
|
|
589
|
+
*/
|
|
590
|
+
export function resolveFling(
|
|
591
|
+
prop: boolean | TimeScrollConfig | undefined,
|
|
592
|
+
): boolean {
|
|
593
|
+
if (prop == null || typeof prop === "boolean") return true;
|
|
594
|
+
return prop.fling ?? true;
|
|
595
|
+
}
|
|
596
|
+
|
|
585
597
|
/**
|
|
586
598
|
* Resolved transition durations. `undefined` for a field means "use the
|
|
587
599
|
* component's built-in default" (so we don't duplicate the default constants
|
|
@@ -875,6 +887,7 @@ export function resolveMarkerCluster(
|
|
|
875
887
|
overlap: clamp01(prop.overlap ?? MARKER_CLUSTER_OVERLAP),
|
|
876
888
|
gap: MARKER_CLUSTER_GAP,
|
|
877
889
|
maxBeforeGroup: prop.maxBeforeGroup ?? MARKER_CLUSTER_MAX_BEFORE_GROUP,
|
|
890
|
+
maxVisible: prop.maxVisible ?? Number.MAX_SAFE_INTEGER,
|
|
878
891
|
groupBadge: prop.groupBadge ?? "count",
|
|
879
892
|
showGroupCount: prop.showGroupCount ?? false,
|
|
880
893
|
};
|
|
@@ -885,6 +898,7 @@ export function resolveMarkerCluster(
|
|
|
885
898
|
overlap: MARKER_CLUSTER_OVERLAP,
|
|
886
899
|
gap: MARKER_CLUSTER_GAP,
|
|
887
900
|
maxBeforeGroup: MARKER_CLUSTER_MAX_BEFORE_GROUP,
|
|
901
|
+
maxVisible: Number.MAX_SAFE_INTEGER,
|
|
888
902
|
groupBadge: "count",
|
|
889
903
|
showGroupCount: false,
|
|
890
904
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @see https://github.com/benjitaylor/liveline
|
|
7
7
|
*/
|
|
8
|
-
import { useEffect, useRef, useState } from "react";
|
|
8
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
9
9
|
import {
|
|
10
10
|
cancelAnimation,
|
|
11
11
|
Easing,
|
|
@@ -497,7 +497,11 @@ export function useLiveChartEngine(
|
|
|
497
497
|
// Compare against the previous key (not a "first render" flag) so React 18
|
|
498
498
|
// StrictMode's double-invoked mount effect can't fire a spurious snap.
|
|
499
499
|
const lastSnapKey = useRef(config.snapKey);
|
|
500
|
-
|
|
500
|
+
// Layout effect, not passive: a passive effect runs after the commit has
|
|
501
|
+
// painted, so the frame between them draws the new data/candleWidth against
|
|
502
|
+
// the OLD framing (a one-frame squeeze on a timeframe switch). Flipping the
|
|
503
|
+
// flag before paint lets the first frame with the new props consume the snap.
|
|
504
|
+
useLayoutEffect(() => {
|
|
501
505
|
if (config.snapKey === lastSnapKey.current) return;
|
|
502
506
|
lastSnapKey.current = config.snapKey;
|
|
503
507
|
snapSV.set(true);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
cancelAnimation,
|
|
4
4
|
Easing,
|
|
@@ -343,7 +343,9 @@ export function useLiveChartSeriesEngine(
|
|
|
343
343
|
// changes, consumed + cleared by the next frame (mirrors useLiveChartEngine).
|
|
344
344
|
const snapSV = useSharedValue(false);
|
|
345
345
|
const lastSnapKey = useRef(config.snapKey);
|
|
346
|
-
|
|
346
|
+
// Layout effect so the snap flag lands before the commit paints — a passive
|
|
347
|
+
// effect leaves one frame of new data in the old framing (mirrors useLiveChartEngine).
|
|
348
|
+
useLayoutEffect(() => {
|
|
347
349
|
if (config.snapKey === lastSnapKey.current) return;
|
|
348
350
|
lastSnapKey.current = config.snapKey;
|
|
349
351
|
snapSV.set(true);
|
|
@@ -17,33 +17,25 @@ import { usePathBuilder } from "./usePathBuilder";
|
|
|
17
17
|
const CANDLE_WIDTH_LERP_SPEED = 0.08;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* Bridges the `candleWidth` prop to the UI thread and eases the displayed
|
|
21
|
+
* width toward it each frame. Must be called from the chart's OUTER component
|
|
22
|
+
* (the controller), not from inside the Skia canvas: canvas children render
|
|
23
|
+
* through Skia's own reconciler one commit behind the outer tree, so a width
|
|
24
|
+
* bridged in there lands one frame after the engine's framing targets — a
|
|
25
|
+
* timeframe switch then draws one frame of new data at the old width. See #176.
|
|
24
26
|
*/
|
|
25
|
-
export function
|
|
26
|
-
engine: SingleEngineState,
|
|
27
|
-
padding: ChartPadding,
|
|
28
|
-
candles: SharedValue<CandlePoint[]> | undefined,
|
|
29
|
-
liveCandle: SharedValue<CandlePoint | null> | undefined,
|
|
27
|
+
export function useCandleWidthLerp(
|
|
30
28
|
candleWidthSecs: number,
|
|
31
|
-
active: boolean,
|
|
32
|
-
candleMetrics: CandleMetrics = CANDLE_METRICS_DEFAULTS,
|
|
33
|
-
/** Reserved volume-band height (px). `0` = no volume bars. */
|
|
34
|
-
volumeBandHeight = 0,
|
|
35
|
-
/** Corner radius (px) of the volume bars. */
|
|
36
|
-
volumeRadius = 0,
|
|
37
|
-
/** Static charts run no loops: register without starting. Default `true`. */
|
|
38
|
-
autostart = true,
|
|
39
29
|
/**
|
|
40
30
|
* Per-frame lerp speed (0–1) for the candle-body width as it eases toward
|
|
41
31
|
* `candleWidthSecs`. `1` snaps in one frame (no "fat → thin" slide on a
|
|
42
32
|
* timeframe / bucket change); `undefined` uses {@link CANDLE_WIDTH_LERP_SPEED}.
|
|
43
33
|
* Resolved from `transitions.candleLerpSpeed`. See #176.
|
|
44
34
|
*/
|
|
45
|
-
candleLerpSpeed
|
|
46
|
-
|
|
35
|
+
candleLerpSpeed: number | undefined,
|
|
36
|
+
/** Static charts run no loops: register without starting. */
|
|
37
|
+
autostart: boolean,
|
|
38
|
+
): SharedValue<number> {
|
|
47
39
|
const targetCandleWidth = useDerivedValue(() => candleWidthSecs);
|
|
48
40
|
const displayCandleWidth = useSharedValue(candleWidthSecs);
|
|
49
41
|
// `transitions.candleLerpSpeed` overrides the built-in speed (0–1, already
|
|
@@ -54,16 +46,8 @@ export function useCandlePaths(
|
|
|
54
46
|
() => candleLerpSpeed ?? CANDLE_WIDTH_LERP_SPEED,
|
|
55
47
|
);
|
|
56
48
|
|
|
57
|
-
const upBodiesBuilder = usePathBuilder();
|
|
58
|
-
const downBodiesBuilder = usePathBuilder();
|
|
59
|
-
const upWicksBuilder = usePathBuilder();
|
|
60
|
-
const downWicksBuilder = usePathBuilder();
|
|
61
|
-
const upBarsBuilder = usePathBuilder();
|
|
62
|
-
const downBarsBuilder = usePathBuilder();
|
|
63
|
-
|
|
64
49
|
useFrameCallback((frameInfo) => {
|
|
65
50
|
"worklet";
|
|
66
|
-
if (!active) return;
|
|
67
51
|
const dt = frameInfo.timeSincePreviousFrame ?? MS_PER_FRAME_60FPS;
|
|
68
52
|
displayCandleWidth.set(
|
|
69
53
|
lerp(
|
|
@@ -75,6 +59,36 @@ export function useCandlePaths(
|
|
|
75
59
|
);
|
|
76
60
|
}, autostart);
|
|
77
61
|
|
|
62
|
+
return displayCandleWidth;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Candle paths (up/down bodies + up/down wicks). Each is built into a reused
|
|
67
|
+
* `Skia.PathBuilder` and finalized with `detach()` each frame — a fresh
|
|
68
|
+
* immutable `SkPath`, so Skia repaints without a per-curve ping-pong and no
|
|
69
|
+
* mutable `SkPath` is retained across frames.
|
|
70
|
+
*/
|
|
71
|
+
export function useCandlePaths(
|
|
72
|
+
engine: SingleEngineState,
|
|
73
|
+
padding: ChartPadding,
|
|
74
|
+
candles: SharedValue<CandlePoint[]> | undefined,
|
|
75
|
+
liveCandle: SharedValue<CandlePoint | null> | undefined,
|
|
76
|
+
/** Displayed bucket width from {@link useCandleWidthLerp} (outer component). */
|
|
77
|
+
displayCandleWidth: SharedValue<number>,
|
|
78
|
+
active: boolean,
|
|
79
|
+
candleMetrics: CandleMetrics = CANDLE_METRICS_DEFAULTS,
|
|
80
|
+
/** Reserved volume-band height (px). `0` = no volume bars. */
|
|
81
|
+
volumeBandHeight = 0,
|
|
82
|
+
/** Corner radius (px) of the volume bars. */
|
|
83
|
+
volumeRadius = 0,
|
|
84
|
+
) {
|
|
85
|
+
const upBodiesBuilder = usePathBuilder();
|
|
86
|
+
const downBodiesBuilder = usePathBuilder();
|
|
87
|
+
const upWicksBuilder = usePathBuilder();
|
|
88
|
+
const downWicksBuilder = usePathBuilder();
|
|
89
|
+
const upBarsBuilder = usePathBuilder();
|
|
90
|
+
const downBarsBuilder = usePathBuilder();
|
|
91
|
+
|
|
78
92
|
/* istanbul ignore next -- worklet */
|
|
79
93
|
const geometry = useDerivedValue(() => {
|
|
80
94
|
if (!active || !candles) return { bodies: [], wicks: [] };
|
|
@@ -51,8 +51,9 @@ export interface ChartRevealState {
|
|
|
51
51
|
/**
|
|
52
52
|
* Drives loading / empty / live visibility.
|
|
53
53
|
*
|
|
54
|
-
* Chart is fully revealed only when `!loading && hasData`.
|
|
55
|
-
*
|
|
54
|
+
* Chart is fully revealed only when `!loading && hasData`. Data appearing
|
|
55
|
+
* grows `morphT` from 0 to 1; data disappearing snaps it to 0 so stale paths
|
|
56
|
+
* never visibly flatten over a loading or empty shell. `isEmpty` is derived as
|
|
56
57
|
* `!loading && !hasData` for the empty overlay label.
|
|
57
58
|
*/
|
|
58
59
|
export function useChartReveal(
|
|
@@ -87,11 +88,18 @@ export function useChartReveal(
|
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
if (prev !== chartVisible) {
|
|
91
|
+
// Collapse (live → loading/empty) snaps: the data is already gone, so
|
|
92
|
+
// animating would draw the stale line morphing flat over the loading
|
|
93
|
+
// shell.
|
|
94
|
+
if (!chartVisible) {
|
|
95
|
+
morphT.set(0);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
90
98
|
// 0ms → withTiming resolves on the next frame (effectively a snap), so an
|
|
91
99
|
// explicit `transitions={{ reveal: 0 }}` / `transitions={false}` removes
|
|
92
100
|
// the grow-in without a special-case branch.
|
|
93
101
|
morphT.set(
|
|
94
|
-
withTiming(
|
|
102
|
+
withTiming(1, {
|
|
95
103
|
duration: revealDuration,
|
|
96
104
|
easing: Easing.out(Easing.cubic),
|
|
97
105
|
}),
|
package/src/hooks/useMarkers.ts
CHANGED
|
@@ -81,6 +81,13 @@ export interface UsePanScrollOptions {
|
|
|
81
81
|
* `timeScroll.overscroll` (see `resolveOverscroll`).
|
|
82
82
|
*/
|
|
83
83
|
overscroll?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Fling inertia on release: a fast drag keeps scrolling and decays to a stop.
|
|
86
|
+
* `false` stops the window dead where the finger lifts (a release near the
|
|
87
|
+
* live edge still re-attaches to live). Resolved from `timeScroll.fling`
|
|
88
|
+
* (see `resolveFling`). Default `true`.
|
|
89
|
+
*/
|
|
90
|
+
fling?: boolean;
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
/**
|
|
@@ -218,6 +225,7 @@ export function usePanScroll({
|
|
|
218
225
|
scrollActive,
|
|
219
226
|
scrubActive,
|
|
220
227
|
overscroll = 0,
|
|
228
|
+
fling = true,
|
|
221
229
|
}: UsePanScrollOptions): ReturnType<typeof Gesture.Pan> {
|
|
222
230
|
const { viewEnd, liveEdge, displayWindow, canvasWidth, canvasHeight } = engine;
|
|
223
231
|
const padLeft = padding.left;
|
|
@@ -307,7 +315,10 @@ export function usePanScroll({
|
|
|
307
315
|
// to FOLLOW_SNAP of the window around it — this release callback is the
|
|
308
316
|
// ONLY place that re-attaches to live (never mid-drag, see nextViewEnd).
|
|
309
317
|
const snapZone = overscroll > 0 ? win * FOLLOW_SNAP : 1e-3;
|
|
310
|
-
|
|
318
|
+
// `fling: false` → zero velocity: the decay resolves immediately where
|
|
319
|
+
// the finger lifted, and the completion callback below still re-attaches
|
|
320
|
+
// to live when the release lands inside the snap zone.
|
|
321
|
+
const velocity = fling ? flingVelocity(e.velocityX, chartW, win) : 0;
|
|
311
322
|
cancelAnimation(viewEnd);
|
|
312
323
|
viewEnd.set(
|
|
313
324
|
withDecay({ velocity, clamp: [lo, hi] }, (finished) => {
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,9 @@ export interface ResolvedMarkerCluster {
|
|
|
20
20
|
gap: number;
|
|
21
21
|
/** Collapse a co-located run to a single count badge once it exceeds this many. */
|
|
22
22
|
maxBeforeGroup: number;
|
|
23
|
+
/** Cap a `"vertical"` column at this many glyphs: the newest overflow is
|
|
24
|
+
* hidden instead of the column growing unbounded. */
|
|
25
|
+
maxVisible: number;
|
|
23
26
|
/** What a collapsed group draws: `"count"` = the round count badge (default);
|
|
24
27
|
* `"marker"` = the representative marker's own glyph; a {@link MarkerGroupBadge}
|
|
25
28
|
* = a dedicated badge (custom image/icon) independent of the members. */
|
|
@@ -131,9 +134,20 @@ function layoutBucket(
|
|
|
131
134
|
// AWAY from the line in the side direction (above → up, below → down,
|
|
132
135
|
// center → up from the line). `j` runs in time order, so the newest sits
|
|
133
136
|
// furthest out — and, drawn last in array order, paints over the one below it.
|
|
137
|
+
//
|
|
138
|
+
// `maxVisible` caps the column: the oldest glyphs keep their slots and the
|
|
139
|
+
// newest overflow is simply hidden.
|
|
134
140
|
const dir = side === "below" ? 1 : -1;
|
|
141
|
+
const cap = opts.config.maxVisible;
|
|
135
142
|
for (let j = 0; j < count; j++) {
|
|
136
143
|
const p = proj[idx[s + j]];
|
|
144
|
+
if (j >= cap) {
|
|
145
|
+
p.hidden = true;
|
|
146
|
+
p.isGrouped = false;
|
|
147
|
+
p.groupCount = 0;
|
|
148
|
+
p.groupRep = -1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
137
151
|
p.x = anchorX;
|
|
138
152
|
p.y = anchorY + sideDy + dir * j * step;
|
|
139
153
|
p.hidden = false;
|
package/src/types.ts
CHANGED
|
@@ -737,6 +737,22 @@ export interface XAxisConfig {
|
|
|
737
737
|
minGap?: number;
|
|
738
738
|
}
|
|
739
739
|
|
|
740
|
+
/**
|
|
741
|
+
* Auto-hide both axes while idle (see {@link LiveChartCoreProps.axisAutoHide}).
|
|
742
|
+
*
|
|
743
|
+
* @experimental
|
|
744
|
+
*/
|
|
745
|
+
export interface AxisAutoHideConfig {
|
|
746
|
+
/** Fade-in duration (ms) when interaction starts — near-instant. Default `60`. */
|
|
747
|
+
fadeInMs?: number;
|
|
748
|
+
/** Fade-out duration (ms) back to idle. Default `250`. */
|
|
749
|
+
fadeOutMs?: number;
|
|
750
|
+
/** Axis opacity while idle (0 = fully hidden). Default `0`. */
|
|
751
|
+
idleOpacity?: number;
|
|
752
|
+
/** Untouched time (ms) before the axes fade back out. Default `3000`. */
|
|
753
|
+
hideAfterMs?: number;
|
|
754
|
+
}
|
|
755
|
+
|
|
740
756
|
/**
|
|
741
757
|
* Morfi-style on-canvas tooltip for {@link LiveChartSeries}: a time-range pill
|
|
742
758
|
* above the guide plus one value pill at every visible series intersection.
|
|
@@ -1360,6 +1376,12 @@ export interface MarkerClusterConfig {
|
|
|
1360
1376
|
/** Collapse a co-located run to a single count badge once it exceeds this many.
|
|
1361
1377
|
* Default `5`. */
|
|
1362
1378
|
maxBeforeGroup?: number;
|
|
1379
|
+
/**
|
|
1380
|
+
* `"vertical"` only: cap the column at this many glyphs instead of letting it
|
|
1381
|
+
* grow unbounded (and off the plot). The oldest `maxVisible` glyphs keep
|
|
1382
|
+
* their stack; the newest overflow is hidden. Default unbounded.
|
|
1383
|
+
*/
|
|
1384
|
+
maxVisible?: number;
|
|
1363
1385
|
/**
|
|
1364
1386
|
* What a collapsed cluster draws in place of its members:
|
|
1365
1387
|
* - `"count"` (default) — the built-in round count badge (a circle with the
|
|
@@ -1791,6 +1813,14 @@ export interface TimeScrollConfig {
|
|
|
1791
1813
|
* clamped. Applies to the pinch-zoom (`zoom`) clamps too.
|
|
1792
1814
|
*/
|
|
1793
1815
|
overscroll?: number;
|
|
1816
|
+
/**
|
|
1817
|
+
* Fling inertia on release: a fast drag keeps scrolling and decays to a
|
|
1818
|
+
* stop (the classic momentum feel). Set `false` to stop the window dead
|
|
1819
|
+
* where the finger lifts — a deliberate, TradingView-style hard stop. A
|
|
1820
|
+
* release that lands within the snap zone of the live edge still
|
|
1821
|
+
* re-attaches to live. Default `true`.
|
|
1822
|
+
*/
|
|
1823
|
+
fling?: boolean;
|
|
1794
1824
|
}
|
|
1795
1825
|
|
|
1796
1826
|
/**
|
|
@@ -1925,6 +1955,14 @@ export interface LiveChartCoreProps {
|
|
|
1925
1955
|
insets?: ChartInsets;
|
|
1926
1956
|
/** Container View style. */
|
|
1927
1957
|
style?: ViewStyle;
|
|
1958
|
+
/**
|
|
1959
|
+
* UI-thread opacity multiplier for the plotted data. Set a value from `0`
|
|
1960
|
+
* (hidden) to `1` (fully visible), such as `0.5` while replacement data loads.
|
|
1961
|
+
* Applies to every plotted series — line/fill/candles, live dots, value lines,
|
|
1962
|
+
* and inline values — without affecting axes, reference lines, legends, or
|
|
1963
|
+
* scrub UI. Default `1`.
|
|
1964
|
+
*/
|
|
1965
|
+
seriesOpacity?: SharedValue<number>;
|
|
1928
1966
|
/**
|
|
1929
1967
|
* Canvas composition mode. `"transparent"` keeps Skia's default compositing
|
|
1930
1968
|
* path (TextureView on Android) and can reveal content behind the chart.
|
|
@@ -1997,7 +2035,10 @@ export interface LiveChartCoreProps {
|
|
|
1997
2035
|
exaggerate?: boolean;
|
|
1998
2036
|
/**
|
|
1999
2037
|
* Clamp the Y-axis lower bound at 0 (prices, market caps, volumes) so the axis
|
|
2000
|
-
* never shows negative ticks when data collapses toward zero.
|
|
2038
|
+
* never shows negative ticks when data collapses toward zero. With
|
|
2039
|
+
* `yRangeScale` zoomed out, the expansion stops once the lower bound reaches
|
|
2040
|
+
* 0 so the data stays centered instead of continuing to grow only upward.
|
|
2041
|
+
* Default `false`.
|
|
2001
2042
|
*/
|
|
2002
2043
|
nonNegative?: boolean;
|
|
2003
2044
|
/**
|
|
@@ -2030,6 +2071,15 @@ export interface LiveChartCoreProps {
|
|
|
2030
2071
|
yAxis?: boolean | YAxisConfig;
|
|
2031
2072
|
/** X-axis time labels. `true` = defaults, `false` = hidden, or pass `XAxisConfig`. Default `true`. */
|
|
2032
2073
|
xAxis?: boolean | XAxisConfig;
|
|
2074
|
+
/**
|
|
2075
|
+
* Auto-hide both axes while the chart is at rest: the X/Y axes (grid, ticks,
|
|
2076
|
+
* labels) fade in on any interaction — scrubbing, time-scrolling, flinging,
|
|
2077
|
+
* pinch-zooming — and fade back out once the chart goes untouched for
|
|
2078
|
+
* `hideAfterMs`. `true` = defaults, or pass `AxisAutoHideConfig`. Default off.
|
|
2079
|
+
*
|
|
2080
|
+
* @experimental
|
|
2081
|
+
*/
|
|
2082
|
+
axisAutoHide?: boolean | AxisAutoHideConfig;
|
|
2033
2083
|
/**
|
|
2034
2084
|
* Label floated at the top edge of the plot area. `true` = the built-in value
|
|
2035
2085
|
* label showing the chart's current TOP Y-axis bound (updated each frame),
|