react-native-livechart 1.0.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 (243) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +198 -0
  3. package/dist/components/AnimatedLabel.d.ts +21 -0
  4. package/dist/components/AnimatedLabel.d.ts.map +1 -0
  5. package/dist/components/BadgeOverlay.d.ts +16 -0
  6. package/dist/components/BadgeOverlay.d.ts.map +1 -0
  7. package/dist/components/CrosshairLine.d.ts +20 -0
  8. package/dist/components/CrosshairLine.d.ts.map +1 -0
  9. package/dist/components/CrosshairOverlay.d.ts +29 -0
  10. package/dist/components/CrosshairOverlay.d.ts.map +1 -0
  11. package/dist/components/DegenParticlesOverlay.d.ts +16 -0
  12. package/dist/components/DegenParticlesOverlay.d.ts.map +1 -0
  13. package/dist/components/DotOverlay.d.ts +17 -0
  14. package/dist/components/DotOverlay.d.ts.map +1 -0
  15. package/dist/components/LeftEdgeFade.d.ts +9 -0
  16. package/dist/components/LeftEdgeFade.d.ts.map +1 -0
  17. package/dist/components/LiveChart.d.ts +3 -0
  18. package/dist/components/LiveChart.d.ts.map +1 -0
  19. package/dist/components/LiveChartSeries.d.ts +3 -0
  20. package/dist/components/LiveChartSeries.d.ts.map +1 -0
  21. package/dist/components/LiveChartTransition.d.ts +28 -0
  22. package/dist/components/LiveChartTransition.d.ts.map +1 -0
  23. package/dist/components/LoadingOverlay.d.ts +24 -0
  24. package/dist/components/LoadingOverlay.d.ts.map +1 -0
  25. package/dist/components/MarkerOverlay.d.ts +25 -0
  26. package/dist/components/MarkerOverlay.d.ts.map +1 -0
  27. package/dist/components/MultiSeriesDots.d.ts +11 -0
  28. package/dist/components/MultiSeriesDots.d.ts.map +1 -0
  29. package/dist/components/MultiSeriesStroke.d.ts +14 -0
  30. package/dist/components/MultiSeriesStroke.d.ts.map +1 -0
  31. package/dist/components/MultiSeriesTooltipStack.d.ts +10 -0
  32. package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -0
  33. package/dist/components/MultiSeriesValueLabels.d.ts +11 -0
  34. package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -0
  35. package/dist/components/MultiSeriesValueLines.d.ts +10 -0
  36. package/dist/components/MultiSeriesValueLines.d.ts.map +1 -0
  37. package/dist/components/ReferenceLineOverlay.d.ts +19 -0
  38. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -0
  39. package/dist/components/SeriesToggleChips.d.ts +15 -0
  40. package/dist/components/SeriesToggleChips.d.ts.map +1 -0
  41. package/dist/components/TradeStreamOverlay.d.ts +14 -0
  42. package/dist/components/TradeStreamOverlay.d.ts.map +1 -0
  43. package/dist/components/ValueLineOverlay.d.ts +16 -0
  44. package/dist/components/ValueLineOverlay.d.ts.map +1 -0
  45. package/dist/components/ValueTextOverlay.d.ts +20 -0
  46. package/dist/components/ValueTextOverlay.d.ts.map +1 -0
  47. package/dist/components/XAxisOverlay.d.ts +14 -0
  48. package/dist/components/XAxisOverlay.d.ts.map +1 -0
  49. package/dist/components/YAxisOverlay.d.ts +23 -0
  50. package/dist/components/YAxisOverlay.d.ts.map +1 -0
  51. package/dist/components/seriesMetaSig.d.ts +4 -0
  52. package/dist/components/seriesMetaSig.d.ts.map +1 -0
  53. package/dist/constants.d.ts +38 -0
  54. package/dist/constants.d.ts.map +1 -0
  55. package/dist/core/liveChartEngineTick.d.ts +45 -0
  56. package/dist/core/liveChartEngineTick.d.ts.map +1 -0
  57. package/dist/core/liveChartSeriesEngineTick.d.ts +38 -0
  58. package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -0
  59. package/dist/core/multiSeriesLayout.d.ts +22 -0
  60. package/dist/core/multiSeriesLayout.d.ts.map +1 -0
  61. package/dist/core/resolveConfig.d.ts +188 -0
  62. package/dist/core/resolveConfig.d.ts.map +1 -0
  63. package/dist/core/useLiveChartEngine.d.ts +79 -0
  64. package/dist/core/useLiveChartEngine.d.ts.map +1 -0
  65. package/dist/core/useLiveChartSeriesEngine.d.ts +68 -0
  66. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -0
  67. package/dist/draw/candle.d.ts +25 -0
  68. package/dist/draw/candle.d.ts.map +1 -0
  69. package/dist/draw/grid.d.ts +24 -0
  70. package/dist/draw/grid.d.ts.map +1 -0
  71. package/dist/draw/line.d.ts +86 -0
  72. package/dist/draw/line.d.ts.map +1 -0
  73. package/dist/draw/trade.d.ts +38 -0
  74. package/dist/draw/trade.d.ts.map +1 -0
  75. package/dist/hooks/crosshairSeries.d.ts +13 -0
  76. package/dist/hooks/crosshairSeries.d.ts.map +1 -0
  77. package/dist/hooks/crosshairShared.d.ts +72 -0
  78. package/dist/hooks/crosshairShared.d.ts.map +1 -0
  79. package/dist/hooks/index.d.ts +24 -0
  80. package/dist/hooks/index.d.ts.map +1 -0
  81. package/dist/hooks/resolveChartLayout.d.ts +42 -0
  82. package/dist/hooks/resolveChartLayout.d.ts.map +1 -0
  83. package/dist/hooks/useBadge.d.ts +14 -0
  84. package/dist/hooks/useBadge.d.ts.map +1 -0
  85. package/dist/hooks/useCandlePaths.d.ts +19 -0
  86. package/dist/hooks/useCandlePaths.d.ts.map +1 -0
  87. package/dist/hooks/useCanvasLayout.d.ts +12 -0
  88. package/dist/hooks/useCanvasLayout.d.ts.map +1 -0
  89. package/dist/hooks/useChartColors.d.ts +20 -0
  90. package/dist/hooks/useChartColors.d.ts.map +1 -0
  91. package/dist/hooks/useChartPaths.d.ts +19 -0
  92. package/dist/hooks/useChartPaths.d.ts.map +1 -0
  93. package/dist/hooks/useChartReveal.d.ts +30 -0
  94. package/dist/hooks/useChartReveal.d.ts.map +1 -0
  95. package/dist/hooks/useChartSkiaFont.d.ts +8 -0
  96. package/dist/hooks/useChartSkiaFont.d.ts.map +1 -0
  97. package/dist/hooks/useCrosshair.d.ts +20 -0
  98. package/dist/hooks/useCrosshair.d.ts.map +1 -0
  99. package/dist/hooks/useCrosshairSeries.d.ts +10 -0
  100. package/dist/hooks/useCrosshairSeries.d.ts.map +1 -0
  101. package/dist/hooks/useDegen.d.ts +17 -0
  102. package/dist/hooks/useDegen.d.ts.map +1 -0
  103. package/dist/hooks/useLiveChartHasData.d.ts +17 -0
  104. package/dist/hooks/useLiveChartHasData.d.ts.map +1 -0
  105. package/dist/hooks/useLiveDot.d.ts +12 -0
  106. package/dist/hooks/useLiveDot.d.ts.map +1 -0
  107. package/dist/hooks/useMarkers.d.ts +15 -0
  108. package/dist/hooks/useMarkers.d.ts.map +1 -0
  109. package/dist/hooks/useModeBlend.d.ts +18 -0
  110. package/dist/hooks/useModeBlend.d.ts.map +1 -0
  111. package/dist/hooks/useMomentum.d.ts +16 -0
  112. package/dist/hooks/useMomentum.d.ts.map +1 -0
  113. package/dist/hooks/useMultiSeriesDegen.d.ts +25 -0
  114. package/dist/hooks/useMultiSeriesDegen.d.ts.map +1 -0
  115. package/dist/hooks/useMultiSeriesLinePaths.d.ts +15 -0
  116. package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -0
  117. package/dist/hooks/useReferenceLine.d.ts +32 -0
  118. package/dist/hooks/useReferenceLine.d.ts.map +1 -0
  119. package/dist/hooks/useReverseMorphEngineInputs.d.ts +24 -0
  120. package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -0
  121. package/dist/hooks/useTradeStream.d.ts +12 -0
  122. package/dist/hooks/useTradeStream.d.ts.map +1 -0
  123. package/dist/hooks/useXAxis.d.ts +32 -0
  124. package/dist/hooks/useXAxis.d.ts.map +1 -0
  125. package/dist/hooks/useYAxis.d.ts +13 -0
  126. package/dist/hooks/useYAxis.d.ts.map +1 -0
  127. package/dist/index.d.ts +18 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/lib/format.d.ts +5 -0
  130. package/dist/lib/format.d.ts.map +1 -0
  131. package/dist/lib/measureFontTextWidth.d.ts +7 -0
  132. package/dist/lib/measureFontTextWidth.d.ts.map +1 -0
  133. package/dist/lib/monoFontFamily.d.ts +6 -0
  134. package/dist/lib/monoFontFamily.d.ts.map +1 -0
  135. package/dist/math/color.d.ts +5 -0
  136. package/dist/math/color.d.ts.map +1 -0
  137. package/dist/math/degenTick.d.ts +57 -0
  138. package/dist/math/degenTick.d.ts.map +1 -0
  139. package/dist/math/interpolate.d.ts +7 -0
  140. package/dist/math/interpolate.d.ts.map +1 -0
  141. package/dist/math/intervals.d.ts +3 -0
  142. package/dist/math/intervals.d.ts.map +1 -0
  143. package/dist/math/lerp.d.ts +7 -0
  144. package/dist/math/lerp.d.ts.map +1 -0
  145. package/dist/math/markers.d.ts +44 -0
  146. package/dist/math/markers.d.ts.map +1 -0
  147. package/dist/math/momentum.d.ts +8 -0
  148. package/dist/math/momentum.d.ts.map +1 -0
  149. package/dist/math/pickCandle.d.ts +8 -0
  150. package/dist/math/pickCandle.d.ts.map +1 -0
  151. package/dist/math/range.d.ts +10 -0
  152. package/dist/math/range.d.ts.map +1 -0
  153. package/dist/math/referenceLines.d.ts +20 -0
  154. package/dist/math/referenceLines.d.ts.map +1 -0
  155. package/dist/math/spline.d.ts +45 -0
  156. package/dist/math/spline.d.ts.map +1 -0
  157. package/dist/math/squiggly.d.ts +36 -0
  158. package/dist/math/squiggly.d.ts.map +1 -0
  159. package/dist/theme.d.ts +30 -0
  160. package/dist/theme.d.ts.map +1 -0
  161. package/dist/types.d.ts +720 -0
  162. package/dist/types.d.ts.map +1 -0
  163. package/package.json +72 -0
  164. package/src/components/AnimatedLabel.tsx +41 -0
  165. package/src/components/BadgeOverlay.tsx +39 -0
  166. package/src/components/CrosshairLine.tsx +90 -0
  167. package/src/components/CrosshairOverlay.tsx +170 -0
  168. package/src/components/DegenParticlesOverlay.tsx +141 -0
  169. package/src/components/DotOverlay.tsx +69 -0
  170. package/src/components/LeftEdgeFade.tsx +36 -0
  171. package/src/components/LiveChart.tsx +821 -0
  172. package/src/components/LiveChartSeries.tsx +607 -0
  173. package/src/components/LiveChartTransition.tsx +135 -0
  174. package/src/components/LoadingOverlay.tsx +301 -0
  175. package/src/components/MarkerOverlay.tsx +331 -0
  176. package/src/components/MultiSeriesDots.tsx +117 -0
  177. package/src/components/MultiSeriesStroke.tsx +79 -0
  178. package/src/components/MultiSeriesTooltipStack.tsx +78 -0
  179. package/src/components/MultiSeriesValueLabels.tsx +103 -0
  180. package/src/components/MultiSeriesValueLines.tsx +115 -0
  181. package/src/components/ReferenceLineOverlay.tsx +317 -0
  182. package/src/components/SeriesToggleChips.tsx +201 -0
  183. package/src/components/TradeStreamOverlay.tsx +117 -0
  184. package/src/components/ValueLineOverlay.tsx +63 -0
  185. package/src/components/ValueTextOverlay.tsx +43 -0
  186. package/src/components/XAxisOverlay.tsx +109 -0
  187. package/src/components/YAxisOverlay.tsx +135 -0
  188. package/src/components/seriesMetaSig.ts +13 -0
  189. package/src/constants.ts +44 -0
  190. package/src/core/liveChartEngineTick.ts +185 -0
  191. package/src/core/liveChartSeriesEngineTick.ts +163 -0
  192. package/src/core/multiSeriesLayout.ts +80 -0
  193. package/src/core/resolveConfig.ts +538 -0
  194. package/src/core/useLiveChartEngine.ts +207 -0
  195. package/src/core/useLiveChartSeriesEngine.ts +215 -0
  196. package/src/draw/candle.ts +210 -0
  197. package/src/draw/grid.ts +174 -0
  198. package/src/draw/line.ts +257 -0
  199. package/src/draw/trade.ts +202 -0
  200. package/src/hooks/crosshairSeries.ts +78 -0
  201. package/src/hooks/crosshairShared.ts +296 -0
  202. package/src/hooks/index.ts +26 -0
  203. package/src/hooks/resolveChartLayout.ts +144 -0
  204. package/src/hooks/useBadge.ts +192 -0
  205. package/src/hooks/useCandlePaths.ts +165 -0
  206. package/src/hooks/useCanvasLayout.ts +22 -0
  207. package/src/hooks/useChartColors.ts +48 -0
  208. package/src/hooks/useChartPaths.ts +121 -0
  209. package/src/hooks/useChartReveal.ts +123 -0
  210. package/src/hooks/useChartSkiaFont.ts +34 -0
  211. package/src/hooks/useCrosshair.ts +250 -0
  212. package/src/hooks/useCrosshairSeries.ts +163 -0
  213. package/src/hooks/useDegen.ts +262 -0
  214. package/src/hooks/useLiveChartHasData.ts +31 -0
  215. package/src/hooks/useLiveDot.ts +31 -0
  216. package/src/hooks/useMarkers.ts +95 -0
  217. package/src/hooks/useModeBlend.ts +52 -0
  218. package/src/hooks/useMomentum.ts +37 -0
  219. package/src/hooks/useMultiSeriesDegen.ts +260 -0
  220. package/src/hooks/useMultiSeriesLinePaths.ts +76 -0
  221. package/src/hooks/useReferenceLine.ts +212 -0
  222. package/src/hooks/useReverseMorphEngineInputs.ts +154 -0
  223. package/src/hooks/useTradeStream.ts +64 -0
  224. package/src/hooks/useXAxis.ts +200 -0
  225. package/src/hooks/useYAxis.ts +48 -0
  226. package/src/index.ts +69 -0
  227. package/src/lib/format.ts +36 -0
  228. package/src/lib/measureFontTextWidth.ts +10 -0
  229. package/src/lib/monoFontFamily.ts +8 -0
  230. package/src/math/color.ts +31 -0
  231. package/src/math/degenTick.ts +151 -0
  232. package/src/math/interpolate.ts +32 -0
  233. package/src/math/intervals.ts +17 -0
  234. package/src/math/lerp.ts +17 -0
  235. package/src/math/markers.ts +175 -0
  236. package/src/math/momentum.ts +38 -0
  237. package/src/math/pickCandle.ts +42 -0
  238. package/src/math/range.ts +54 -0
  239. package/src/math/referenceLines.ts +56 -0
  240. package/src/math/spline.ts +183 -0
  241. package/src/math/squiggly.ts +108 -0
  242. package/src/theme.ts +231 -0
  243. package/src/types.ts +789 -0
