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
@@ -2,10 +2,13 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { StyleSheet, View } from "react-native";
3
3
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
4
  import Animated, {
5
+ cancelAnimation,
5
6
  useAnimatedReaction,
6
7
  useAnimatedStyle,
7
8
  useDerivedValue,
8
9
  useSharedValue,
10
+ withDelay,
11
+ withSequence,
9
12
  withTiming,
10
13
  } from "react-native-reanimated";
11
14
  import { scheduleOnRN } from "react-native-worklets";
@@ -46,6 +49,7 @@ import {
46
49
  resolveScrub,
47
50
  resolveScrubAction,
48
51
  resolveTransitions,
52
+ resolveFling,
49
53
  resolveOverscroll,
50
54
  resolveReturnToLiveMs,
51
55
  resolveSelectionDot,
@@ -68,7 +72,7 @@ import { useLiveChartEngine } from "../core/useLiveChartEngine";
68
72
  import { pulseRadialOutset } from "../draw/line";
69
73
  import { resolveChartLayout } from "../hooks/resolveChartLayout";
70
74
  import { useBadge } from "../hooks/useBadge";
71
- import { useCandlePaths } from "../hooks/useCandlePaths";
75
+ import { useCandlePaths, useCandleWidthLerp } from "../hooks/useCandlePaths";
72
76
  import { useCanvasLayout } from "../hooks/useCanvasLayout";
73
77
  import { useChartColors } from "../hooks/useChartColors";
74
78
  import { useChartOverlayContext } from "../hooks/useChartOverlayContext";
@@ -257,6 +261,7 @@ function useLiveChartController({
257
261
  font: fontProp,
258
262
  insets,
259
263
  style,
264
+ seriesOpacity,
260
265
  canvasMode = "transparent",
261
266
 
262
267
  // ── Candlestick ─────────────────────────────────────────────────────────
@@ -293,6 +298,7 @@ function useLiveChartController({
293
298
  // ── Overlays ────────────────────────────────────────────────────────────
294
299
  yAxis = true,
295
300
  xAxis = true,
301
+ axisAutoHide = false,
296
302
  topLabel,
297
303
  bottomLabel,
298
304
  badge = true,
@@ -335,6 +341,8 @@ function useLiveChartController({
335
341
  onReachStart,
336
342
  onDegenShake,
337
343
  }: LiveChartProps) {
344
+ const fullSeriesOpacity = useSharedValue(1);
345
+ const resolvedSeriesOpacity = seriesOpacity ?? fullSeriesOpacity;
338
346
  const emptyMarkers = useSharedValue<Marker[]>([]);
339
347
  const markersSV = markers ?? emptyMarkers;
340
348
  // Stand-in threshold value so `useThreshold` can be called unconditionally
@@ -614,6 +622,8 @@ function useLiveChartController({
614
622
  const timeScrollOverscroll = timeScrollEnabled
615
623
  ? resolveOverscroll(timeScroll)
616
624
  : 0;
625
+ // Release inertia (fling) — `timeScroll.fling: false` stops the pan dead.
626
+ const timeScrollFling = resolveFling(timeScroll);
617
627
  const zoomCfg = resolveZoom(zoom);
618
628
  const zoomEnabled = zoomCfg !== null && !isStatic;
619
629
 
@@ -947,6 +957,15 @@ function useLiveChartController({
947
957
  : momentumSV.value,
948
958
  );
949
959
 
960
+ // Width bridge lives here (outer tree), not in ChartCandleLayer: canvas
961
+ // children commit one frame behind, which would lag the width target behind
962
+ // the engine's framing snap on a timeframe switch. See useCandleWidthLerp.
963
+ const displayCandleWidth = useCandleWidthLerp(
964
+ candleWidth,
965
+ transitionsCfg.candleLerpSpeed,
966
+ !isStatic && isCandle,
967
+ );
968
+
950
969
  // ── Overlay hooks ─────────────────────────────────────────────────────
951
970
  // Scrub/crosshair must see the same stash-backed candles as the engine.
952
971
  const candleOpts = isCandle
@@ -1099,6 +1118,7 @@ function useLiveChartController({
1099
1118
  enabled: timeScrollEnabled,
1100
1119
  mode: scrollGestureMode,
1101
1120
  overscroll: timeScrollOverscroll,
1121
+ fling: timeScrollFling,
1102
1122
  scrollActive,
1103
1123
  // Once a scrub is engaged the chart is locked: scrolling goes inert so the
1104
1124
  // finger only moves the price indicator across a fixed window.
@@ -1128,6 +1148,78 @@ function useLiveChartController({
1128
1148
  },
1129
1149
  });
1130
1150
 
1151
+ // Axis auto-hide: fade both axes out at rest and back in while the user
1152
+ // interacts. Any movement — scrub / time-scroll touch, or a viewEnd /
1153
+ // viewWindow change (fling momentum, pinch-zoom) — shows the axes; once the
1154
+ // touch lifts and the view settles, they fade back out after `hideAfterMs`.
1155
+ // Only the axis groups' opacity animates; the axis worklets keep running
1156
+ // underneath so a fade-in shows current labels.
1157
+ const axisAutoHideCfg =
1158
+ axisAutoHide === true ? {} : axisAutoHide === false ? null : axisAutoHide;
1159
+ const axisIdleOpacity = axisAutoHideCfg?.idleOpacity ?? 0;
1160
+ const axisFadeInMs = axisAutoHideCfg?.fadeInMs ?? 60;
1161
+ const axisFadeOutMs = axisAutoHideCfg?.fadeOutMs ?? 250;
1162
+ const axisHideAfterMs = axisAutoHideCfg?.hideAfterMs ?? 3000;
1163
+ const axisAutoHideOpacity = useSharedValue(
1164
+ axisAutoHideCfg ? axisIdleOpacity : 1,
1165
+ );
1166
+ const axisAutoHideEnabled = axisAutoHideCfg !== null;
1167
+ const lastAxisAutoHide = useRef({
1168
+ enabled: axisAutoHideEnabled,
1169
+ idleOpacity: axisIdleOpacity,
1170
+ });
1171
+ // `useSharedValue` only reads its initial value on mount. Keep prop changes
1172
+ // in sync as well: turning auto-hide off must immediately restore the axes,
1173
+ // and turning it on starts from the configured idle opacity.
1174
+ useEffect(() => {
1175
+ if (
1176
+ lastAxisAutoHide.current.enabled === axisAutoHideEnabled &&
1177
+ lastAxisAutoHide.current.idleOpacity === axisIdleOpacity
1178
+ ) {
1179
+ return;
1180
+ }
1181
+ lastAxisAutoHide.current = {
1182
+ enabled: axisAutoHideEnabled,
1183
+ idleOpacity: axisIdleOpacity,
1184
+ };
1185
+ cancelAnimation(axisAutoHideOpacity);
1186
+ axisAutoHideOpacity.value = axisAutoHideEnabled ? axisIdleOpacity : 1;
1187
+ }, [axisAutoHideEnabled, axisAutoHideOpacity, axisIdleOpacity]);
1188
+ useAnimatedReaction(
1189
+ () => ({
1190
+ gesture: scrollActive.value || crosshairScrubActive.value,
1191
+ viewEnd: engine.viewEnd.value,
1192
+ viewWindow: engine.viewWindow.value,
1193
+ }),
1194
+ (curr, prev) => {
1195
+ if (!axisAutoHideEnabled || prev === null) return;
1196
+ const moved =
1197
+ curr.gesture !== prev.gesture ||
1198
+ curr.viewEnd !== prev.viewEnd ||
1199
+ curr.viewWindow !== prev.viewWindow;
1200
+ if (!moved) return;
1201
+ cancelAnimation(axisAutoHideOpacity);
1202
+ axisAutoHideOpacity.value = curr.gesture
1203
+ ? withTiming(1, { duration: axisFadeInMs })
1204
+ : // Movement without a held touch (gesture end, fling, pinch): show,
1205
+ // then fade back out once the chart goes untouched for a while.
1206
+ withSequence(
1207
+ withTiming(1, { duration: axisFadeInMs }),
1208
+ withDelay(
1209
+ axisHideAfterMs,
1210
+ withTiming(axisIdleOpacity, { duration: axisFadeOutMs }),
1211
+ ),
1212
+ );
1213
+ },
1214
+ [
1215
+ axisAutoHideEnabled,
1216
+ axisIdleOpacity,
1217
+ axisFadeInMs,
1218
+ axisFadeOutMs,
1219
+ axisHideAfterMs,
1220
+ ],
1221
+ );
1222
+
1131
1223
  // Paging callbacks: report the visible range / proximity to the oldest data so
1132
1224
  // a host can lazily load history. Inert unless a callback is supplied.
1133
1225
  useVisibleRange({
@@ -1230,7 +1322,8 @@ function useLiveChartController({
1230
1322
  liveIndicatorScrollOpacity(
1231
1323
  hideLiveOnScrollBack,
1232
1324
  engine.viewEnd.value,
1233
- ),
1325
+ ) *
1326
+ resolvedSeriesOpacity.value,
1234
1327
  );
1235
1328
  // Same scrolled-back gating for the value line: it would draw a dashed line
1236
1329
  // at the live value's Y — a price that isn't in the scrolled-back view.
@@ -1240,7 +1333,8 @@ function useLiveChartController({
1240
1333
  liveIndicatorScrollOpacity(
1241
1334
  hideLiveOnScrollBack,
1242
1335
  engine.viewEnd.value,
1243
- ),
1336
+ ) *
1337
+ resolvedSeriesOpacity.value,
1244
1338
  );
1245
1339
  const liveBadgeOpacity = useDerivedValue(
1246
1340
  () =>
@@ -1248,7 +1342,8 @@ function useLiveChartController({
1248
1342
  liveIndicatorScrollOpacity(
1249
1343
  hideLiveOnScrollBack,
1250
1344
  engine.viewEnd.value,
1251
- ),
1345
+ ) *
1346
+ resolvedSeriesOpacity.value,
1252
1347
  );
1253
1348
 
1254
1349
  // Fade the annotation overlays (markers + reference lines) out while scrubbing
@@ -1280,6 +1375,7 @@ function useLiveChartController({
1280
1375
  showValue,
1281
1376
  valueMomentumColor,
1282
1377
  lineProp,
1378
+ seriesOpacity: resolvedSeriesOpacity,
1283
1379
  formatValue,
1284
1380
  formatTime,
1285
1381
  isCandle,
@@ -1351,6 +1447,7 @@ function useLiveChartController({
1351
1447
  // engine + reveal
1352
1448
  engine,
1353
1449
  reveal,
1450
+ axisAutoHideOpacity,
1354
1451
  // loading shell styling (null → not loading)
1355
1452
  loadingLineColor: loadingCfg?.color,
1356
1453
  loadingStrokeWidth: loadingCfg?.strokeWidth,
@@ -1369,6 +1466,7 @@ function useLiveChartController({
1369
1466
  candlesEngine,
1370
1467
  liveEngine,
1371
1468
  candleWidth,
1469
+ displayCandleWidth,
1372
1470
  transitionsCfg,
1373
1471
  layoutWidth,
1374
1472
  onLayout,
@@ -1495,9 +1593,15 @@ function ChartYAxisLayer({
1495
1593
  yAxisFloat,
1496
1594
  yAxisCfg,
1497
1595
  liveBadgeOpacity,
1596
+ axisAutoHideOpacity,
1498
1597
  } = model;
1598
+ // Fold the axis auto-hide fade into the reveal opacity (1 when the feature
1599
+ // is off).
1600
+ const yAxisGroupOpacity = useDerivedValue(
1601
+ () => reveal.yAxisOpacity.value * axisAutoHideOpacity.value,
1602
+ );
1499
1603
  return (
1500
- <Group opacity={reveal.yAxisOpacity}>
1604
+ <Group opacity={yAxisGroupOpacity}>
1501
1605
  <YAxisOverlay
1502
1606
  variant={variant}
1503
1607
  float={variant === "labels" && yAxisFloat}
@@ -1538,14 +1642,17 @@ function ChartXAxisLayer({ model }: { model: LiveChartModel }) {
1538
1642
  skiaFont,
1539
1643
  );
1540
1644
  return (
1541
- <XAxisOverlay
1542
- entries={xAxisEntries}
1543
- engine={engine}
1544
- padding={effectivePadding}
1545
- palette={palette}
1546
- font={skiaFont}
1547
- volumeBandHeight={volumeBandHeight}
1548
- />
1645
+ // Axis auto-hide fade (1 when the feature is off).
1646
+ <Group opacity={model.axisAutoHideOpacity}>
1647
+ <XAxisOverlay
1648
+ entries={xAxisEntries}
1649
+ engine={engine}
1650
+ padding={effectivePadding}
1651
+ palette={palette}
1652
+ font={skiaFont}
1653
+ volumeBandHeight={volumeBandHeight}
1654
+ />
1655
+ </Group>
1549
1656
  );
1550
1657
  }
1551
1658
 
@@ -1583,6 +1690,7 @@ function ChartFillLayer({
1583
1690
  thresholdIsSeries,
1584
1691
  thresholdSeriesHasPoints,
1585
1692
  thresholdFillUniforms,
1693
+ seriesOpacity,
1586
1694
  } = model;
1587
1695
  return (
1588
1696
  <Group transform={degen?.shakeTransform}>
@@ -1597,62 +1705,64 @@ function ChartFillLayer({
1597
1705
  />
1598
1706
  )}
1599
1707
 
1600
- {/* Dot-lattice area fill (the under-line `fillPath` painted with a dot
1601
- shader). Drawn before the gradient so a gradient (if also enabled)
1602
- composites on top. */}
1603
- {areaDotsCfg && (
1604
- <Group opacity={reveal.fillOpacity}>
1605
- <AreaDotsOverlay
1606
- fillPath={fillPath}
1607
- color={areaDotColorVec}
1608
- spacing={areaDotsCfg.spacing}
1609
- size={areaDotsCfg.size}
1610
- />
1611
- </Group>
1612
- )}
1613
-
1614
- {/* Area gradient fill */}
1615
- {gradientCfg && (
1616
- <Group opacity={reveal.fillOpacity}>
1617
- <Path path={fillPath} style="fill">
1618
- <LinearGradient
1619
- start={vec(0, effectivePadding.top)}
1620
- end={vec(0, gradientEnd)}
1621
- colors={gradientColors}
1622
- positions={gradientPositions}
1708
+ <Group opacity={seriesOpacity}>
1709
+ {/* Dot-lattice area fill (the under-line `fillPath` painted with a dot
1710
+ shader). Drawn before the gradient so a gradient (if also enabled)
1711
+ composites on top. */}
1712
+ {areaDotsCfg && (
1713
+ <Group opacity={reveal.fillOpacity}>
1714
+ <AreaDotsOverlay
1715
+ fillPath={fillPath}
1716
+ color={areaDotColorVec}
1717
+ spacing={areaDotsCfg.spacing}
1718
+ size={areaDotsCfg.size}
1623
1719
  />
1624
- </Path>
1625
- </Group>
1626
- )}
1720
+ </Group>
1721
+ )}
1627
1722
 
1628
- {/* Threshold profit/loss band — the area between the line and the threshold,
1629
- split into the above/below colors. Independent of the baseline area fill
1630
- above (set `gradient={false}` for the band alone). A time-varying series
1631
- paints with the per-fragment split shader; a constant value with the
1632
- vertical hard-stop gradient. */}
1633
- {thresholdCfg?.fill &&
1634
- (thresholdIsSeries ? (
1635
- // The availability gate keeps a failed shader compile from filling the
1636
- // band with the default paint (opaque black) — see THRESHOLD_SPLIT_AVAILABLE.
1637
- thresholdSeriesHasPoints && THRESHOLD_SPLIT_AVAILABLE ? (
1638
- <Group opacity={reveal.fillOpacity}>
1639
- <Path path={thresholdFillPath} style="fill">
1640
- <ThresholdSplitShader uniforms={thresholdFillUniforms} />
1641
- </Path>
1642
- </Group>
1643
- ) : null
1644
- ) : thresholdFillColors ? (
1723
+ {/* Area gradient fill */}
1724
+ {gradientCfg && (
1645
1725
  <Group opacity={reveal.fillOpacity}>
1646
- <Path path={thresholdFillPath} style="fill">
1726
+ <Path path={fillPath} style="fill">
1647
1727
  <LinearGradient
1648
- start={vec(0, 0)}
1649
- end={thresholdGeom.gradientEnd}
1650
- colors={thresholdFillColors}
1651
- positions={thresholdGeom.splitPositions}
1728
+ start={vec(0, effectivePadding.top)}
1729
+ end={vec(0, gradientEnd)}
1730
+ colors={gradientColors}
1731
+ positions={gradientPositions}
1652
1732
  />
1653
1733
  </Path>
1654
1734
  </Group>
1655
- ) : null)}
1735
+ )}
1736
+
1737
+ {/* Threshold profit/loss band — the area between the line and the threshold,
1738
+ split into the above/below colors. Independent of the baseline area fill
1739
+ above (set `gradient={false}` for the band alone). A time-varying series
1740
+ paints with the per-fragment split shader; a constant value with the
1741
+ vertical hard-stop gradient. */}
1742
+ {thresholdCfg?.fill &&
1743
+ (thresholdIsSeries ? (
1744
+ // The availability gate keeps a failed shader compile from filling the
1745
+ // band with the default paint (opaque black) — see THRESHOLD_SPLIT_AVAILABLE.
1746
+ thresholdSeriesHasPoints && THRESHOLD_SPLIT_AVAILABLE ? (
1747
+ <Group opacity={reveal.fillOpacity}>
1748
+ <Path path={thresholdFillPath} style="fill">
1749
+ <ThresholdSplitShader uniforms={thresholdFillUniforms} />
1750
+ </Path>
1751
+ </Group>
1752
+ ) : null
1753
+ ) : thresholdFillColors ? (
1754
+ <Group opacity={reveal.fillOpacity}>
1755
+ <Path path={thresholdFillPath} style="fill">
1756
+ <LinearGradient
1757
+ start={vec(0, 0)}
1758
+ end={thresholdGeom.gradientEnd}
1759
+ colors={thresholdFillColors}
1760
+ positions={thresholdGeom.splitPositions}
1761
+ />
1762
+ </Path>
1763
+ </Group>
1764
+ ) : null)}
1765
+ </Group>
1656
1766
  </Group>
1657
1767
  );
1658
1768
  }
@@ -1668,13 +1778,12 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1668
1778
  effectivePadding,
1669
1779
  candlesEngine,
1670
1780
  liveEngine,
1671
- candleWidth,
1781
+ displayCandleWidth,
1672
1782
  metricsCfg,
1673
1783
  volumeBandHeight,
1674
1784
  volumeCfg,
1675
- isStatic,
1676
- transitionsCfg,
1677
1785
  candleGroupOpacity,
1786
+ seriesOpacity,
1678
1787
  palette,
1679
1788
  volumeOpacity,
1680
1789
  volumeUpColor,
@@ -1692,17 +1801,15 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1692
1801
  effectivePadding,
1693
1802
  candlesEngine,
1694
1803
  liveEngine,
1695
- candleWidth,
1804
+ displayCandleWidth,
1696
1805
  true,
1697
1806
  metricsCfg.candle,
1698
1807
  volumeBandHeight,
1699
1808
  volumeCfg?.radius ?? 0,
1700
- !isStatic,
1701
- transitionsCfg.candleLerpSpeed,
1702
1809
  );
1703
1810
 
1704
1811
  return (
1705
- <>
1812
+ <Group opacity={seriesOpacity}>
1706
1813
  <Group opacity={candleGroupOpacity}>
1707
1814
  <Path
1708
1815
  path={upWicksPath}
@@ -1728,7 +1835,7 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1728
1835
  </Group>
1729
1836
  </Group>
1730
1837
  )}
1731
- </>
1838
+ </Group>
1732
1839
  );
1733
1840
  }
1734
1841
 
@@ -1775,6 +1882,7 @@ function ChartStack({
1775
1882
  thresholdSeriesPts,
1776
1883
  formatValue,
1777
1884
  lineGroupOpacity,
1885
+ seriesOpacity,
1778
1886
  linePath,
1779
1887
  lineIsLinear,
1780
1888
  strokeWidth,
@@ -1875,49 +1983,51 @@ function ChartStack({
1875
1983
  full-width gradient paints the base color outside segments and each
1876
1984
  segment's color within — so the line itself is recolored/faded (alpha in
1877
1985
  the segment color reduces the line's opacity), not covered by an overlay. */}
1878
- <Group opacity={lineGroupOpacity}>
1879
- <Path
1880
- path={linePath}
1881
- style="stroke"
1882
- strokeWidth={strokeWidth}
1883
- strokeCap={lineProp?.cap ?? "round"}
1884
- strokeJoin={lineProp?.join ?? "round"}
1885
- color={lineProp?.color ?? palette.line}
1886
- >
1887
- {thresholdIsSeries ? (
1888
- // Time-varying split: a per-fragment shader colors the stroke above/
1889
- // below the threshold polyline. Supersedes line.colors + segments.
1890
- // An empty series renders no paint child → the plain stroke color
1891
- // (null here keeps the empty case out of the NaN constant gradient).
1892
- thresholdSeriesHasPoints ? (
1893
- <ThresholdSplitShader uniforms={thresholdStrokeUniforms} />
1894
- ) : null
1895
- ) : thresholdCfg && thresholdStrokeColors ? (
1896
- // Vertical hard split at the threshold Y — supersedes line.colors and
1897
- // segment recoloring for the stroke while a threshold is set.
1898
- <LinearGradient
1899
- start={vec(0, 0)}
1900
- end={thresholdGeom.gradientEnd}
1901
- colors={thresholdStrokeColors}
1902
- positions={thresholdGeom.splitPositions}
1903
- />
1904
- ) : hasRecolorSegments ? (
1905
- <SegmentLineGradient
1906
- engine={engine}
1907
- segments={resolvedSegments}
1908
- padding={effectivePadding}
1909
- baseColor={lineProp?.color ?? palette.line}
1910
- scrubX={crosshair.scrubX}
1911
- scrubActive={crosshair.scrubActive}
1912
- />
1913
- ) : lineProp?.colors?.length ? (
1914
- <LinearGradient
1915
- start={vec(0, 0)}
1916
- end={vec(layoutWidth, 0)}
1917
- colors={lineProp.colors}
1918
- />
1919
- ) : null}
1920
- </Path>
1986
+ <Group opacity={seriesOpacity}>
1987
+ <Group opacity={lineGroupOpacity}>
1988
+ <Path
1989
+ path={linePath}
1990
+ style="stroke"
1991
+ strokeWidth={strokeWidth}
1992
+ strokeCap={lineProp?.cap ?? "round"}
1993
+ strokeJoin={lineProp?.join ?? "round"}
1994
+ color={lineProp?.color ?? palette.line}
1995
+ >
1996
+ {thresholdIsSeries ? (
1997
+ // Time-varying split: a per-fragment shader colors the stroke above/
1998
+ // below the threshold polyline. Supersedes line.colors + segments.
1999
+ // An empty series renders no paint child → the plain stroke color
2000
+ // (null here keeps the empty case out of the NaN constant gradient).
2001
+ thresholdSeriesHasPoints ? (
2002
+ <ThresholdSplitShader uniforms={thresholdStrokeUniforms} />
2003
+ ) : null
2004
+ ) : thresholdCfg && thresholdStrokeColors ? (
2005
+ // Vertical hard split at the threshold Y — supersedes line.colors and
2006
+ // segment recoloring for the stroke while a threshold is set.
2007
+ <LinearGradient
2008
+ start={vec(0, 0)}
2009
+ end={thresholdGeom.gradientEnd}
2010
+ colors={thresholdStrokeColors}
2011
+ positions={thresholdGeom.splitPositions}
2012
+ />
2013
+ ) : hasRecolorSegments ? (
2014
+ <SegmentLineGradient
2015
+ engine={engine}
2016
+ segments={resolvedSegments}
2017
+ padding={effectivePadding}
2018
+ baseColor={lineProp?.color ?? palette.line}
2019
+ scrubX={crosshair.scrubX}
2020
+ scrubActive={crosshair.scrubActive}
2021
+ />
2022
+ ) : lineProp?.colors?.length ? (
2023
+ <LinearGradient
2024
+ start={vec(0, 0)}
2025
+ end={vec(layoutWidth, 0)}
2026
+ colors={lineProp.colors}
2027
+ />
2028
+ ) : null}
2029
+ </Path>
2030
+ </Group>
1921
2031
  </Group>
1922
2032
 
1923
2033
  {isCandle && <ChartCandleLayer model={model} />}
@@ -2190,21 +2300,24 @@ function ChartValueOverlay({
2190
2300
  momentumSV,
2191
2301
  valueMomentumColor,
2192
2302
  reveal,
2303
+ seriesOpacity,
2193
2304
  } = model;
2194
2305
  if (!showValue) return null;
2195
2306
 
2196
2307
  return (
2197
2308
  <Group transform={degen?.shakeTransform}>
2198
- <Group opacity={reveal.lineOpacity}>
2199
- <ValueTextOverlay
2200
- engine={engine}
2201
- padding={effectivePadding}
2202
- palette={palette}
2203
- font={valueFont}
2204
- formatValue={formatValue}
2205
- momentum={momentumSV}
2206
- momentumColor={valueMomentumColor}
2207
- />
2309
+ <Group opacity={seriesOpacity}>
2310
+ <Group opacity={reveal.lineOpacity}>
2311
+ <ValueTextOverlay
2312
+ engine={engine}
2313
+ padding={effectivePadding}
2314
+ palette={palette}
2315
+ font={valueFont}
2316
+ formatValue={formatValue}
2317
+ momentum={momentumSV}
2318
+ momentumColor={valueMomentumColor}
2319
+ />
2320
+ </Group>
2208
2321
  </Group>
2209
2322
  </Group>
2210
2323
  );