react-native-livechart 4.12.0 → 4.13.1

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 (55) hide show
  1. package/dist/components/CrosshairLine.d.ts +12 -4
  2. package/dist/components/CrosshairLine.d.ts.map +1 -1
  3. package/dist/components/CrosshairOverlay.d.ts +12 -2
  4. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  5. package/dist/components/CustomTooltipOverlay.d.ts +5 -2
  6. package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
  7. package/dist/components/LiveChart.d.ts.map +1 -1
  8. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  9. package/dist/components/PerSeriesTooltipOverlay.d.ts +3 -1
  10. package/dist/components/PerSeriesTooltipOverlay.d.ts.map +1 -1
  11. package/dist/components/ReferenceLineOverlay.d.ts +8 -1
  12. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  13. package/dist/components/SegmentLineGradient.d.ts +23 -0
  14. package/dist/components/SegmentLineGradient.d.ts.map +1 -0
  15. package/dist/components/YAxisOverlay.d.ts +6 -2
  16. package/dist/components/YAxisOverlay.d.ts.map +1 -1
  17. package/dist/core/resolveConfig.d.ts +16 -0
  18. package/dist/core/resolveConfig.d.ts.map +1 -1
  19. package/dist/draw/grid.d.ts +13 -0
  20. package/dist/draw/grid.d.ts.map +1 -1
  21. package/dist/hooks/crosshairShared.d.ts +22 -3
  22. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  23. package/dist/hooks/useCrosshair.d.ts +7 -1
  24. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  25. package/dist/hooks/useCrosshairSeries.d.ts +6 -1
  26. package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
  27. package/dist/hooks/useCrosshairVisibleOpacity.d.ts +8 -0
  28. package/dist/hooks/useCrosshairVisibleOpacity.d.ts.map +1 -0
  29. package/dist/hooks/useReferenceLine.d.ts +10 -1
  30. package/dist/hooks/useReferenceLine.d.ts.map +1 -1
  31. package/dist/hooks/useSegmentLineGradient.d.ts.map +1 -1
  32. package/dist/math/segments.d.ts +4 -0
  33. package/dist/math/segments.d.ts.map +1 -1
  34. package/dist/types.d.ts +47 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/components/CrosshairLine.tsx +39 -11
  38. package/src/components/CrosshairOverlay.tsx +49 -15
  39. package/src/components/CustomTooltipOverlay.tsx +15 -3
  40. package/src/components/LiveChart.tsx +34 -23
  41. package/src/components/LiveChartSeries.tsx +83 -16
  42. package/src/components/PerSeriesTooltipOverlay.tsx +4 -1
  43. package/src/components/ReferenceLineOverlay.tsx +14 -0
  44. package/src/components/SegmentLineGradient.tsx +57 -0
  45. package/src/components/YAxisOverlay.tsx +34 -9
  46. package/src/core/resolveConfig.ts +29 -0
  47. package/src/draw/grid.ts +33 -0
  48. package/src/hooks/crosshairShared.ts +81 -4
  49. package/src/hooks/useCrosshair.ts +46 -28
  50. package/src/hooks/useCrosshairSeries.ts +31 -4
  51. package/src/hooks/useCrosshairVisibleOpacity.ts +57 -0
  52. package/src/hooks/useReferenceLine.ts +26 -1
  53. package/src/hooks/useSegmentLineGradient.ts +13 -5
  54. package/src/math/segments.ts +26 -0
  55. package/src/types.ts +47 -0