@@ -0,0 +1,115 @@
1
+ import {
2
+ DashPathEffect,
3
+ Group,
4
+ Path,
5
+ Skia,
6
+ type SkPath,
7
+ } from "@shopify/react-native-skia";
8
+
9
+ import { useRef } from "react";
10
+ import { useDerivedValue } from "react-native-reanimated";
11
+ import { MAX_MULTI_SERIES } from "../constants";
12
+ import type { ChartPadding } from "../draw/line";
13
+ import type { ResolvedValueLineConfig } from "../core/resolveConfig";
14
+ import type { MultiEngineState } from "../core/useLiveChartEngine";
15
+
16
+ function SeriesValueLineAtIndex({
17
+ index,
18
+ engine,
19
+ padding,
20
+ color,
21
+ config,
22
+ }: {
23
+ index: number;
24
+ engine: MultiEngineState;
25
+ padding: ChartPadding;
26
+ color: string;
27
+ config: ResolvedValueLineConfig;
28
+ }) {
29
+ const cacheRef = useRef<{
30
+ a: SkPath;
31
+ b: SkPath;
32
+ tick: boolean;
33
+ } | null>(null);
34
+ if (cacheRef.current === null) {
35
+ cacheRef.current = {
36
+ a: Skia.Path.Make(),
37
+ b: Skia.Path.Make(),
38
+ tick: false,
39
+ };
40
+ }
41
+
42
+ const path = useDerivedValue(() => {
43
+ const cache = cacheRef.current!;
44
+ cache.tick = !cache.tick;
45
+ const p = cache.tick ? cache.a : cache.b;
46
+ p.reset();
47
+ const h = engine.canvasHeight.get();
48
+ if (h === 0) return p;
49
+ const s = engine.series.get();
50
+ const displays = engine.displaySeriesValues.get();
51
+ if (index >= s.length) return p;
52
+
53
+ const chartH = h - padding.top - padding.bottom;
54
+ const dMin = engine.displayMin.get();
55
+ const dMax = engine.displayMax.get();
56
+ const valRange = dMax - dMin;
57
+ const v = displays[index] ?? s[index].value;
58
+ const y =
59
+ valRange === 0
60
+ ? padding.top + chartH / 2
61
+ : padding.top + ((dMax - v) / valRange) * chartH;
62
+
63
+ if (y < 0) return p;
64
+ p.moveTo(padding.left, y);
65
+ p.lineTo(engine.canvasWidth.get() - padding.right, y);
66
+ return p;
67
+ });
68
+
69
+ const opacity = useDerivedValue(() => {
70
+ const s = engine.series.get();
71
+ const op = engine.seriesOpacities.get();
72
+ if (index >= s.length || index >= op.length) return 0;
73
+ return (op[index] ?? 0) * 0.4;
74
+ });
75
+
76
+ return (
77
+ <Group opacity={opacity}>
78
+ <Path
79
+ path={path}
80
+ style="stroke"
81
+ strokeWidth={config.strokeWidth}
82
+ color={config.color ?? color}
83
+ >
84
+ <DashPathEffect intervals={config.intervals} />
85
+ </Path>
86
+ </Group>
87
+ );
88
+ }
89
+
90
+ export function MultiSeriesValueLines({
91
+ engine,
92
+ padding,
93
+ colors,
94
+ config,
95
+ }: {
96
+ engine: MultiEngineState;
97
+ padding: ChartPadding;
98
+ colors: string[];
99
+ config: ResolvedValueLineConfig;
100
+ }) {
101
+ return (
102
+ <Group>
103
+ {Array.from({ length: MAX_MULTI_SERIES }, (_, i) => (
104
+ <SeriesValueLineAtIndex
105
+ key={i}
106
+ index={i}
107
+ engine={engine}
108
+ padding={padding}
109
+ color={colors[i] ?? "#ffffff"}
110
+ config={config}
111
+ />
112
+ ))}
113
+ </Group>
114
+ );
115
+ }
@@ -0,0 +1,317 @@
1
+ import {
2
+ DashPathEffect,
3
+ Group,
4
+ Path,
5
+ RoundedRect,
6
+ Skia,
7
+ Text as SkiaText,
8
+ type SkFont,
9
+ type SkPath,
10
+ } from "@shopify/react-native-skia";
11
+ import { useRef } from "react";
12
+ import { useDerivedValue } from "react-native-reanimated";
13
+
14
+ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
15
+ import type { ChartPadding } from "../draw/line";
16
+ import { useReferenceLine } from "../hooks/useReferenceLine";
17
+ import { measureFontTextWidth } from "../lib/measureFontTextWidth";
18
+ import { referenceLineForm } from "../math/referenceLines";
19
+ import type { LiveChartPalette, ReferenceLine } from "../types";
20
+
21
+ /** Translucent fill alpha for value / time bands. */
22
+ const BAND_FILL_OPACITY = 0.16;
23
+
24
+ /** Padding inside the off-axis badge pill, in px. */
25
+ const OFF_AXIS_PILL_PAD_X = 6;
26
+ const OFF_AXIS_PILL_PAD_Y = 3;
27
+ const OFF_AXIS_PILL_RADIUS = 5;
28
+
29
+ /**
30
+ * Renders one reference line or band into the chart canvas. Handles all three
31
+ * `ReferenceLine` forms (horizontal line, horizontal value band, vertical time
32
+ * band) plus the off-axis badge for an off-screen Form-A value. Self-contained
33
+ * so callers can `.map()` over a variable-length `referenceLines` array.
34
+ */
35
+ export function ReferenceLineOverlay({
36
+ engine,
37
+ padding,
38
+ line,
39
+ palette,
40
+ formatValue,
41
+ font,
42
+ }: {
43
+ engine: ChartEngineLayout;
44
+ padding: ChartPadding;
45
+ line: ReferenceLine;
46
+ palette: LiveChartPalette;
47
+ formatValue: (v: number) => string;
48
+ font: SkFont;
49
+ }) {
50
+ const form = referenceLineForm(line);
51
+ const isBand = form === "value-band" || form === "time-band";
52
+ const layout = useReferenceLine(engine, padding, line, formatValue, font);
53
+
54
+ const color = line.color ?? palette.refLine;
55
+ const labelColor = line.labelColor ?? line.color ?? palette.refLabel;
56
+ const strokeWidth = line.strokeWidth ?? 1;
57
+ const intervals = line.intervals ?? [4, 4];
58
+
59
+ // Band fill + optional dashed border (border only when strokeWidth is set).
60
+ const bandFillOpacity = line.fillOpacity ?? BAND_FILL_OPACITY;
61
+ const hasBandBorder = isBand && line.strokeWidth !== undefined;
62
+
63
+ // Off-axis target pill styling.
64
+ const badgeBackground = line.badgeBackground ?? palette.tooltipBg;
65
+ const badgeBorderColor = line.badgeBorderColor ?? color;
66
+ const badgeRadius = line.badgeRadius ?? OFF_AXIS_PILL_RADIUS;
67
+
68
+ const cacheRef = useRef<{
69
+ lineA: SkPath;
70
+ lineB: SkPath;
71
+ lineT: boolean;
72
+ bandA: SkPath;
73
+ bandB: SkPath;
74
+ bandT: boolean;
75
+ borderA: SkPath;
76
+ borderB: SkPath;
77
+ borderT: boolean;
78
+ offA: SkPath;
79
+ offB: SkPath;
80
+ offT: boolean;
81
+ chevA: SkPath;
82
+ chevB: SkPath;
83
+ chevT: boolean;
84
+ } | null>(null);
85
+ if (cacheRef.current === null) {
86
+ cacheRef.current = {
87
+ lineA: Skia.Path.Make(),
88
+ lineB: Skia.Path.Make(),
89
+ lineT: false,
90
+ bandA: Skia.Path.Make(),
91
+ bandB: Skia.Path.Make(),
92
+ bandT: false,
93
+ borderA: Skia.Path.Make(),
94
+ borderB: Skia.Path.Make(),
95
+ borderT: false,
96
+ offA: Skia.Path.Make(),
97
+ offB: Skia.Path.Make(),
98
+ offT: false,
99
+ chevA: Skia.Path.Make(),
100
+ chevB: Skia.Path.Make(),
101
+ chevT: false,
102
+ };
103
+ }
104
+
105
+ const linePath = useDerivedValue(() => {
106
+ const cache = cacheRef.current!;
107
+ cache.lineT = !cache.lineT;
108
+ const p = cache.lineT ? cache.lineA : cache.lineB;
109
+ p.reset();
110
+ const l = layout.get();
111
+ if (!l.visible || l.offAxis || isBand) return p;
112
+ p.moveTo(l.x1, l.y);
113
+ p.lineTo(l.x2, l.y);
114
+ return p;
115
+ });
116
+
117
+ const bandPath = useDerivedValue(() => {
118
+ const cache = cacheRef.current!;
119
+ cache.bandT = !cache.bandT;
120
+ const p = cache.bandT ? cache.bandA : cache.bandB;
121
+ p.reset();
122
+ const l = layout.get();
123
+ if (!l.visible || !isBand) return p;
124
+ p.moveTo(l.x1, l.y);
125
+ p.lineTo(l.x2, l.y);
126
+ p.lineTo(l.x2, l.yBottom);
127
+ p.lineTo(l.x1, l.yBottom);
128
+ p.close();
129
+ return p;
130
+ });
131
+
132
+ const bandBorderPath = useDerivedValue(() => {
133
+ const cache = cacheRef.current!;
134
+ cache.borderT = !cache.borderT;
135
+ const p = cache.borderT ? cache.borderA : cache.borderB;
136
+ p.reset();
137
+ const l = layout.get();
138
+ if (!l.visible || !hasBandBorder) return p;
139
+ if (form === "time-band") {
140
+ // Vertical edges at the band's left / right.
141
+ p.moveTo(l.x1, l.y);
142
+ p.lineTo(l.x1, l.yBottom);
143
+ p.moveTo(l.x2, l.y);
144
+ p.lineTo(l.x2, l.yBottom);
145
+ } else {
146
+ // Horizontal edges at the band's top / bottom.
147
+ p.moveTo(l.x1, l.y);
148
+ p.lineTo(l.x2, l.y);
149
+ p.moveTo(l.x1, l.yBottom);
150
+ p.lineTo(l.x2, l.yBottom);
151
+ }
152
+ return p;
153
+ });
154
+
155
+ const offLinePath = useDerivedValue(() => {
156
+ const cache = cacheRef.current!;
157
+ cache.offT = !cache.offT;
158
+ const p = cache.offT ? cache.offA : cache.offB;
159
+ p.reset();
160
+ const l = layout.get();
161
+ if (!l.visible || !l.offAxis) return p;
162
+ // Start the connector just past the badge pill's right edge so the dashed
163
+ // line runs out to the chart edge rather than behind the badge.
164
+ const pillRight =
165
+ l.labelX + measureFontTextWidth(font, l.label) + OFF_AXIS_PILL_PAD_X;
166
+ const start = pillRight + 4;
167
+ if (start >= l.x2) return p;
168
+ p.moveTo(start, l.y);
169
+ p.lineTo(l.x2, l.y);
170
+ return p;
171
+ });
172
+
173
+ const chevronPath = useDerivedValue(() => {
174
+ const cache = cacheRef.current!;
175
+ cache.chevT = !cache.chevT;
176
+ const p = cache.chevT ? cache.chevA : cache.chevB;
177
+ p.reset();
178
+ const l = layout.get();
179
+ if (!l.visible || !l.offAxis) return p;
180
+ const cx = l.x1 + 6;
181
+ const cy = l.y;
182
+ const s = 4;
183
+ if (l.chevronUp) {
184
+ p.moveTo(cx - s, cy + s);
185
+ p.lineTo(cx, cy - s);
186
+ p.lineTo(cx + s, cy + s);
187
+ } else {
188
+ p.moveTo(cx - s, cy - s);
189
+ p.lineTo(cx, cy + s);
190
+ p.lineTo(cx + s, cy - s);
191
+ }
192
+ return p;
193
+ });
194
+
195
+ const lineOpacity = useDerivedValue(() =>
196
+ layout.get().visible && !layout.get().offAxis && !isBand ? 1 : 0,
197
+ );
198
+ const bandOpacity = useDerivedValue(() =>
199
+ layout.get().visible && isBand ? bandFillOpacity : 0,
200
+ );
201
+ const bandBorderOpacity = useDerivedValue(() =>
202
+ layout.get().visible && hasBandBorder ? 1 : 0,
203
+ );
204
+ const offOpacity = useDerivedValue(() =>
205
+ layout.get().visible && layout.get().offAxis ? 1 : 0,
206
+ );
207
+ const labelOpacity = useDerivedValue(() => (layout.get().visible ? 1 : 0));
208
+
209
+ const labelX = useDerivedValue(() => layout.get().labelX);
210
+ const labelY = useDerivedValue(() => layout.get().labelY);
211
+ const labelText = useDerivedValue(() => layout.get().label);
212
+
213
+ // Font metrics depend only on the (stable) font, so read them once instead of
214
+ // on every frame inside the pill worklets (`getMetrics` allocates + crosses
215
+ // JSI). The off-axis pill's ascent offset and height are then plain constants.
216
+ const { ascent: fontAscent, height: pillH } = (() => {
217
+ const fm = font.getMetrics();
218
+ return {
219
+ ascent: fm.ascent,
220
+ height: fm.descent - fm.ascent + OFF_AXIS_PILL_PAD_Y * 2,
221
+ };
222
+ })();
223
+
224
+ // Off-axis badge pill — a rounded background behind the chevron + label.
225
+ const pillX = useDerivedValue(() => layout.get().x1 + 2);
226
+ const pillY = useDerivedValue(
227
+ () => layout.get().labelY + fontAscent - OFF_AXIS_PILL_PAD_Y,
228
+ );
229
+ const pillW = useDerivedValue(() => {
230
+ const l = layout.get();
231
+ const textW = measureFontTextWidth(font, l.label);
232
+ return l.labelX + textW + OFF_AXIS_PILL_PAD_X - (l.x1 + 2);
233
+ });
234
+
235
+ return (
236
+ <Group>
237
+ {isBand && (
238
+ <Group opacity={bandOpacity}>
239
+ <Path path={bandPath} style="fill" color={color} />
240
+ </Group>
241
+ )}
242
+
243
+ {hasBandBorder && (
244
+ <Group opacity={bandBorderOpacity}>
245
+ <Path
246
+ path={bandBorderPath}
247
+ style="stroke"
248
+ strokeWidth={strokeWidth}
249
+ color={color}
250
+ >
251
+ <DashPathEffect intervals={intervals} />
252
+ </Path>
253
+ </Group>
254
+ )}
255
+
256
+ {!isBand && (
257
+ <Group opacity={lineOpacity}>
258
+ <Path
259
+ path={linePath}
260
+ style="stroke"
261
+ strokeWidth={strokeWidth}
262
+ color={color}
263
+ >
264
+ <DashPathEffect intervals={intervals} />
265
+ </Path>
266
+ </Group>
267
+ )}
268
+
269
+ <Group opacity={offOpacity}>
270
+ <Path
271
+ path={offLinePath}
272
+ style="stroke"
273
+ strokeWidth={strokeWidth}
274
+ color={color}
275
+ >
276
+ <DashPathEffect intervals={intervals} />
277
+ </Path>
278
+ <RoundedRect
279
+ x={pillX}
280
+ y={pillY}
281
+ width={pillW}
282
+ height={pillH}
283
+ r={badgeRadius}
284
+ color={badgeBackground}
285
+ />
286
+ <RoundedRect
287
+ x={pillX}
288
+ y={pillY}
289
+ width={pillW}
290
+ height={pillH}
291
+ r={badgeRadius}
292
+ color={badgeBorderColor}
293
+ style="stroke"
294
+ strokeWidth={1}
295
+ />
296
+ <Path
297
+ path={chevronPath}
298
+ style="stroke"
299
+ strokeWidth={1.5}
300
+ color={color}
301
+ strokeCap="round"
302
+ strokeJoin="round"
303
+ />
304
+ </Group>
305
+
306
+ <Group opacity={labelOpacity}>
307
+ <SkiaText
308
+ x={labelX}
309
+ y={labelY}
310
+ text={labelText}
311
+ font={font}
312
+ color={labelColor}
313
+ />
314
+ </Group>
315
+ </Group>
316
+ );
317
+ }
@@ -0,0 +1,201 @@
1
+ import { useState } from "react";
2
+ import { Pressable, StyleSheet, Text, View } from "react-native";
3
+ import { useAnimatedReaction, type SharedValue } from "react-native-reanimated";
4
+ import { scheduleOnRN } from "react-native-worklets";
5
+ import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
6
+ import type { ResolvedLegendConfig } from "../core/resolveConfig";
7
+ import type { LiveChartPalette, SeriesConfig } from "../types";
8
+ import { seriesMetaSig } from "./seriesMetaSig";
9
+
10
+ export interface SeriesToggleChipsProps {
11
+ series: SharedValue<SeriesConfig[]>;
12
+ legend: ResolvedLegendConfig;
13
+ /** Chart palette, used to derive theme-aware default chip colors. */
14
+ palette?: LiveChartPalette;
15
+ onSeriesToggle?: (id: string, visible: boolean) => void;
16
+ }
17
+
18
+ /**
19
+ * Neutral, readable default chip colors derived from the chart background
20
+ * luminance, so the legend stays legible on both light and dark themes. Each is
21
+ * only a fallback — matching `legend.style` overrides win.
22
+ */
23
+ function legendColorDefaults(palette: LiveChartPalette | undefined) {
24
+ const bg = palette?.bgRgb;
25
+ const isLight = bg
26
+ ? (0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2]) / 255 > 0.5
27
+ : false;
28
+ return isLight
29
+ ? {
30
+ activeBackground: "rgba(0,0,0,0.06)",
31
+ hiddenBackground: "rgba(0,0,0,0.03)",
32
+ activeColor: "rgba(0,0,0,0.85)",
33
+ hiddenColor: "rgba(0,0,0,0.4)",
34
+ }
35
+ : {
36
+ activeBackground: "rgba(255,255,255,0.12)",
37
+ hiddenBackground: "rgba(255,255,255,0.05)",
38
+ activeColor: "rgba(255,255,255,0.92)",
39
+ hiddenColor: "rgba(255,255,255,0.45)",
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Mirrors `series` into React state when id/label/color/visibility changes (not on every data tick).
45
+ */
46
+ export function SeriesToggleChips({
47
+ series,
48
+ legend,
49
+ palette,
50
+ onSeriesToggle,
51
+ }: SeriesToggleChipsProps) {
52
+ // Seed from the current series at mount; the reaction below keeps it in sync.
53
+ const [snapshot, setSnapshot] = useState<SeriesConfig[]>(() =>
54
+ series.get().slice(),
55
+ );
56
+
57
+ // Read the `series` prop from closure rather than a SharedValue passed
58
+ // through `scheduleOnRN`: the handle serialized across the worklet→JS
59
+ // boundary exposes the native `.value` accessor but NOT the `.get()` method,
60
+ // so calling `.get()` on it throws ("sv.get is not a function").
61
+ const pullSnapshot = () => {
62
+ setSnapshot(series.get().slice());
63
+ };
64
+
65
+ useAnimatedReaction(
66
+ () => seriesMetaSig(series.get()),
67
+ /* istanbul ignore next -- Reanimated reaction; snapshot seeded at mount, pulled here on change */
68
+ (sig, prev) => {
69
+ if (sig !== prev) {
70
+ scheduleOnRN(pullSnapshot);
71
+ }
72
+ },
73
+ [series, pullSnapshot],
74
+ );
75
+
76
+ if (!legend.visible) return null;
77
+
78
+ const toggle = (id: string) => {
79
+ const arr = series.get().slice();
80
+ const idx = arr.findIndex((s) => s.id === id);
81
+ /* istanbul ignore next -- defensive; chips only call with known ids */
82
+ if (idx < 0) return;
83
+ const wasVisible = arr[idx].visible !== false;
84
+ const nextVisible = !wasVisible;
85
+ const next = arr.map((s, i) =>
86
+ i === idx ? { ...s, visible: nextVisible } : s,
87
+ );
88
+ series.set(next);
89
+ setSnapshot(next);
90
+ onSeriesToggle?.(id, nextVisible);
91
+ };
92
+
93
+ const compact = legend.compact;
94
+ const st = legend.style;
95
+ const fallback = legendColorDefaults(palette);
96
+
97
+ return (
98
+ <View style={[styles.row, compact && styles.rowCompact]}>
99
+ {snapshot.map((s) => {
100
+ const on = s.visible !== false;
101
+ const label = s.label ?? s.id;
102
+ const swatchSize = st?.dotSize;
103
+ const chipBg = on
104
+ ? (st?.activeBackground ?? fallback.activeBackground)
105
+ : (st?.hiddenBackground ?? fallback.hiddenBackground);
106
+ const textColor = on
107
+ ? (st?.activeColor ?? fallback.activeColor)
108
+ : (st?.hiddenColor ?? fallback.hiddenColor);
109
+ return (
110
+ <Pressable
111
+ key={s.id}
112
+ onPress={() => toggle(s.id)}
113
+ style={[
114
+ styles.chip,
115
+ compact && styles.chipCompact,
116
+ st?.borderRadius !== undefined && { borderRadius: st.borderRadius },
117
+ { backgroundColor: chipBg },
118
+ ]}
119
+ >
120
+ <View
121
+ style={[
122
+ styles.swatch,
123
+ { backgroundColor: s.color ?? "#888" },
124
+ swatchSize !== undefined && {
125
+ width: swatchSize,
126
+ height: swatchSize,
127
+ borderRadius: swatchSize / 2,
128
+ },
129
+ ]}
130
+ />
131
+ <Text
132
+ style={[
133
+ styles.chipText,
134
+ compact && styles.chipTextCompact,
135
+ s.kind === "derived" && styles.chipTextDerived,
136
+ st?.fontSize !== undefined && { fontSize: st.fontSize },
137
+ { color: textColor },
138
+ ]}
139
+ numberOfLines={1}
140
+ >
141
+ {label}
142
+ {s.valueLabel ? (
143
+ <Text style={styles.chipValue}> {s.valueLabel}</Text>
144
+ ) : null}
145
+ </Text>
146
+ </Pressable>
147
+ );
148
+ })}
149
+ </View>
150
+ );
151
+ }
152
+
153
+ const styles = StyleSheet.create({
154
+ row: {
155
+ flexDirection: "row",
156
+ flexWrap: "wrap",
157
+ gap: 8,
158
+ marginBottom: 8,
159
+ paddingHorizontal: 4,
160
+ },
161
+ rowCompact: {
162
+ gap: 4,
163
+ marginBottom: 4,
164
+ },
165
+ chip: {
166
+ flexDirection: "row",
167
+ alignItems: "center",
168
+ gap: 6,
169
+ paddingHorizontal: 12,
170
+ paddingVertical: 8,
171
+ borderRadius: 8,
172
+ maxWidth: "48%",
173
+ },
174
+ chipCompact: {
175
+ paddingHorizontal: 8,
176
+ paddingVertical: 4,
177
+ gap: 4,
178
+ borderRadius: 6,
179
+ maxWidth: "32%",
180
+ },
181
+ swatch: {
182
+ width: 8,
183
+ height: 8,
184
+ borderRadius: 4,
185
+ },
186
+ chipText: {
187
+ flexShrink: 1,
188
+ fontSize: 13,
189
+ fontFamily: MONO_FONT_FAMILY,
190
+ },
191
+ chipTextCompact: {
192
+ fontSize: 11,
193
+ },
194
+ chipTextDerived: {
195
+ fontStyle: "italic",
196
+ opacity: 0.85,
197
+ },
198
+ chipValue: {
199
+ fontWeight: "700",
200
+ },
201
+ });