react-native-livechart 2.0.1 → 3.1.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/README.md +9 -3
- package/dist/components/AxisLabelOverlay.d.ts +29 -0
- package/dist/components/AxisLabelOverlay.d.ts.map +1 -0
- package/dist/components/CrosshairLine.d.ts +12 -1
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +15 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts +17 -0
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesValueLines.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/SelectionDot.d.ts +32 -0
- package/dist/components/SelectionDot.d.ts.map +1 -0
- package/dist/components/ValueLineOverlay.d.ts.map +1 -1
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +46 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +6 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/draw/particleAtlas.d.ts +39 -0
- package/dist/draw/particleAtlas.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +10 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useBadge.d.ts +2 -2
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +11 -11
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartColors.d.ts +5 -0
- package/dist/hooks/useChartColors.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +8 -7
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useChartReveal.d.ts +3 -1
- package/dist/hooks/useChartReveal.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +1 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts +3 -1
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +3 -1
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +9 -8
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/hooks/usePathBuilder.d.ts +27 -0
- package/dist/hooks/usePathBuilder.d.ts.map +1 -0
- package/dist/hooks/useTradeStream.d.ts +3 -1
- package/dist/hooks/useTradeStream.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/spline.d.ts +10 -2
- package/dist/math/spline.d.ts.map +1 -1
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/AxisLabelOverlay.tsx +133 -0
- package/src/components/CrosshairLine.tsx +27 -0
- package/src/components/CrosshairOverlay.tsx +50 -18
- package/src/components/DegenParticlesOverlay.tsx +93 -109
- package/src/components/LiveChart.tsx +66 -10
- package/src/components/LiveChartSeries.tsx +40 -0
- package/src/components/LoadingOverlay.tsx +15 -29
- package/src/components/MarkerOverlay.tsx +23 -28
- package/src/components/MultiSeriesValueLines.tsx +20 -40
- package/src/components/ReferenceLineOverlay.tsx +65 -107
- package/src/components/SelectionDot.tsx +92 -0
- package/src/components/ValueLineOverlay.tsx +9 -28
- package/src/components/XAxisOverlay.tsx +9 -29
- package/src/components/YAxisOverlay.tsx +6 -22
- package/src/core/resolveConfig.ts +98 -0
- package/src/core/useLiveChartEngine.ts +78 -27
- package/src/draw/markerAtlas.ts +9 -9
- package/src/draw/particleAtlas.ts +95 -0
- package/src/hooks/crosshairShared.ts +26 -0
- package/src/hooks/useBadge.ts +17 -32
- package/src/hooks/useCandlePaths.ts +27 -64
- package/src/hooks/useChartColors.ts +19 -0
- package/src/hooks/useChartPaths.ts +27 -39
- package/src/hooks/useChartReveal.ts +7 -4
- package/src/hooks/useCrosshair.ts +46 -7
- package/src/hooks/useCrosshairSeries.ts +47 -8
- package/src/hooks/useDegen.ts +6 -0
- package/src/hooks/useMarkers.ts +3 -0
- package/src/hooks/useMultiSeriesLinePaths.ts +26 -28
- package/src/hooks/usePathBuilder.ts +42 -0
- package/src/hooks/useTradeStream.ts +3 -0
- package/src/index.ts +4 -0
- package/src/math/spline.ts +22 -2
- package/src/types.ts +101 -0
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type SeriesLineStyle,
|
|
25
25
|
} from "../core/multiSeriesLayout";
|
|
26
26
|
import {
|
|
27
|
+
resolveAxisLabel,
|
|
27
28
|
resolveDegen,
|
|
28
29
|
resolveGridStyle,
|
|
29
30
|
resolveLeftEdgeFade,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
resolveMetrics,
|
|
32
33
|
resolveMultiSeriesDot,
|
|
33
34
|
resolveScrub,
|
|
35
|
+
resolveSelectionDot,
|
|
34
36
|
resolveXAxis,
|
|
35
37
|
resolveYAxis,
|
|
36
38
|
} from "../core/resolveConfig";
|
|
@@ -60,6 +62,7 @@ import {
|
|
|
60
62
|
resolveTheme,
|
|
61
63
|
} from "../theme";
|
|
62
64
|
import type { LiveChartSeriesProps, Marker, SeriesConfig } from "../types";
|
|
65
|
+
import { AxisLabelOverlay } from "./AxisLabelOverlay";
|
|
63
66
|
import { CrosshairLine } from "./CrosshairLine";
|
|
64
67
|
import { DegenParticlesOverlay } from "./DegenParticlesOverlay";
|
|
65
68
|
import { LeftEdgeFade } from "./LeftEdgeFade";
|
|
@@ -121,12 +124,17 @@ function useLiveChartSeriesController({
|
|
|
121
124
|
formatTime = defaultFormatTime,
|
|
122
125
|
yAxis = true,
|
|
123
126
|
xAxis = true,
|
|
127
|
+
topLabel,
|
|
128
|
+
bottomLabel,
|
|
124
129
|
referenceLines,
|
|
125
130
|
gridStyle,
|
|
126
131
|
palette: paletteOverride,
|
|
127
132
|
metrics,
|
|
128
133
|
scrub = true,
|
|
134
|
+
selectionDot,
|
|
129
135
|
onScrub,
|
|
136
|
+
onGestureStart,
|
|
137
|
+
onGestureEnd,
|
|
130
138
|
onSeriesToggle,
|
|
131
139
|
dot: dotProp,
|
|
132
140
|
legend: legendProp,
|
|
@@ -142,8 +150,11 @@ function useLiveChartSeriesController({
|
|
|
142
150
|
const markersActive = markers != null;
|
|
143
151
|
const yAxisCfg = resolveYAxis(yAxis);
|
|
144
152
|
const xAxisCfg = resolveXAxis(xAxis);
|
|
153
|
+
const topLabelCfg = resolveAxisLabel(topLabel);
|
|
154
|
+
const bottomLabelCfg = resolveAxisLabel(bottomLabel);
|
|
145
155
|
const scrubCfg = resolveScrub(scrub);
|
|
146
156
|
const scrubEnabled = scrubCfg !== null;
|
|
157
|
+
const selectionDotCfg = resolveSelectionDot(selectionDot);
|
|
147
158
|
const gridStyleCfg = resolveGridStyle(gridStyle);
|
|
148
159
|
const dotCfg = resolveMultiSeriesDot(dotProp);
|
|
149
160
|
// Outer footprint of a dot (the color-filled radius plus the halo ring).
|
|
@@ -307,6 +318,8 @@ function useLiveChartSeriesController({
|
|
|
307
318
|
scrubEnabled,
|
|
308
319
|
onScrub,
|
|
309
320
|
scrubCfg?.panGestureDelay ?? 0,
|
|
321
|
+
onGestureStart,
|
|
322
|
+
onGestureEnd,
|
|
310
323
|
);
|
|
311
324
|
|
|
312
325
|
// `projected` is used internally by the hit-test gesture; the overlay
|
|
@@ -373,6 +386,12 @@ function useLiveChartSeriesController({
|
|
|
373
386
|
rootGesture,
|
|
374
387
|
markersActive,
|
|
375
388
|
markersSV,
|
|
389
|
+
// selection dot: resolved config + fallback color (the leading series' color)
|
|
390
|
+
selectionDot: selectionDotCfg,
|
|
391
|
+
selectionColor: lineColors[0],
|
|
392
|
+
// RN axis edge labels (floated over the canvas as a sibling layer)
|
|
393
|
+
topLabelCfg,
|
|
394
|
+
bottomLabelCfg,
|
|
376
395
|
};
|
|
377
396
|
}
|
|
378
397
|
|
|
@@ -591,6 +610,11 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
591
610
|
palette,
|
|
592
611
|
dotCfg,
|
|
593
612
|
dotOuterRadius,
|
|
613
|
+
selectionDot,
|
|
614
|
+
selectionColor,
|
|
615
|
+
formatValue,
|
|
616
|
+
topLabelCfg,
|
|
617
|
+
bottomLabelCfg,
|
|
594
618
|
} = model;
|
|
595
619
|
|
|
596
620
|
// Extend the scrub dim past the plot's right edge to fully cover the series
|
|
@@ -647,6 +671,10 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
647
671
|
engine={engine}
|
|
648
672
|
padding={effectivePadding}
|
|
649
673
|
palette={palette}
|
|
674
|
+
selectionDot={selectionDot}
|
|
675
|
+
selectionY={crosshair.scrubDotY}
|
|
676
|
+
scrubActive={crosshair.scrubActive}
|
|
677
|
+
selectionColor={selectionColor}
|
|
650
678
|
dimOpacity={scrubCfg.dimOpacity}
|
|
651
679
|
liveDotExtent={liveDotExtent}
|
|
652
680
|
crosshairLineColor={scrubCfg.crosshairLineColor}
|
|
@@ -658,6 +686,18 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
658
686
|
clips them (they track each series' live value, not the scrub). */}
|
|
659
687
|
<SeriesValueLabelLayer model={model} />
|
|
660
688
|
</Canvas>
|
|
689
|
+
|
|
690
|
+
{/* RN labels floated over the canvas (sibling of <Canvas>, an RN
|
|
691
|
+
view). Inside the canvas wrapper so its top/bottom edges align
|
|
692
|
+
with the plot area, not the legend row. */}
|
|
693
|
+
<AxisLabelOverlay
|
|
694
|
+
topLabel={topLabelCfg}
|
|
695
|
+
bottomLabel={bottomLabelCfg}
|
|
696
|
+
engine={engine}
|
|
697
|
+
formatValue={formatValue}
|
|
698
|
+
defaultColor={palette.gridLabel}
|
|
699
|
+
padding={effectivePadding}
|
|
700
|
+
/>
|
|
661
701
|
</View>
|
|
662
702
|
</GestureDetector>
|
|
663
703
|
{legendCfg.position === "bottom" ? legend : null}
|
|
@@ -4,13 +4,10 @@ import {
|
|
|
4
4
|
Path,
|
|
5
5
|
Rect,
|
|
6
6
|
RoundedRect,
|
|
7
|
-
Skia,
|
|
8
7
|
Text as SkiaText,
|
|
9
8
|
vec,
|
|
10
9
|
type SkFont,
|
|
11
|
-
type SkPath,
|
|
12
10
|
} from "@shopify/react-native-skia";
|
|
13
|
-
import { useRef } from "react";
|
|
14
11
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
15
12
|
import {
|
|
16
13
|
BADGE_METRICS_DEFAULTS,
|
|
@@ -22,6 +19,10 @@ import {
|
|
|
22
19
|
pillTextLeftX,
|
|
23
20
|
type ChartPadding,
|
|
24
21
|
} from "../draw/line";
|
|
22
|
+
import {
|
|
23
|
+
usePathBuilder,
|
|
24
|
+
type ReanimatedPathBuilder,
|
|
25
|
+
} from "../hooks/usePathBuilder";
|
|
25
26
|
import { drawSpline } from "../math/spline";
|
|
26
27
|
import { buildSquigglyPts } from "../math/squiggly";
|
|
27
28
|
import type {
|
|
@@ -37,14 +38,14 @@ const RECT_H = 4;
|
|
|
37
38
|
const RECT_R = 4;
|
|
38
39
|
const RECT_SPACING = 32;
|
|
39
40
|
|
|
40
|
-
function
|
|
41
|
+
function buildSplineDetached(b: ReanimatedPathBuilder, pts: number[]) {
|
|
41
42
|
"worklet";
|
|
42
|
-
path.reset();
|
|
43
43
|
const n = pts.length >> 1;
|
|
44
|
-
if (n
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
if (n >= 2) {
|
|
45
|
+
b.moveTo(pts[0], pts[1]);
|
|
46
|
+
drawSpline(b, pts);
|
|
47
|
+
}
|
|
48
|
+
return b.detach();
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export function LoadingOverlay({
|
|
@@ -85,29 +86,14 @@ export function LoadingOverlay({
|
|
|
85
86
|
const leftInset =
|
|
86
87
|
badgeMetrics.dotGap + badgeTailAndCap(font.getSize(), badgeTail, badgeMetrics);
|
|
87
88
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
const squigglyCacheRef = useRef<{
|
|
91
|
-
a: SkPath;
|
|
92
|
-
b: SkPath;
|
|
93
|
-
tick: boolean;
|
|
94
|
-
} | null>(null);
|
|
95
|
-
if (squigglyCacheRef.current === null) {
|
|
96
|
-
squigglyCacheRef.current = {
|
|
97
|
-
a: Skia.Path.Make(),
|
|
98
|
-
b: Skia.Path.Make(),
|
|
99
|
-
tick: false,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
89
|
+
// Squiggly path — built into a reused PathBuilder and detach()-ed each frame.
|
|
90
|
+
const squigglyBuilder = usePathBuilder();
|
|
102
91
|
|
|
103
92
|
// Squiggly path — animated each frame via timestamp
|
|
104
93
|
const squigglyPath = useDerivedValue(() => {
|
|
105
|
-
const
|
|
106
|
-
squigglyCache.tick = !squigglyCache.tick;
|
|
107
|
-
const path = squigglyCache.tick ? squigglyCache.a : squigglyCache.b;
|
|
94
|
+
const b = squigglyBuilder.value;
|
|
108
95
|
if (!isLoading.get() && !isEmpty.value && morphT.get() >= 1) {
|
|
109
|
-
|
|
110
|
-
return path;
|
|
96
|
+
return b.detach();
|
|
111
97
|
}
|
|
112
98
|
const pts = buildSquigglyPts(
|
|
113
99
|
engine.canvasWidth.get(),
|
|
@@ -115,7 +101,7 @@ export function LoadingOverlay({
|
|
|
115
101
|
padding,
|
|
116
102
|
engine.timestamp.get(),
|
|
117
103
|
);
|
|
118
|
-
return
|
|
104
|
+
return buildSplineDetached(b, pts);
|
|
119
105
|
});
|
|
120
106
|
|
|
121
107
|
// Fades out quickly as the reveal animation begins (first 33% of morphT)
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
Path,
|
|
5
5
|
Skia,
|
|
6
6
|
type SkFont,
|
|
7
|
-
type SkPath,
|
|
8
7
|
} from "@shopify/react-native-skia";
|
|
9
8
|
import { useMemo, useRef, useState } from "react";
|
|
10
9
|
import {
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
import { scheduleOnRN } from "react-native-worklets";
|
|
16
15
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
17
16
|
import type { ChartPadding } from "../draw/line";
|
|
17
|
+
import { usePathBuilder } from "../hooks/usePathBuilder";
|
|
18
18
|
import {
|
|
19
19
|
buildMarkerAtlas,
|
|
20
20
|
defaultMarkerColor,
|
|
@@ -84,37 +84,32 @@ function ConnectorGlyph({
|
|
|
84
84
|
|
|
85
85
|
const opacity = useDerivedValue(() => (layout.get().visible ? 1 : 0));
|
|
86
86
|
|
|
87
|
-
const
|
|
88
|
-
if (cacheRef.current === null) {
|
|
89
|
-
cacheRef.current = { a: Skia.Path.Make(), b: Skia.Path.Make(), tick: false };
|
|
90
|
-
}
|
|
87
|
+
const glyphBuilder = usePathBuilder();
|
|
91
88
|
|
|
92
89
|
const glyphPath = useDerivedValue(() => {
|
|
93
|
-
const
|
|
94
|
-
cache.tick = !cache.tick;
|
|
95
|
-
const p: SkPath = cache.tick ? cache.a : cache.b;
|
|
96
|
-
p.reset();
|
|
90
|
+
const b = glyphBuilder.value;
|
|
97
91
|
const l = layout.get();
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
92
|
+
if (l.visible) {
|
|
93
|
+
const x = l.x;
|
|
94
|
+
const y = l.y;
|
|
95
|
+
if (kind === "graduation") {
|
|
96
|
+
b.moveTo(x, axisY.get());
|
|
97
|
+
b.lineTo(x, y);
|
|
98
|
+
b.moveTo(x, y - 7);
|
|
99
|
+
b.lineTo(x + 8, y - 4);
|
|
100
|
+
b.lineTo(x, y - 1);
|
|
101
|
+
b.close();
|
|
102
|
+
} else if (kind === "clawback") {
|
|
103
|
+
const s = 5;
|
|
104
|
+
const ay = axisY.get() - s;
|
|
105
|
+
b.moveTo(x - s, ay);
|
|
106
|
+
b.lineTo(x + s, ay);
|
|
107
|
+
b.lineTo(x + s, ay + s * 2);
|
|
108
|
+
b.lineTo(x - s, ay + s * 2);
|
|
109
|
+
b.close();
|
|
110
|
+
}
|
|
116
111
|
}
|
|
117
|
-
return
|
|
112
|
+
return b.detach();
|
|
118
113
|
});
|
|
119
114
|
|
|
120
115
|
const fill = kind === "clawback";
|
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DashPathEffect,
|
|
3
|
-
Group,
|
|
4
|
-
Path,
|
|
5
|
-
Skia,
|
|
6
|
-
type SkPath,
|
|
7
|
-
} from "@shopify/react-native-skia";
|
|
1
|
+
import { DashPathEffect, Group, Path } from "@shopify/react-native-skia";
|
|
8
2
|
|
|
9
|
-
import { useRef } from "react";
|
|
10
3
|
import { useDerivedValue } from "react-native-reanimated";
|
|
11
4
|
import { MAX_MULTI_SERIES } from "../constants";
|
|
12
5
|
import type { ChartPadding } from "../draw/line";
|
|
13
6
|
import type { ResolvedValueLineConfig } from "../core/resolveConfig";
|
|
14
7
|
import type { MultiEngineState } from "../core/useLiveChartEngine";
|
|
8
|
+
import { usePathBuilder } from "../hooks/usePathBuilder";
|
|
15
9
|
|
|
16
10
|
function SeriesValueLineAtIndex({
|
|
17
11
|
index,
|
|
@@ -26,44 +20,30 @@ function SeriesValueLineAtIndex({
|
|
|
26
20
|
color: string;
|
|
27
21
|
config: ResolvedValueLineConfig;
|
|
28
22
|
}) {
|
|
29
|
-
const
|
|
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
|
-
}
|
|
23
|
+
const builder = usePathBuilder();
|
|
41
24
|
|
|
42
25
|
const path = useDerivedValue(() => {
|
|
43
|
-
const
|
|
44
|
-
cache.tick = !cache.tick;
|
|
45
|
-
const p = cache.tick ? cache.a : cache.b;
|
|
46
|
-
p.reset();
|
|
26
|
+
const b = builder.value;
|
|
47
27
|
const h = engine.canvasHeight.get();
|
|
48
|
-
if (h === 0) return p;
|
|
49
28
|
const s = engine.series.get();
|
|
50
29
|
const displays = engine.displaySeriesValues.get();
|
|
51
|
-
if (index
|
|
30
|
+
if (h !== 0 && index < s.length) {
|
|
31
|
+
const chartH = h - padding.top - padding.bottom;
|
|
32
|
+
const dMin = engine.displayMin.get();
|
|
33
|
+
const dMax = engine.displayMax.get();
|
|
34
|
+
const valRange = dMax - dMin;
|
|
35
|
+
const v = displays[index] ?? s[index].value;
|
|
36
|
+
const y =
|
|
37
|
+
valRange === 0
|
|
38
|
+
? padding.top + chartH / 2
|
|
39
|
+
: padding.top + ((dMax - v) / valRange) * chartH;
|
|
52
40
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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;
|
|
41
|
+
if (y >= 0) {
|
|
42
|
+
b.moveTo(padding.left, y);
|
|
43
|
+
b.lineTo(engine.canvasWidth.get() - padding.right, y);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return b.detach();
|
|
67
47
|
});
|
|
68
48
|
|
|
69
49
|
const opacity = useDerivedValue(() => {
|
|
@@ -3,16 +3,14 @@ import {
|
|
|
3
3
|
Group,
|
|
4
4
|
Path,
|
|
5
5
|
RoundedRect,
|
|
6
|
-
Skia,
|
|
7
6
|
Text as SkiaText,
|
|
8
7
|
type SkFont,
|
|
9
|
-
type SkPath,
|
|
10
8
|
} from "@shopify/react-native-skia";
|
|
11
|
-
import { useRef } from "react";
|
|
12
9
|
import { useDerivedValue } from "react-native-reanimated";
|
|
13
10
|
|
|
14
11
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
15
12
|
import type { ChartPadding } from "../draw/line";
|
|
13
|
+
import { usePathBuilder } from "../hooks/usePathBuilder";
|
|
16
14
|
import { useReferenceLine } from "../hooks/useReferenceLine";
|
|
17
15
|
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
18
16
|
import { referenceLineForm } from "../math/referenceLines";
|
|
@@ -65,131 +63,91 @@ export function ReferenceLineOverlay({
|
|
|
65
63
|
const badgeBorderColor = line.badgeBorderColor ?? color;
|
|
66
64
|
const badgeRadius = line.badgeRadius ?? OFF_AXIS_PILL_RADIUS;
|
|
67
65
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
}
|
|
66
|
+
const lineBuilder = usePathBuilder();
|
|
67
|
+
const bandBuilder = usePathBuilder();
|
|
68
|
+
const borderBuilder = usePathBuilder();
|
|
69
|
+
const offBuilder = usePathBuilder();
|
|
70
|
+
const chevBuilder = usePathBuilder();
|
|
104
71
|
|
|
105
72
|
const linePath = useDerivedValue(() => {
|
|
106
|
-
const
|
|
107
|
-
cache.lineT = !cache.lineT;
|
|
108
|
-
const p = cache.lineT ? cache.lineA : cache.lineB;
|
|
109
|
-
p.reset();
|
|
73
|
+
const b = lineBuilder.value;
|
|
110
74
|
const l = layout.get();
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
75
|
+
if (l.visible && !l.offAxis && !isBand) {
|
|
76
|
+
b.moveTo(l.x1, l.y);
|
|
77
|
+
b.lineTo(l.x2, l.y);
|
|
78
|
+
}
|
|
79
|
+
return b.detach();
|
|
115
80
|
});
|
|
116
81
|
|
|
117
82
|
const bandPath = useDerivedValue(() => {
|
|
118
|
-
const
|
|
119
|
-
cache.bandT = !cache.bandT;
|
|
120
|
-
const p = cache.bandT ? cache.bandA : cache.bandB;
|
|
121
|
-
p.reset();
|
|
83
|
+
const b = bandBuilder.value;
|
|
122
84
|
const l = layout.get();
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
85
|
+
if (l.visible && isBand) {
|
|
86
|
+
b.moveTo(l.x1, l.y);
|
|
87
|
+
b.lineTo(l.x2, l.y);
|
|
88
|
+
b.lineTo(l.x2, l.yBottom);
|
|
89
|
+
b.lineTo(l.x1, l.yBottom);
|
|
90
|
+
b.close();
|
|
91
|
+
}
|
|
92
|
+
return b.detach();
|
|
130
93
|
});
|
|
131
94
|
|
|
132
95
|
const bandBorderPath = useDerivedValue(() => {
|
|
133
|
-
const
|
|
134
|
-
cache.borderT = !cache.borderT;
|
|
135
|
-
const p = cache.borderT ? cache.borderA : cache.borderB;
|
|
136
|
-
p.reset();
|
|
96
|
+
const b = borderBuilder.value;
|
|
137
97
|
const l = layout.get();
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
98
|
+
if (l.visible && hasBandBorder) {
|
|
99
|
+
if (form === "time-band") {
|
|
100
|
+
// Vertical edges at the band's left / right.
|
|
101
|
+
b.moveTo(l.x1, l.y);
|
|
102
|
+
b.lineTo(l.x1, l.yBottom);
|
|
103
|
+
b.moveTo(l.x2, l.y);
|
|
104
|
+
b.lineTo(l.x2, l.yBottom);
|
|
105
|
+
} else {
|
|
106
|
+
// Horizontal edges at the band's top / bottom.
|
|
107
|
+
b.moveTo(l.x1, l.y);
|
|
108
|
+
b.lineTo(l.x2, l.y);
|
|
109
|
+
b.moveTo(l.x1, l.yBottom);
|
|
110
|
+
b.lineTo(l.x2, l.yBottom);
|
|
111
|
+
}
|
|
151
112
|
}
|
|
152
|
-
return
|
|
113
|
+
return b.detach();
|
|
153
114
|
});
|
|
154
115
|
|
|
155
116
|
const offLinePath = useDerivedValue(() => {
|
|
156
|
-
const
|
|
157
|
-
cache.offT = !cache.offT;
|
|
158
|
-
const p = cache.offT ? cache.offA : cache.offB;
|
|
159
|
-
p.reset();
|
|
117
|
+
const b = offBuilder.value;
|
|
160
118
|
const l = layout.get();
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
119
|
+
if (l.visible && l.offAxis) {
|
|
120
|
+
// Start the connector just past the badge pill's right edge so the dashed
|
|
121
|
+
// line runs out to the chart edge rather than behind the badge.
|
|
122
|
+
const pillRight =
|
|
123
|
+
l.labelX + measureFontTextWidth(font, l.label) + OFF_AXIS_PILL_PAD_X;
|
|
124
|
+
const start = pillRight + 4;
|
|
125
|
+
if (start < l.x2) {
|
|
126
|
+
b.moveTo(start, l.y);
|
|
127
|
+
b.lineTo(l.x2, l.y);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return b.detach();
|
|
171
131
|
});
|
|
172
132
|
|
|
173
133
|
const chevronPath = useDerivedValue(() => {
|
|
174
|
-
const
|
|
175
|
-
cache.chevT = !cache.chevT;
|
|
176
|
-
const p = cache.chevT ? cache.chevA : cache.chevB;
|
|
177
|
-
p.reset();
|
|
134
|
+
const b = chevBuilder.value;
|
|
178
135
|
const l = layout.get();
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
136
|
+
if (l.visible && l.offAxis) {
|
|
137
|
+
const cx = l.x1 + 6;
|
|
138
|
+
const cy = l.y;
|
|
139
|
+
const s = 4;
|
|
140
|
+
if (l.chevronUp) {
|
|
141
|
+
b.moveTo(cx - s, cy + s);
|
|
142
|
+
b.lineTo(cx, cy - s);
|
|
143
|
+
b.lineTo(cx + s, cy + s);
|
|
144
|
+
} else {
|
|
145
|
+
b.moveTo(cx - s, cy - s);
|
|
146
|
+
b.lineTo(cx, cy + s);
|
|
147
|
+
b.lineTo(cx + s, cy - s);
|
|
148
|
+
}
|
|
191
149
|
}
|
|
192
|
-
return
|
|
150
|
+
return b.detach();
|
|
193
151
|
});
|
|
194
152
|
|
|
195
153
|
const lineOpacity = useDerivedValue(() =>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Circle, Group } from "@shopify/react-native-skia";
|
|
2
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
3
|
+
import type { ResolvedSelectionDotConfig } from "../core/resolveConfig";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Built-in selection dot: a filled circle at the scrub intersection plus an
|
|
7
|
+
* optional subtle outer ring (the `ring` config). Drawn only when `y >= 0` (the
|
|
8
|
+
* sentinel for "no dot") — an off-screen Y hides it naturally.
|
|
9
|
+
*/
|
|
10
|
+
export function DefaultSelectionDot({
|
|
11
|
+
x,
|
|
12
|
+
y,
|
|
13
|
+
opacity,
|
|
14
|
+
color,
|
|
15
|
+
size,
|
|
16
|
+
ring,
|
|
17
|
+
}: {
|
|
18
|
+
x: SharedValue<number>;
|
|
19
|
+
y: SharedValue<number>;
|
|
20
|
+
opacity: SharedValue<number>;
|
|
21
|
+
color: string;
|
|
22
|
+
size: number;
|
|
23
|
+
ring: ResolvedSelectionDotConfig["ring"];
|
|
24
|
+
}) {
|
|
25
|
+
// Push the dot off-screen when there's no value to mark (y < 0), so the
|
|
26
|
+
// sentinel never paints a stray dot at the top of the plot.
|
|
27
|
+
const cy = useDerivedValue(() => (y.get() < 0 ? -size * 4 : y.get()));
|
|
28
|
+
return (
|
|
29
|
+
<Group opacity={opacity}>
|
|
30
|
+
{ring && (
|
|
31
|
+
<Circle
|
|
32
|
+
cx={x}
|
|
33
|
+
cy={cy}
|
|
34
|
+
r={size + ring.width}
|
|
35
|
+
color={ring.color ?? color}
|
|
36
|
+
opacity={0.25}
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
<Circle cx={x} cy={cy} r={size} color={color} />
|
|
40
|
+
</Group>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the selection-dot slot inside a crosshair overlay: renders nothing
|
|
46
|
+
* when `config` is null or the position is unavailable, the consumer's custom
|
|
47
|
+
* `component` when one is given, and the built-in dot otherwise.
|
|
48
|
+
*
|
|
49
|
+
* `color` is the resolved fallback (line / leading-series color) used when the
|
|
50
|
+
* config's own `color` is unset.
|
|
51
|
+
*/
|
|
52
|
+
export function SelectionDotSlot({
|
|
53
|
+
config,
|
|
54
|
+
x,
|
|
55
|
+
y,
|
|
56
|
+
active,
|
|
57
|
+
opacity,
|
|
58
|
+
color,
|
|
59
|
+
}: {
|
|
60
|
+
config?: ResolvedSelectionDotConfig | null;
|
|
61
|
+
x: SharedValue<number>;
|
|
62
|
+
y?: SharedValue<number>;
|
|
63
|
+
active?: SharedValue<number> | SharedValue<boolean>;
|
|
64
|
+
opacity: SharedValue<number>;
|
|
65
|
+
color: string;
|
|
66
|
+
}) {
|
|
67
|
+
if (!config || y === undefined || !active) return null;
|
|
68
|
+
const dotColor = config.color ?? color;
|
|
69
|
+
if (config.component) {
|
|
70
|
+
const Custom = config.component;
|
|
71
|
+
return (
|
|
72
|
+
<Custom
|
|
73
|
+
x={x}
|
|
74
|
+
y={y}
|
|
75
|
+
active={active as SharedValue<boolean>}
|
|
76
|
+
opacity={opacity}
|
|
77
|
+
color={dotColor}
|
|
78
|
+
size={config.size}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
<DefaultSelectionDot
|
|
84
|
+
x={x}
|
|
85
|
+
y={y}
|
|
86
|
+
opacity={opacity}
|
|
87
|
+
color={dotColor}
|
|
88
|
+
size={config.size}
|
|
89
|
+
ring={config.ring}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|