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.
- package/LICENSE +33 -0
- package/README.md +198 -0
- package/dist/components/AnimatedLabel.d.ts +21 -0
- package/dist/components/AnimatedLabel.d.ts.map +1 -0
- package/dist/components/BadgeOverlay.d.ts +16 -0
- package/dist/components/BadgeOverlay.d.ts.map +1 -0
- package/dist/components/CrosshairLine.d.ts +20 -0
- package/dist/components/CrosshairLine.d.ts.map +1 -0
- package/dist/components/CrosshairOverlay.d.ts +29 -0
- package/dist/components/CrosshairOverlay.d.ts.map +1 -0
- package/dist/components/DegenParticlesOverlay.d.ts +16 -0
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -0
- package/dist/components/DotOverlay.d.ts +17 -0
- package/dist/components/DotOverlay.d.ts.map +1 -0
- package/dist/components/LeftEdgeFade.d.ts +9 -0
- package/dist/components/LeftEdgeFade.d.ts.map +1 -0
- package/dist/components/LiveChart.d.ts +3 -0
- package/dist/components/LiveChart.d.ts.map +1 -0
- package/dist/components/LiveChartSeries.d.ts +3 -0
- package/dist/components/LiveChartSeries.d.ts.map +1 -0
- package/dist/components/LiveChartTransition.d.ts +28 -0
- package/dist/components/LiveChartTransition.d.ts.map +1 -0
- package/dist/components/LoadingOverlay.d.ts +24 -0
- package/dist/components/LoadingOverlay.d.ts.map +1 -0
- package/dist/components/MarkerOverlay.d.ts +25 -0
- package/dist/components/MarkerOverlay.d.ts.map +1 -0
- package/dist/components/MultiSeriesDots.d.ts +11 -0
- package/dist/components/MultiSeriesDots.d.ts.map +1 -0
- package/dist/components/MultiSeriesStroke.d.ts +14 -0
- package/dist/components/MultiSeriesStroke.d.ts.map +1 -0
- package/dist/components/MultiSeriesTooltipStack.d.ts +10 -0
- package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -0
- package/dist/components/MultiSeriesValueLabels.d.ts +11 -0
- package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -0
- package/dist/components/MultiSeriesValueLines.d.ts +10 -0
- package/dist/components/MultiSeriesValueLines.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +19 -0
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -0
- package/dist/components/SeriesToggleChips.d.ts +15 -0
- package/dist/components/SeriesToggleChips.d.ts.map +1 -0
- package/dist/components/TradeStreamOverlay.d.ts +14 -0
- package/dist/components/TradeStreamOverlay.d.ts.map +1 -0
- package/dist/components/ValueLineOverlay.d.ts +16 -0
- package/dist/components/ValueLineOverlay.d.ts.map +1 -0
- package/dist/components/ValueTextOverlay.d.ts +20 -0
- package/dist/components/ValueTextOverlay.d.ts.map +1 -0
- package/dist/components/XAxisOverlay.d.ts +14 -0
- package/dist/components/XAxisOverlay.d.ts.map +1 -0
- package/dist/components/YAxisOverlay.d.ts +23 -0
- package/dist/components/YAxisOverlay.d.ts.map +1 -0
- package/dist/components/seriesMetaSig.d.ts +4 -0
- package/dist/components/seriesMetaSig.d.ts.map +1 -0
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +45 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts +38 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -0
- package/dist/core/multiSeriesLayout.d.ts +22 -0
- package/dist/core/multiSeriesLayout.d.ts.map +1 -0
- package/dist/core/resolveConfig.d.ts +188 -0
- package/dist/core/resolveConfig.d.ts.map +1 -0
- package/dist/core/useLiveChartEngine.d.ts +79 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts +68 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -0
- package/dist/draw/candle.d.ts +25 -0
- package/dist/draw/candle.d.ts.map +1 -0
- package/dist/draw/grid.d.ts +24 -0
- package/dist/draw/grid.d.ts.map +1 -0
- package/dist/draw/line.d.ts +86 -0
- package/dist/draw/line.d.ts.map +1 -0
- package/dist/draw/trade.d.ts +38 -0
- package/dist/draw/trade.d.ts.map +1 -0
- package/dist/hooks/crosshairSeries.d.ts +13 -0
- package/dist/hooks/crosshairSeries.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +72 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +24 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +42 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -0
- package/dist/hooks/useBadge.d.ts +14 -0
- package/dist/hooks/useBadge.d.ts.map +1 -0
- package/dist/hooks/useCandlePaths.d.ts +19 -0
- package/dist/hooks/useCandlePaths.d.ts.map +1 -0
- package/dist/hooks/useCanvasLayout.d.ts +12 -0
- package/dist/hooks/useCanvasLayout.d.ts.map +1 -0
- package/dist/hooks/useChartColors.d.ts +20 -0
- package/dist/hooks/useChartColors.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +19 -0
- package/dist/hooks/useChartPaths.d.ts.map +1 -0
- package/dist/hooks/useChartReveal.d.ts +30 -0
- package/dist/hooks/useChartReveal.d.ts.map +1 -0
- package/dist/hooks/useChartSkiaFont.d.ts +8 -0
- package/dist/hooks/useChartSkiaFont.d.ts.map +1 -0
- package/dist/hooks/useCrosshair.d.ts +20 -0
- package/dist/hooks/useCrosshair.d.ts.map +1 -0
- package/dist/hooks/useCrosshairSeries.d.ts +10 -0
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -0
- package/dist/hooks/useDegen.d.ts +17 -0
- package/dist/hooks/useDegen.d.ts.map +1 -0
- package/dist/hooks/useLiveChartHasData.d.ts +17 -0
- package/dist/hooks/useLiveChartHasData.d.ts.map +1 -0
- package/dist/hooks/useLiveDot.d.ts +12 -0
- package/dist/hooks/useLiveDot.d.ts.map +1 -0
- package/dist/hooks/useMarkers.d.ts +15 -0
- package/dist/hooks/useMarkers.d.ts.map +1 -0
- package/dist/hooks/useModeBlend.d.ts +18 -0
- package/dist/hooks/useModeBlend.d.ts.map +1 -0
- package/dist/hooks/useMomentum.d.ts +16 -0
- package/dist/hooks/useMomentum.d.ts.map +1 -0
- package/dist/hooks/useMultiSeriesDegen.d.ts +25 -0
- package/dist/hooks/useMultiSeriesDegen.d.ts.map +1 -0
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +15 -0
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +32 -0
- package/dist/hooks/useReferenceLine.d.ts.map +1 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts +24 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -0
- package/dist/hooks/useTradeStream.d.ts +12 -0
- package/dist/hooks/useTradeStream.d.ts.map +1 -0
- package/dist/hooks/useXAxis.d.ts +32 -0
- package/dist/hooks/useXAxis.d.ts.map +1 -0
- package/dist/hooks/useYAxis.d.ts +13 -0
- package/dist/hooks/useYAxis.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/lib/format.d.ts +5 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/measureFontTextWidth.d.ts +7 -0
- package/dist/lib/measureFontTextWidth.d.ts.map +1 -0
- package/dist/lib/monoFontFamily.d.ts +6 -0
- package/dist/lib/monoFontFamily.d.ts.map +1 -0
- package/dist/math/color.d.ts +5 -0
- package/dist/math/color.d.ts.map +1 -0
- package/dist/math/degenTick.d.ts +57 -0
- package/dist/math/degenTick.d.ts.map +1 -0
- package/dist/math/interpolate.d.ts +7 -0
- package/dist/math/interpolate.d.ts.map +1 -0
- package/dist/math/intervals.d.ts +3 -0
- package/dist/math/intervals.d.ts.map +1 -0
- package/dist/math/lerp.d.ts +7 -0
- package/dist/math/lerp.d.ts.map +1 -0
- package/dist/math/markers.d.ts +44 -0
- package/dist/math/markers.d.ts.map +1 -0
- package/dist/math/momentum.d.ts +8 -0
- package/dist/math/momentum.d.ts.map +1 -0
- package/dist/math/pickCandle.d.ts +8 -0
- package/dist/math/pickCandle.d.ts.map +1 -0
- package/dist/math/range.d.ts +10 -0
- package/dist/math/range.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +20 -0
- package/dist/math/referenceLines.d.ts.map +1 -0
- package/dist/math/spline.d.ts +45 -0
- package/dist/math/spline.d.ts.map +1 -0
- package/dist/math/squiggly.d.ts +36 -0
- package/dist/math/squiggly.d.ts.map +1 -0
- package/dist/theme.d.ts +30 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/types.d.ts +720 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +72 -0
- package/src/components/AnimatedLabel.tsx +41 -0
- package/src/components/BadgeOverlay.tsx +39 -0
- package/src/components/CrosshairLine.tsx +90 -0
- package/src/components/CrosshairOverlay.tsx +170 -0
- package/src/components/DegenParticlesOverlay.tsx +141 -0
- package/src/components/DotOverlay.tsx +69 -0
- package/src/components/LeftEdgeFade.tsx +36 -0
- package/src/components/LiveChart.tsx +821 -0
- package/src/components/LiveChartSeries.tsx +607 -0
- package/src/components/LiveChartTransition.tsx +135 -0
- package/src/components/LoadingOverlay.tsx +301 -0
- package/src/components/MarkerOverlay.tsx +331 -0
- package/src/components/MultiSeriesDots.tsx +117 -0
- package/src/components/MultiSeriesStroke.tsx +79 -0
- package/src/components/MultiSeriesTooltipStack.tsx +78 -0
- package/src/components/MultiSeriesValueLabels.tsx +103 -0
- package/src/components/MultiSeriesValueLines.tsx +115 -0
- package/src/components/ReferenceLineOverlay.tsx +317 -0
- package/src/components/SeriesToggleChips.tsx +201 -0
- package/src/components/TradeStreamOverlay.tsx +117 -0
- package/src/components/ValueLineOverlay.tsx +63 -0
- package/src/components/ValueTextOverlay.tsx +43 -0
- package/src/components/XAxisOverlay.tsx +109 -0
- package/src/components/YAxisOverlay.tsx +135 -0
- package/src/components/seriesMetaSig.ts +13 -0
- package/src/constants.ts +44 -0
- package/src/core/liveChartEngineTick.ts +185 -0
- package/src/core/liveChartSeriesEngineTick.ts +163 -0
- package/src/core/multiSeriesLayout.ts +80 -0
- package/src/core/resolveConfig.ts +538 -0
- package/src/core/useLiveChartEngine.ts +207 -0
- package/src/core/useLiveChartSeriesEngine.ts +215 -0
- package/src/draw/candle.ts +210 -0
- package/src/draw/grid.ts +174 -0
- package/src/draw/line.ts +257 -0
- package/src/draw/trade.ts +202 -0
- package/src/hooks/crosshairSeries.ts +78 -0
- package/src/hooks/crosshairShared.ts +296 -0
- package/src/hooks/index.ts +26 -0
- package/src/hooks/resolveChartLayout.ts +144 -0
- package/src/hooks/useBadge.ts +192 -0
- package/src/hooks/useCandlePaths.ts +165 -0
- package/src/hooks/useCanvasLayout.ts +22 -0
- package/src/hooks/useChartColors.ts +48 -0
- package/src/hooks/useChartPaths.ts +121 -0
- package/src/hooks/useChartReveal.ts +123 -0
- package/src/hooks/useChartSkiaFont.ts +34 -0
- package/src/hooks/useCrosshair.ts +250 -0
- package/src/hooks/useCrosshairSeries.ts +163 -0
- package/src/hooks/useDegen.ts +262 -0
- package/src/hooks/useLiveChartHasData.ts +31 -0
- package/src/hooks/useLiveDot.ts +31 -0
- package/src/hooks/useMarkers.ts +95 -0
- package/src/hooks/useModeBlend.ts +52 -0
- package/src/hooks/useMomentum.ts +37 -0
- package/src/hooks/useMultiSeriesDegen.ts +260 -0
- package/src/hooks/useMultiSeriesLinePaths.ts +76 -0
- package/src/hooks/useReferenceLine.ts +212 -0
- package/src/hooks/useReverseMorphEngineInputs.ts +154 -0
- package/src/hooks/useTradeStream.ts +64 -0
- package/src/hooks/useXAxis.ts +200 -0
- package/src/hooks/useYAxis.ts +48 -0
- package/src/index.ts +69 -0
- package/src/lib/format.ts +36 -0
- package/src/lib/measureFontTextWidth.ts +10 -0
- package/src/lib/monoFontFamily.ts +8 -0
- package/src/math/color.ts +31 -0
- package/src/math/degenTick.ts +151 -0
- package/src/math/interpolate.ts +32 -0
- package/src/math/intervals.ts +17 -0
- package/src/math/lerp.ts +17 -0
- package/src/math/markers.ts +175 -0
- package/src/math/momentum.ts +38 -0
- package/src/math/pickCandle.ts +42 -0
- package/src/math/range.ts +54 -0
- package/src/math/referenceLines.ts +56 -0
- package/src/math/spline.ts +183 -0
- package/src/math/squiggly.ts +108 -0
- package/src/theme.ts +231 -0
- 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
|
+
});
|