@@ -89,7 +89,6 @@ import {
89
89
  import { AXIS_GRAB_MIN_PX, usePanScroll } from "../hooks/usePanScroll";
90
90
  import { usePinchZoom } from "../hooks/usePinchZoom";
91
91
  import { useVisibleRange } from "../hooks/useVisibleRange";
92
- import { useSegmentLineGradient } from "../hooks/useSegmentLineGradient";
93
92
  import { useSingleChartReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
94
93
  import {
95
94
  useThreshold,
@@ -162,6 +161,7 @@ import { ReferenceLineGroupOverlay } from "./ReferenceLineGroupOverlay";
162
161
  import { ReferenceLineOverlay } from "./ReferenceLineOverlay";
163
162
  import { ScrubActionOverlay } from "./ScrubActionOverlay";
164
163
  import { SegmentDividerOverlay } from "./SegmentDividerOverlay";
164
+ import { SegmentLineGradient } from "./SegmentLineGradient";
165
165
  import { TradeStreamOverlay } from "./TradeStreamOverlay";
166
166
  import { ValueLineOverlay } from "./ValueLineOverlay";
167
167
  import { XAxisOverlay } from "./XAxisOverlay";
@@ -1066,6 +1066,7 @@ function useLiveChartController({
1066
1066
  ? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
1067
1067
  : 0,
1068
1068
  scrollActive,
1069
+ scrubCfg?.clampToPlot ?? false,
1069
1070
  );
1070
1071
 
1071
1072
  // Capture only the shared value in the worklets below. Referencing
@@ -1199,19 +1200,6 @@ function useLiveChartController({
1199
1200
  effectivePadding,
1200
1201
  );
1201
1202
 
1202
- // Scrub-focus gradient painted onto the line stroke: uniform at rest, and while
1203
- // scrubbing (or with an `active` segment) the focused segment stays full while
1204
- // the others are de-emphasized. Declared after `crosshair` so it can read the
1205
- // live scrub state.
1206
- const segmentGradient = useSegmentLineGradient(
1207
- engine,
1208
- resolvedSegments,
1209
- effectivePadding,
1210
- lineProp?.color ?? palette.line,
1211
- crosshair.scrubX,
1212
- crosshair.scrubActive,
1213
- );
1214
-
1215
1203
  // Hide the live dot while scrubbing when a selection dot is marking the scrub
1216
1204
  // point instead — otherwise both dots show at once. Applies on static charts
1217
1205
  // too, now that they're scrubbable.
@@ -1325,7 +1313,6 @@ function useLiveChartController({
1325
1313
  refGroupFormat,
1326
1314
  resolvedSegments,
1327
1315
  hasRecolorSegments,
1328
- segmentGradient,
1329
1316
  thresholdCfg,
1330
1317
  thresholdGeom,
1331
1318
  thresholdStrokeColors: thresholdStopColors?.stroke ?? null,
@@ -1495,6 +1482,7 @@ function ChartYAxisLayer({
1495
1482
  metricsCfg,
1496
1483
  gridStyleCfg,
1497
1484
  yAxisFloat,
1485
+ yAxisCfg,
1498
1486
  liveBadgeOpacity,
1499
1487
  } = model;
1500
1488
  return (
@@ -1515,6 +1503,8 @@ function ChartYAxisLayer({
1515
1503
  badgeOffsetY={badgeCfg?.offsetY ?? 0}
1516
1504
  badgeOpacity={badgeUsesRightGutter ? liveBadgeOpacity : undefined}
1517
1505
  gridStyle={gridStyleCfg}
1506
+ labelRightMargin={yAxisCfg?.labelRightMargin}
1507
+ gridEndGap={yAxisCfg?.gridEndGap}
1518
1508
  />
1519
1509
  </Group>
1520
1510
  );
@@ -1760,7 +1750,7 @@ function ChartStack({
1760
1750
  dragValues,
1761
1751
  resolvedSegments,
1762
1752
  hasRecolorSegments,
1763
- segmentGradient,
1753
+ crosshair,
1764
1754
  thresholdCfg,
1765
1755
  thresholdGeom,
1766
1756
  thresholdStrokeColors,
@@ -1846,6 +1836,9 @@ function ChartStack({
1846
1836
  fontProp={fontProp}
1847
1837
  dragValues={dragValues}
1848
1838
  index={i}
1839
+ yAxisEntries={yAxisEntries}
1840
+ labelRightMargin={yAxisCfg?.labelRightMargin}
1841
+ gridEndGap={yAxisCfg?.gridEndGap}
1849
1842
  />
1850
1843
  ))}
1851
1844
  </Group>
@@ -1898,11 +1891,13 @@ function ChartStack({
1898
1891
  positions={thresholdGeom.splitPositions}
1899
1892
  />
1900
1893
  ) : hasRecolorSegments ? (
1901
- <LinearGradient
1902
- start={vec(0, 0)}
1903
- end={segmentGradient.gradientEnd}
1904
- colors={segmentGradient.colors}
1905
- positions={segmentGradient.positions}
1894
+ <SegmentLineGradient
1895
+ engine={engine}
1896
+ segments={resolvedSegments}
1897
+ padding={effectivePadding}
1898
+ baseColor={lineProp?.color ?? palette.line}
1899
+ scrubX={crosshair.scrubX}
1900
+ scrubActive={crosshair.scrubActive}
1906
1901
  />
1907
1902
  ) : lineProp?.colors?.length ? (
1908
1903
  <LinearGradient
@@ -2119,6 +2114,11 @@ function ChartScrubLayer({
2119
2114
  dimOpacity={scrubCfg.dimOpacity}
2120
2115
  liveDotExtent={liveDotExtent}
2121
2116
  crosshairLineColor={scrubCfg.crosshairLineColor}
2117
+ crosshairStrokeWidth={scrubCfg.crosshairStrokeWidth}
2118
+ crosshairOvershoot={scrubCfg.crosshairOvershoot}
2119
+ crosshairFade={scrubCfg.crosshairFade}
2120
+ crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
2121
+ crosshairLineCap={scrubCfg.crosshairLineCap}
2122
2122
  crosshairDash={scrubCfg.crosshairDash}
2123
2123
  crosshairDimColor={scrubCfg.crosshairDimColor}
2124
2124
  tooltipBackground={scrubCfg.tooltipBackground}
@@ -2262,9 +2262,11 @@ function ChartBadgeLayer({
2262
2262
  function ChartRefBadgeLayer({
2263
2263
  model,
2264
2264
  degen,
2265
+ yAxisEntries,
2265
2266
  }: {
2266
2267
  model: LiveChartModel;
2267
2268
  degen: DegenState | null;
2269
+ yAxisEntries: YAxisEntries | null;
2268
2270
  }) {
2269
2271
  const {
2270
2272
  allRefLines,
@@ -2285,6 +2287,7 @@ function ChartRefBadgeLayer({
2285
2287
  formatValue,
2286
2288
  skiaFont,
2287
2289
  fontProp,
2290
+ yAxisCfg,
2288
2291
  overlayScrubFade,
2289
2292
  } = model;
2290
2293
  if (allRefLines.length === 0) return null;
@@ -2307,6 +2310,9 @@ function ChartRefBadgeLayer({
2307
2310
  suppressTagWhenOffAxis={refLineOffAxisCustom[i]}
2308
2311
  customTagWidths={refLineCustomTagWidths}
2309
2312
  groupHidden={refGroupingActive ? groupHidden : undefined}
2313
+ yAxisEntries={yAxisEntries}
2314
+ labelRightMargin={yAxisCfg?.labelRightMargin}
2315
+ gridEndGap={yAxisCfg?.gridEndGap}
2310
2316
  />
2311
2317
  ))}
2312
2318
  {/* Collapsed count handles for grouped (near-value) lines. */}
@@ -2547,7 +2553,11 @@ function ChartView({
2547
2553
  )}
2548
2554
 
2549
2555
  {/* Reference-line badges + labels above the fade so they stay crisp. */}
2550
- <ChartRefBadgeLayer model={model} degen={degen} />
2556
+ <ChartRefBadgeLayer
2557
+ model={model}
2558
+ degen={degen}
2559
+ yAxisEntries={yAxisEntries}
2560
+ />
2551
2561
 
2552
2562
  <ChartValueOverlay model={model} degen={degen} />
2553
2563
 
@@ -2601,12 +2611,13 @@ function ChartView({
2601
2611
  scrubTime={crosshair.scrubTime}
2602
2612
  scrubActive={crosshair.scrubActive}
2603
2613
  scrubCandle={crosshair.scrubCandle}
2604
- crosshairOpacity={crosshair.crosshairOpacity}
2605
2614
  tooltipLayout={crosshair.tooltipLayout}
2606
2615
  engine={engine}
2607
2616
  padding={effectivePadding}
2608
2617
  placement={scrubCfg.tooltipPlacement}
2609
2618
  margin={scrubCfg.tooltipMargin}
2619
+ crosshairFade={scrubCfg.crosshairFade}
2620
+ crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
2610
2621
  lineTop={crosshair.tooltipLineTop}
2611
2622
  scrubDotY={crosshair.scrubDotY}
2612
2623
  />
@@ -55,6 +55,7 @@ import { useChartReveal } from "../hooks/useChartReveal";
55
55
  import { useChartOverlayContext } from "../hooks/useChartOverlayContext";
56
56
  import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
57
57
  import { useCrosshairSeries } from "../hooks/useCrosshairSeries";
58
+ import { useCrosshairVisibleOpacity } from "../hooks/useCrosshairVisibleOpacity";
58
59
  import { useMarkers } from "../hooks/useMarkers";
59
60
  import { useMultiSeriesDegen } from "../hooks/useMultiSeriesDegen";
60
61
  import { useMultiSeriesLinePaths } from "../hooks/useMultiSeriesLinePaths";
@@ -463,6 +464,7 @@ function useLiveChartSeriesController({
463
464
  maxTime: tooltipMaxTime,
464
465
  }
465
466
  : undefined,
467
+ scrubCfg?.clampToPlot ?? false,
466
468
  );
467
469
 
468
470
  // Capture only the shared value in the worklets below. Referencing
@@ -712,6 +714,8 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
712
714
  badge={false}
713
715
  seriesLabelInset={seriesLabelInset}
714
716
  gridStyle={gridStyleCfg}
717
+ labelRightMargin={yAxisCfg.labelRightMargin}
718
+ gridEndGap={yAxisCfg.gridEndGap}
715
719
  />
716
720
  </Group>
717
721
  )}
@@ -728,6 +732,9 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
728
732
  palette={palette}
729
733
  formatValue={formatValue}
730
734
  font={skiaFont}
735
+ yAxisEntries={yAxisEntries}
736
+ labelRightMargin={yAxisCfg?.labelRightMargin}
737
+ gridEndGap={yAxisCfg?.gridEndGap}
731
738
  />
732
739
  ))}
733
740
  </Group>
@@ -888,6 +895,8 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
888
895
  palette,
889
896
  formatValue,
890
897
  skiaFont,
898
+ yAxisCfg,
899
+ yAxisEntries,
891
900
  degenShakeTransform,
892
901
  overlayScrubFade,
893
902
  } = model;
@@ -907,6 +916,9 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
907
916
  suppressTag={refLineCustom[i]}
908
917
  suppressTagWhenOffAxis={refLineOffAxisCustom[i]}
909
918
  customTagWidths={refLineCustomTagWidths}
919
+ yAxisEntries={yAxisEntries}
920
+ labelRightMargin={yAxisCfg?.labelRightMargin}
921
+ gridEndGap={yAxisCfg?.gridEndGap}
910
922
  />
911
923
  ))}
912
924
  </Group>
@@ -920,6 +932,53 @@ function SeriesCustomConsumerOverlay({ model }: { model: LiveChartSeriesModel })
920
932
  return <ChartOverlayLayer render={renderOverlay!} context={overlayContext} />;
921
933
  }
922
934
 
935
+ /** Keeps the extra fade mapper opt-in with the per-series tooltip itself. */
936
+ function SeriesTooltipLayer({
937
+ model,
938
+ config,
939
+ }: {
940
+ model: LiveChartSeriesModel;
941
+ config: NonNullable<LiveChartSeriesModel["seriesTooltipCfg"]>;
942
+ }) {
943
+ const {
944
+ crosshair,
945
+ engine,
946
+ effectivePadding,
947
+ scrubCfg,
948
+ skiaFont,
949
+ palette,
950
+ activeSeriesCount,
951
+ } = model;
952
+ const activeOpacity = useCrosshairVisibleOpacity(
953
+ crosshair.scrubX,
954
+ engine.canvasWidth,
955
+ effectivePadding.right,
956
+ crosshair.scrubActive,
957
+ scrubCfg?.crosshairFade ?? true,
958
+ scrubCfg?.crosshairFadeDistance ?? 4,
959
+ );
960
+ const scrubActive = crosshair.scrubActive;
961
+ const alwaysShow = config.alwaysShow;
962
+ const opacity = useDerivedValue(
963
+ () => (scrubActive.get() ? activeOpacity.get() : alwaysShow ? 1 : 0),
964
+ [scrubActive, activeOpacity, alwaysShow],
965
+ );
966
+
967
+ return (
968
+ <PerSeriesTooltipOverlay
969
+ layout={crosshair.tooltipLayout}
970
+ font={skiaFont}
971
+ palette={palette}
972
+ config={config}
973
+ seriesCount={activeSeriesCount}
974
+ opacity={opacity}
975
+ tooltipBackground={scrubCfg?.tooltipBackground}
976
+ tooltipColor={scrubCfg?.tooltipColor}
977
+ tooltipBorderColor={scrubCfg?.tooltipBorderColor}
978
+ />
979
+ );
980
+ }
981
+
923
982
  export function LiveChartSeries(props: LiveChartSeriesProps) {
924
983
  const model = useLiveChartSeriesController(props);
925
984
  const {
@@ -962,8 +1021,6 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
962
1021
  refLineCustomTagWidths,
963
1022
  overlayScrubFade,
964
1023
  canvasMode,
965
- activeSeriesCount,
966
- skiaFont,
967
1024
  } = model;
968
1025
 
969
1026
  // Mirror the Skia overlay fade onto the RN custom-marker sibling so
@@ -1065,7 +1122,14 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
1065
1122
  seriesTooltipCfg?.guideColor ??
1066
1123
  scrubCfg.crosshairLineColor
1067
1124
  }
1068
- crosshairLineWidth={seriesTooltipCfg?.guideWidth}
1125
+ crosshairStrokeWidth={
1126
+ seriesTooltipCfg?.guideWidth ??
1127
+ scrubCfg.crosshairStrokeWidth
1128
+ }
1129
+ crosshairOvershoot={scrubCfg.crosshairOvershoot}
1130
+ crosshairFade={scrubCfg.crosshairFade}
1131
+ crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
1132
+ crosshairLineCap={scrubCfg.crosshairLineCap}
1069
1133
  crosshairDash={
1070
1134
  seriesTooltipCfg
1071
1135
  ? seriesTooltipCfg.guideDashPattern
@@ -1079,19 +1143,6 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
1079
1143
  {/* Per-series value labels on top of the scrub dim so the dim never
1080
1144
  clips them (they track each series' live value, not the scrub). */}
1081
1145
  <SeriesValueLabelLayer model={model} />
1082
-
1083
- {seriesTooltipCfg && (
1084
- <PerSeriesTooltipOverlay
1085
- layout={crosshair.tooltipLayout}
1086
- font={skiaFont}
1087
- palette={palette}
1088
- config={seriesTooltipCfg}
1089
- seriesCount={activeSeriesCount}
1090
- tooltipBackground={scrubCfg?.tooltipBackground}
1091
- tooltipColor={scrubCfg?.tooltipColor}
1092
- tooltipBorderColor={scrubCfg?.tooltipBorderColor}
1093
- />
1094
- )}
1095
1146
  </Canvas>
1096
1147
 
1097
1148
  {/* RN labels floated over the canvas (sibling of <Canvas>, an RN
@@ -1153,6 +1204,22 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
1153
1204
  </Animated.View>
1154
1205
  )}
1155
1206
 
1207
+ {/* A dedicated transparent Canvas keeps the UI-thread tooltip above
1208
+ custom RN annotations. Rendering it in the primary Canvas would
1209
+ put every post-Canvas `renderReferenceLine` sibling above it and
1210
+ produce mixed stacking through translucent custom tags. */}
1211
+ {seriesTooltipCfg && (
1212
+ <View
1213
+ testID="live-chart-series-tooltip-overlay"
1214
+ pointerEvents="none"
1215
+ style={StyleSheet.absoluteFill}
1216
+ >
1217
+ <Canvas style={StyleSheet.absoluteFill}>
1218
+ <SeriesTooltipLayer model={model} config={seriesTooltipCfg} />
1219
+ </Canvas>
1220
+ </View>
1221
+ )}
1222
+
1156
1223
  {/* Custom consumer overlay — topmost RN sibling with the live plot
1157
1224
  rect, including this multi-series chart's resolved axis inset. */}
1158
1225
  {renderOverlay && <SeriesCustomConsumerOverlay model={model} />}
@@ -232,6 +232,7 @@ export function PerSeriesTooltipOverlay({
232
232
  palette,
233
233
  config,
234
234
  seriesCount,
235
+ opacity,
235
236
  tooltipBackground,
236
237
  tooltipColor,
237
238
  tooltipBorderColor,
@@ -241,6 +242,8 @@ export function PerSeriesTooltipOverlay({
241
242
  palette: LiveChartPalette;
242
243
  config: ResolvedPerSeriesTooltipConfig;
243
244
  seriesCount: number;
245
+ /** Active crosshair opacity; idle `alwaysShow` pills stay fully visible. */
246
+ opacity?: SharedValue<number> | DerivedValue<number>;
244
247
  tooltipBackground?: string;
245
248
  tooltipColor?: string;
246
249
  tooltipBorderColor?: string;
@@ -258,7 +261,7 @@ export function PerSeriesTooltipOverlay({
258
261
  config.seriesPillBorderColor ?? tooltipBorderColor;
259
262
 
260
263
  return (
261
- <Group>
264
+ <Group opacity={opacity}>
262
265
  <TimePill
263
266
  layout={layout}
264
267
  font={font}
@@ -9,6 +9,7 @@ import {
9
9
  import { useDerivedValue, type SharedValue } from "react-native-reanimated";
10
10
 
11
11
  import type { ChartEngineLayout } from "../core/useLiveChartEngine";
12
+ import type { YAxisEntry } from "../draw/grid";
12
13
  import type { ChartPadding } from "../draw/line";
13
14
  import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
14
15
  import { usePathBuilder } from "../hooks/usePathBuilder";
@@ -85,6 +86,12 @@ type ReferenceLineOverlayProps = {
85
86
  dragValues?: SharedValue<number[]>;
86
87
  /** This line's index into {@link dragValues}. */
87
88
  index?: number;
89
+ /** Y-axis entries used to match a right-anchored grid endpoint. */
90
+ yAxisEntries?: SharedValue<YAxisEntry[]> | null;
91
+ /** Enables clipping to the right-anchored Y-axis label column. */
92
+ labelRightMargin?: number;
93
+ /** Gap between the drawn line endpoint and the label column. */
94
+ gridEndGap?: number;
88
95
  };
89
96
 
90
97
  export function ReferenceLineOverlay({
@@ -102,6 +109,9 @@ export function ReferenceLineOverlay({
102
109
  groupHidden,
103
110
  dragValues,
104
111
  index = 0,
112
+ yAxisEntries,
113
+ labelRightMargin,
114
+ gridEndGap,
105
115
  }: ReferenceLineOverlayProps) {
106
116
  const form = referenceLineForm(line);
107
117
  const isBand = form === "value-band" || form === "time-band";
@@ -141,6 +151,10 @@ export function ReferenceLineOverlay({
141
151
  badgeFont,
142
152
  dragValues,
143
153
  index,
154
+ yAxisEntries ?? undefined,
155
+ labelRightMargin,
156
+ gridEndGap,
157
+ font,
144
158
  );
145
159
 
146
160
  const labelColor =
@@ -0,0 +1,57 @@
1
+ import { LinearGradient, vec } from "@shopify/react-native-skia";
2
+ import type { SharedValue } from "react-native-reanimated";
3
+
4
+ import type { ResolvedSegment } from "../core/resolveSegment";
5
+ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
6
+ import type { ChartPadding } from "../draw/line";
7
+ import { useSegmentLineGradient } from "../hooks/useSegmentLineGradient";
8
+ import { segmentGradientStopCount } from "../math/segments";
9
+
10
+ interface SegmentLineGradientProps {
11
+ engine: ChartEngineLayout;
12
+ segments: ResolvedSegment[];
13
+ padding: ChartPadding;
14
+ baseColor: string;
15
+ scrubX: SharedValue<number>;
16
+ scrubActive: SharedValue<boolean>;
17
+ }
18
+
19
+ /**
20
+ * Keeps the two SharedValues passed to Skia on one stop-count lifecycle.
21
+ *
22
+ * Scrubbing can change which stops are visible without changing `stopCount`, so
23
+ * padding keeps those mapper updates safe. When the segment props themselves
24
+ * change the count, the keyed child remounts both SharedValues together instead
25
+ * of letting one retain the previous length for a frame.
26
+ */
27
+ export function SegmentLineGradient(props: SegmentLineGradientProps) {
28
+ const stopCount = segmentGradientStopCount(props.segments);
29
+ return <SegmentLineGradientPaint key={stopCount} {...props} />;
30
+ }
31
+
32
+ function SegmentLineGradientPaint({
33
+ engine,
34
+ segments,
35
+ padding,
36
+ baseColor,
37
+ scrubX,
38
+ scrubActive,
39
+ }: SegmentLineGradientProps) {
40
+ const gradient = useSegmentLineGradient(
41
+ engine,
42
+ segments,
43
+ padding,
44
+ baseColor,
45
+ scrubX,
46
+ scrubActive,
47
+ );
48
+
49
+ return (
50
+ <LinearGradient
51
+ start={vec(0, 0)}
52
+ end={gradient.gradientEnd}
53
+ colors={gradient.colors}
54
+ positions={gradient.positions}
55
+ />
56
+ );
57
+ }
@@ -7,7 +7,10 @@ import {
7
7
  import { useDerivedValue, type SharedValue } from "react-native-reanimated";
8
8
  import { BADGE_METRICS_DEFAULTS, MAX_Y_LABELS } from "../constants";
9
9
  import type { ResolvedGridStyleConfig } from "../core/resolveConfig";
10
- import type { YAxisEntry } from "../draw/grid";
10
+ import {
11
+ rightAnchoredYAxisColumnLayout,
12
+ type YAxisEntry,
13
+ } from "../draw/grid";
11
14
  import {
12
15
  badgeTailAndCap,
13
16
  gutterCenteredTextLeftX,
@@ -57,6 +60,8 @@ export function YAxisOverlay({
57
60
  gridStyle,
58
61
  variant = "all",
59
62
  float = false,
63
+ labelRightMargin,
64
+ gridEndGap = 0,
60
65
  }: {
61
66
  entries: SharedValue<YAxisEntry[]>;
62
67
  engine: ChartEngineLayout;
@@ -92,6 +97,10 @@ export function YAxisOverlay({
92
97
  * plot) instead of centering them in a reserved gutter. See {@link YAxisConfig.float}.
93
98
  */
94
99
  float?: boolean;
100
+ /** Fixed canvas-edge margin for a shared left-aligned label column. */
101
+ labelRightMargin?: number;
102
+ /** Gap between the grid-line end and the shared label column. */
103
+ gridEndGap?: number;
95
104
  }) {
96
105
  const gridColor = gridStyle?.color ?? palette.gridLine;
97
106
  const gridWidth = gridStyle?.strokeWidth ?? 1;
@@ -99,14 +108,26 @@ export function YAxisOverlay({
99
108
  const gridOpacity = gridStyle?.opacity ?? 1;
100
109
  const gridBuilder = usePathBuilder();
101
110
 
111
+ const rightAnchoredColumn = useDerivedValue(() => {
112
+ if (labelRightMargin === undefined) return null;
113
+ return rightAnchoredYAxisColumnLayout(
114
+ engine.canvasWidth.get(),
115
+ entries.get(),
116
+ font,
117
+ labelRightMargin,
118
+ gridEndGap,
119
+ );
120
+ });
121
+
102
122
  const gridLinesPath = useDerivedValue(() => {
103
123
  const b = gridBuilder.value;
104
124
  if (variant === "labels") return b.detach();
105
125
  const items = entries.get();
106
126
  const w = engine.canvasWidth.get();
127
+ const lineX2 = rightAnchoredColumn.get()?.gridEndX ?? w - padding.right;
107
128
  for (let i = 0; i < items.length; i++) {
108
129
  b.moveTo(padding.left, items[i].y);
109
- b.lineTo(w - padding.right, items[i].y);
130
+ b.lineTo(lineX2, items[i].y);
110
131
  }
111
132
  return b.detach();
112
133
  });
@@ -120,6 +141,7 @@ export function YAxisOverlay({
120
141
  const w = engine.canvasWidth.get();
121
142
  const fm = font.getMetrics();
122
143
  const baselineOffset = (fm.ascent + fm.descent) / 2;
144
+ const columnX = rightAnchoredColumn.get()?.labelX;
123
145
  const labelHeight = fm.descent - fm.ascent;
124
146
  const badgeIsVisible = badgeOpacity === undefined || badgeOpacity.get() > 0;
125
147
  const resolvedBadgeCenterY = badgeCenterY && badgeIsVisible
@@ -133,13 +155,16 @@ export function YAxisOverlay({
133
155
  for (let i = 0; i < items.length; i++) {
134
156
  const e = items[i];
135
157
  const textW = measureFontTextWidth(font, e.label);
136
- const x = float
137
- ? gutterRightAlignedTextLeftX(w, textW, FLOAT_LABEL_RIGHT_MARGIN)
138
- : badge
139
- ? pillTextLeftX(w, padding.right, leftInset, textW, badgeMetrics)
140
- : seriesLabelInset > 0
141
- ? gutterRightAlignedTextLeftX(w, textW)
142
- : gutterCenteredTextLeftX(w, padding.right, textW);
158
+ const x =
159
+ columnX !== undefined
160
+ ? columnX
161
+ : float
162
+ ? gutterRightAlignedTextLeftX(w, textW, FLOAT_LABEL_RIGHT_MARGIN)
163
+ : badge
164
+ ? pillTextLeftX(w, padding.right, leftInset, textW, badgeMetrics)
165
+ : seriesLabelInset > 0
166
+ ? gutterRightAlignedTextLeftX(w, textW)
167
+ : gutterCenteredTextLeftX(w, padding.right, textW);
143
168
  result.push({
144
169
  x,
145
170
  y: e.y - baselineOffset,
@@ -89,6 +89,10 @@ export interface ResolvedYAxisConfig {
89
89
  minGap: number;
90
90
  /** Fixed label count (≥ 2), or 0 for the dynamic nice-interval grid. */
91
91
  count: number;
92
+ /** undefined → keep the default centered-gutter label placement. */
93
+ labelRightMargin: number | undefined;
94
+ /** undefined → no gap; inert unless labelRightMargin is set. */
95
+ gridEndGap: number | undefined;
92
96
  /** Float the axis over a full-width plot (no reserved right gutter). */
93
97
  float: boolean;
94
98
  }
@@ -151,6 +155,16 @@ export interface ResolvedScrubConfig {
151
155
  dimOpacity: number;
152
156
  /** undefined → palette.crosshairLine */
153
157
  crosshairLineColor: string | undefined;
158
+ /** Vertical crosshair line width in px. */
159
+ crosshairStrokeWidth: number;
160
+ /** Extension past the plot's top and bottom edges in px. */
161
+ crosshairOvershoot: number;
162
+ /** Fade the crosshair near the live edge. */
163
+ crosshairFade: boolean;
164
+ /** Visible-crosshair fade distance near the live edge in px. */
165
+ crosshairFadeDistance: number;
166
+ /** undefined preserves the existing Skia line-cap default. */
167
+ crosshairLineCap: "butt" | "round" | "square" | undefined;
154
168
  /** Dash intervals `[on, off, …]` for the crosshair line; undefined → solid. */
155
169
  crosshairDash: number[] | undefined;
156
170
  /** undefined → palette.crosshairDim */
@@ -175,6 +189,8 @@ export interface ResolvedScrubConfig {
175
189
  panGestureDelay: number;
176
190
  /** Fade markers + reference lines out while scrubbing. */
177
191
  hideOverlaysOnScrub: boolean;
192
+ /** Reject outside plain-scrub starts and clamp active scrub X to the plot. */
193
+ clampToPlot: boolean;
178
194
  }
179
195
 
180
196
  export interface ResolvedPerSeriesTooltipConfig {
@@ -465,6 +481,8 @@ export function resolveBadge(
465
481
  const Y_AXIS_DEFAULTS: ResolvedYAxisConfig = {
466
482
  minGap: 36,
467
483
  count: 0,
484
+ labelRightMargin: undefined,
485
+ gridEndGap: undefined,
468
486
  float: false,
469
487
  };
470
488
 
@@ -664,6 +682,11 @@ const SCRUB_DEFAULTS: ResolvedScrubConfig = {
664
682
  seriesTooltip: null,
665
683
  dimOpacity: 0.3,
666
684
  crosshairLineColor: undefined,
685
+ crosshairStrokeWidth: 1,
686
+ crosshairOvershoot: 0,
687
+ crosshairFade: true,
688
+ crosshairFadeDistance: 4,
689
+ crosshairLineCap: undefined,
667
690
  crosshairDash: undefined,
668
691
  crosshairDimColor: undefined,
669
692
  tooltipBackground: undefined,
@@ -676,6 +699,7 @@ const SCRUB_DEFAULTS: ResolvedScrubConfig = {
676
699
  tooltipShowTime: true,
677
700
  panGestureDelay: 0,
678
701
  hideOverlaysOnScrub: false,
702
+ clampToPlot: false,
679
703
  };
680
704
 
681
705
  const PER_SERIES_TOOLTIP_DEFAULTS: ResolvedPerSeriesTooltipConfig = {
@@ -743,6 +767,11 @@ export function resolveScrub(
743
767
  const seriesTooltip =
744
768
  typeof prop === "object" ? prop.seriesTooltip : undefined;
745
769
  resolved.seriesTooltip = resolvePerSeriesTooltip(seriesTooltip);
770
+ resolved.crosshairOvershoot = Math.max(0, resolved.crosshairOvershoot);
771
+ resolved.crosshairFadeDistance = Math.max(
772
+ 0,
773
+ resolved.crosshairFadeDistance,
774
+ );
746
775
  }
747
776
  return resolved;
748
777
  }
package/src/draw/grid.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { GRID_METRICS_DEFAULTS, MAX_Y_LABELS } from "../constants";
2
+ import type { SkFont } from "@shopify/react-native-skia";
3
+ import { measureFontTextWidth } from "../lib/measureFontTextWidth";
2
4
  import { lerp } from "../math/lerp";
3
5
  import type { GridMetrics } from "../types";
4
6
 
@@ -8,6 +10,37 @@ export interface YAxisEntry {
8
10
  alpha: number;
9
11
  }
10
12
 
13
+ export interface RightAnchoredYAxisColumnLayout {
14
+ /** Shared left X for every label in the column. */
15
+ labelX: number;
16
+ /** Grid/reference-line end X immediately before the column gap. */
17
+ gridEndX: number;
18
+ }
19
+
20
+ /**
21
+ * Measure the widest Y-axis label and anchor the shared left-aligned column to
22
+ * a fixed canvas-edge margin. Shared by the axis and reference-line worklets so
23
+ * their right edges cannot drift apart.
24
+ */
25
+ export function rightAnchoredYAxisColumnLayout(
26
+ canvasWidth: number,
27
+ entries: readonly YAxisEntry[],
28
+ font: SkFont,
29
+ labelRightMargin: number,
30
+ gridEndGap = 0,
31
+ ): RightAnchoredYAxisColumnLayout {
32
+ "worklet";
33
+ let maxTextW = 0;
34
+ for (let i = 0; i < entries.length; i++) {
35
+ maxTextW = Math.max(
36
+ maxTextW,
37
+ measureFontTextWidth(font, entries[i].label),
38
+ );
39
+ }
40
+ const labelX = canvasWidth - labelRightMargin - maxTextW;
41
+ return { labelX, gridEndX: labelX - gridEndGap };
42
+ }
43
+
11
44
  /**
12
45
  * Fixed-count Y-axis: place exactly `count` price labels evenly **in pixels**
13
46
  * across the plot band — top = `displayMax`, bottom = `displayMin`. Values track