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.
Files changed (36) hide show
  1. package/dist/components/LiveChart.d.ts.map +1 -1
  2. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  3. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  4. package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
  5. package/dist/core/resolveConfig.d.ts +6 -0
  6. package/dist/core/resolveConfig.d.ts.map +1 -1
  7. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  8. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  9. package/dist/hooks/useCandlePaths.d.ts +22 -11
  10. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  11. package/dist/hooks/useChartReveal.d.ts +3 -2
  12. package/dist/hooks/useChartReveal.d.ts.map +1 -1
  13. package/dist/hooks/useMarkers.d.ts.map +1 -1
  14. package/dist/hooks/usePanScroll.d.ts +8 -1
  15. package/dist/hooks/usePanScroll.d.ts.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/math/markerCluster.d.ts +3 -0
  19. package/dist/math/markerCluster.d.ts.map +1 -1
  20. package/dist/types.d.ts +50 -1
  21. package/dist/types.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/src/components/LiveChart.tsx +236 -123
  24. package/src/components/LiveChartSeries.tsx +60 -44
  25. package/src/core/liveChartEngineTick.ts +5 -1
  26. package/src/core/liveChartSeriesEngineTick.ts +5 -1
  27. package/src/core/resolveConfig.ts +14 -0
  28. package/src/core/useLiveChartEngine.ts +6 -2
  29. package/src/core/useLiveChartSeriesEngine.ts +4 -2
  30. package/src/hooks/useCandlePaths.ts +41 -27
  31. package/src/hooks/useChartReveal.ts +11 -3
  32. package/src/hooks/useMarkers.ts +1 -0
  33. package/src/hooks/usePanScroll.ts +12 -1
  34. package/src/index.ts +1 -0
  35. package/src/math/markerCluster.ts +14 -0
  36. 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
- {dotCfg.valueLine && (
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
- <MultiSeriesValueLines
756
- engine={engine}
757
- padding={effectivePadding}
758
- colors={lineColors}
759
- config={dotCfg.valueLine}
760
- seriesCount={activeSeriesCount}
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={reveal.dotOpacity}>
791
- <MultiSeriesDots
792
- engine={engine}
793
- padding={effectivePadding}
794
- colors={lineColors}
795
- radius={dotCfg.radius}
796
- ring={dotCfg.ring}
797
- ringColor={palette.badgeOuterBg}
798
- color={dotCfg.color}
799
- pulse={dotCfg.pulse}
800
- viewEnd={engine.viewEnd}
801
- seriesCount={activeSeriesCount}
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={reveal.dotOpacity}>
882
- <MultiSeriesValueLabels
883
- engine={engine}
884
- padding={effectivePadding}
885
- colors={lineColors}
886
- font={skiaFont}
887
- dotRadius={dotOuterRadius}
888
- seriesCount={activeSeriesCount}
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
- const scaledHalf = ((tMax - tMin) / 2) * yScale;
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
- const scaledHalf = ((tMax - tMin) / 2) * yScale;
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
- useEffect(() => {
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 { useEffect, useRef, useState } from "react";
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
- useEffect(() => {
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
- * Candle paths (up/down bodies + up/down wicks). Each is built into a reused
21
- * `Skia.PathBuilder` and finalized with `detach()` each frame — a fresh
22
- * immutable `SkPath`, so Skia repaints without a per-curve ping-pong and no
23
- * mutable `SkPath` is retained across frames.
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 useCandlePaths(
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?: number,
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`. `morphT` animates
55
- * between 0 and 1 when that condition changes. `isEmpty` is derived as
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(chartVisible ? 1 : 0, {
102
+ withTiming(1, {
95
103
  duration: revealDuration,
96
104
  easing: Easing.out(Easing.cubic),
97
105
  }),
@@ -28,6 +28,7 @@ const ANCHORED_CLUSTER: ResolvedMarkerCluster = {
28
28
  overlap: 0.75,
29
29
  gap: 2,
30
30
  maxBeforeGroup: 5,
31
+ maxVisible: Number.MAX_SAFE_INTEGER,
31
32
  groupBadge: "count",
32
33
  showGroupCount: false,
33
34
  };
@@ -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
- const velocity = flingVelocity(e.velocityX, chartW, win);
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
@@ -33,6 +33,7 @@ export { usePriceY, useTimeX } from "./hooks/useChartOverlayContext";
33
33
 
34
34
  export type {
35
35
  AreaDotsConfig,
36
+ AxisAutoHideConfig,
36
37
  AxisLabelConfig,
37
38
  BadgeConfig,
38
39
  BadgeMetrics,
@@ -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. Default `false`.
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),