react-native-livechart 4.10.0 → 4.11.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 +24 -0
- package/dist/components/CrosshairLine.d.ts +3 -1
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +3 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +4 -2
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/LeftEdgeFade.d.ts +3 -1
- package/dist/components/LeftEdgeFade.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 +3 -1
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +3 -1
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -1
- package/dist/components/MultiSeriesValueLabels.d.ts +3 -1
- package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -1
- package/dist/components/MultiSeriesValueLines.d.ts +3 -1
- package/dist/components/MultiSeriesValueLines.d.ts.map +1 -1
- package/dist/components/ThresholdSplitShader.d.ts.map +1 -1
- package/dist/components/thresholdSplitShaderSource.d.ts +13 -0
- package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -0
- package/dist/core/useLiveChartEngine.d.ts +9 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +10 -5
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/particleAtlas.d.ts +3 -2
- package/dist/draw/particleAtlas.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +4 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +4 -4
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/squiggly.d.ts +6 -3
- package/dist/math/squiggly.d.ts.map +1 -1
- package/dist/math/threshold.d.ts +2 -2
- package/dist/types.d.ts +28 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairLine.tsx +17 -0
- package/src/components/CrosshairOverlay.tsx +48 -35
- package/src/components/CustomReferenceLineOverlay.tsx +16 -7
- package/src/components/DegenParticlesOverlay.tsx +40 -5
- package/src/components/LeftEdgeFade.tsx +12 -6
- package/src/components/LiveChart.tsx +29 -16
- package/src/components/LiveChartSeries.tsx +94 -21
- package/src/components/LoadingOverlay.tsx +26 -12
- package/src/components/MarkerOverlay.tsx +20 -3
- package/src/components/MultiSeriesDots.tsx +4 -2
- package/src/components/MultiSeriesTooltipStack.tsx +4 -2
- package/src/components/MultiSeriesValueLabels.tsx +4 -2
- package/src/components/MultiSeriesValueLines.tsx +4 -2
- package/src/components/ThresholdSplitShader.tsx +5 -37
- package/src/components/thresholdSplitShaderSource.ts +70 -0
- package/src/core/useLiveChartEngine.ts +110 -42
- package/src/core/useLiveChartSeriesEngine.ts +137 -70
- package/src/draw/particleAtlas.ts +21 -10
- package/src/hooks/crosshairShared.ts +5 -0
- package/src/hooks/useChartPaths.ts +11 -0
- package/src/hooks/useCrosshair.ts +16 -10
- package/src/hooks/useCrosshairSeries.ts +11 -6
- package/src/hooks/useMultiSeriesLinePaths.ts +15 -9
- package/src/index.ts +1 -0
- package/src/math/squiggly.ts +28 -11
- package/src/math/threshold.ts +2 -2
- package/src/types.ts +33 -3
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useEffect,
|
|
3
|
-
useLayoutEffect,
|
|
4
|
-
useMemo,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
} from "react";
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
8
2
|
import { StyleSheet, View } from "react-native";
|
|
9
3
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
10
4
|
import Animated, {
|
|
@@ -27,6 +21,7 @@ import {
|
|
|
27
21
|
Group,
|
|
28
22
|
LinearGradient,
|
|
29
23
|
Path,
|
|
24
|
+
Rect,
|
|
30
25
|
vec,
|
|
31
26
|
} from "@shopify/react-native-skia";
|
|
32
27
|
|
|
@@ -253,6 +248,7 @@ function useLiveChartController({
|
|
|
253
248
|
font: fontProp,
|
|
254
249
|
insets,
|
|
255
250
|
style,
|
|
251
|
+
canvasMode = "transparent",
|
|
256
252
|
|
|
257
253
|
// ── Candlestick ─────────────────────────────────────────────────────────
|
|
258
254
|
mode = "line",
|
|
@@ -1204,6 +1200,7 @@ function useLiveChartController({
|
|
|
1204
1200
|
return {
|
|
1205
1201
|
// passthrough props the render needs
|
|
1206
1202
|
style,
|
|
1203
|
+
canvasMode,
|
|
1207
1204
|
accessibilityLabel,
|
|
1208
1205
|
accessibilityRole,
|
|
1209
1206
|
emptyText,
|
|
@@ -1362,9 +1359,7 @@ function ChartWithDegen({
|
|
|
1362
1359
|
onDegenShake,
|
|
1363
1360
|
isStatic,
|
|
1364
1361
|
);
|
|
1365
|
-
return
|
|
1366
|
-
<ChartView model={model} yAxisEntries={yAxisEntries} degen={state} />
|
|
1367
|
-
);
|
|
1362
|
+
return <ChartView model={model} yAxisEntries={yAxisEntries} degen={state} />;
|
|
1368
1363
|
}
|
|
1369
1364
|
|
|
1370
1365
|
/**
|
|
@@ -1721,6 +1716,7 @@ function ChartStack({
|
|
|
1721
1716
|
loadingStrokeWidth,
|
|
1722
1717
|
loadingAmplitude,
|
|
1723
1718
|
loadingSpeed,
|
|
1719
|
+
canvasMode,
|
|
1724
1720
|
} = model;
|
|
1725
1721
|
return (
|
|
1726
1722
|
<Group transform={degen?.shakeTransform}>
|
|
@@ -1939,6 +1935,7 @@ function ChartStack({
|
|
|
1939
1935
|
lineStrokeWidth={loadingStrokeWidth}
|
|
1940
1936
|
waveAmplitude={loadingAmplitude}
|
|
1941
1937
|
waveSpeed={loadingSpeed}
|
|
1938
|
+
opaqueCanvas={canvasMode === "opaque"}
|
|
1942
1939
|
/>
|
|
1943
1940
|
</Group>
|
|
1944
1941
|
);
|
|
@@ -2003,6 +2000,7 @@ function ChartScrubLayer({
|
|
|
2003
2000
|
selectionDot,
|
|
2004
2001
|
selectionColor,
|
|
2005
2002
|
renderTooltip,
|
|
2003
|
+
canvasMode,
|
|
2006
2004
|
} = model;
|
|
2007
2005
|
// A custom tooltip is an RN overlay (sibling of <Canvas>), so the built-in
|
|
2008
2006
|
// Skia tooltip is suppressed here while it's active — the line pill in line
|
|
@@ -2047,6 +2045,7 @@ function ChartScrubLayer({
|
|
|
2047
2045
|
tooltipBorderRadius={scrubCfg.tooltipBorderRadius}
|
|
2048
2046
|
tooltipShowValue={scrubCfg.tooltipShowValue}
|
|
2049
2047
|
tooltipShowTime={scrubCfg.tooltipShowTime}
|
|
2048
|
+
opaqueCanvas={canvasMode === "opaque"}
|
|
2050
2049
|
>
|
|
2051
2050
|
{/* Candle charts render a multi-line OHLC tooltip; the line
|
|
2052
2051
|
chart falls back to CrosshairOverlay's default value/time
|
|
@@ -2364,6 +2363,7 @@ function ChartView({
|
|
|
2364
2363
|
extremaTimeOffset,
|
|
2365
2364
|
topConnector,
|
|
2366
2365
|
bottomConnector,
|
|
2366
|
+
canvasMode,
|
|
2367
2367
|
} = model;
|
|
2368
2368
|
|
|
2369
2369
|
return (
|
|
@@ -2375,7 +2375,21 @@ function ChartView({
|
|
|
2375
2375
|
accessibilityLabel={accessibilityLabel}
|
|
2376
2376
|
accessibilityRole={accessibilityRole}
|
|
2377
2377
|
>
|
|
2378
|
-
|
|
2378
|
+
{/* Skia chooses TextureView vs SurfaceView when the native Canvas mounts. */}
|
|
2379
|
+
<Canvas
|
|
2380
|
+
key={canvasMode}
|
|
2381
|
+
style={{ flex: 1 }}
|
|
2382
|
+
opaque={canvasMode === "opaque"}
|
|
2383
|
+
>
|
|
2384
|
+
{canvasMode === "opaque" && (
|
|
2385
|
+
<Rect
|
|
2386
|
+
x={0}
|
|
2387
|
+
y={0}
|
|
2388
|
+
width={engine.canvasWidth}
|
|
2389
|
+
height={engine.canvasHeight}
|
|
2390
|
+
color={backgroundColor}
|
|
2391
|
+
/>
|
|
2392
|
+
)}
|
|
2379
2393
|
{/* Background fills first, then the left-edge fade (a canvas-space sibling
|
|
2380
2394
|
so dstOut blends correctly), then the line stack on top — so the fade
|
|
2381
2395
|
softens only the fills and the line stays crisp at the left edge. */}
|
|
@@ -2392,15 +2406,14 @@ function ChartView({
|
|
|
2392
2406
|
startColor={leftEdgeFadeCfg.startColor}
|
|
2393
2407
|
endColor={leftEdgeFadeCfg.endColor}
|
|
2394
2408
|
engine={engine}
|
|
2409
|
+
opaqueBackgroundRgb={
|
|
2410
|
+
canvasMode === "opaque" ? palette.bgRgb : undefined
|
|
2411
|
+
}
|
|
2395
2412
|
/>
|
|
2396
2413
|
)}
|
|
2397
2414
|
|
|
2398
2415
|
{/* Line stack above the fade so the line stays crisp at the left edge. */}
|
|
2399
|
-
<ChartStack
|
|
2400
|
-
model={model}
|
|
2401
|
-
yAxisEntries={yAxisEntries}
|
|
2402
|
-
degen={degen}
|
|
2403
|
-
/>
|
|
2416
|
+
<ChartStack model={model} yAxisEntries={yAxisEntries} degen={degen} />
|
|
2404
2417
|
|
|
2405
2418
|
{/* "extrema-edge" connector lines (dot → edge readout), above the chart
|
|
2406
2419
|
content so the dashed guide reads over the line / candles. */}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @see https://github.com/benjitaylor/liveline
|
|
6
6
|
*/
|
|
7
|
-
import { Canvas, Group } from "@shopify/react-native-skia";
|
|
7
|
+
import { Canvas, Group, Rect } from "@shopify/react-native-skia";
|
|
8
8
|
import { useLayoutEffect, useState } from "react";
|
|
9
9
|
import { StyleSheet, View } from "react-native";
|
|
10
10
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
@@ -83,6 +83,10 @@ import {
|
|
|
83
83
|
labelConnector,
|
|
84
84
|
} from "./ExtremaConnectorOverlay";
|
|
85
85
|
import { CustomMarkerOverlay } from "./CustomMarkerOverlay";
|
|
86
|
+
import {
|
|
87
|
+
CustomReferenceLineOverlay,
|
|
88
|
+
customReferenceLineFlags,
|
|
89
|
+
} from "./CustomReferenceLineOverlay";
|
|
86
90
|
import { CrosshairLine } from "./CrosshairLine";
|
|
87
91
|
import { DegenParticlesOverlay } from "./DegenParticlesOverlay";
|
|
88
92
|
import { LeftEdgeFade } from "./LeftEdgeFade";
|
|
@@ -128,6 +132,7 @@ function useLiveChartSeriesController({
|
|
|
128
132
|
font: fontProp,
|
|
129
133
|
insets,
|
|
130
134
|
style,
|
|
135
|
+
canvasMode = "transparent",
|
|
131
136
|
timeWindow = 30,
|
|
132
137
|
paused = false,
|
|
133
138
|
loading = false,
|
|
@@ -172,6 +177,7 @@ function useLiveChartSeriesController({
|
|
|
172
177
|
markerHitRadius = 16,
|
|
173
178
|
markerCluster,
|
|
174
179
|
renderMarker,
|
|
180
|
+
renderReferenceLine,
|
|
175
181
|
leftEdgeFade = true,
|
|
176
182
|
}: LiveChartSeriesProps) {
|
|
177
183
|
const emptyMarkers = useSharedValue<Marker[]>([]);
|
|
@@ -224,6 +230,13 @@ function useLiveChartSeriesController({
|
|
|
224
230
|
|
|
225
231
|
const allRefLines = referenceLines ?? [];
|
|
226
232
|
const refValues = collectReferenceValues(allRefLines);
|
|
233
|
+
// Form-A lines a custom renderer owns keep their line stroke but suppress the
|
|
234
|
+
// built-in Skia tag. The callback is probed per line on the JS thread, matching
|
|
235
|
+
// LiveChart and CustomMarkerOverlay's per-item fallback model.
|
|
236
|
+
const refLineCustom = customReferenceLineFlags(
|
|
237
|
+
allRefLines,
|
|
238
|
+
renderReferenceLine,
|
|
239
|
+
);
|
|
227
240
|
|
|
228
241
|
const palette = applyPaletteOverride(
|
|
229
242
|
resolveTheme(accentColor, theme),
|
|
@@ -256,6 +269,11 @@ function useLiveChartSeriesController({
|
|
|
256
269
|
setSeriesSnapshot(series.get().slice());
|
|
257
270
|
}, [series]);
|
|
258
271
|
|
|
272
|
+
// Mount per-series drawing worklets only for real series. The previous fixed
|
|
273
|
+
// 12-slot render kept 144 derived-value mappers alive for the default stroke,
|
|
274
|
+
// dot, and value-label layers even when a chart contained only one line.
|
|
275
|
+
const activeSeriesCount = Math.min(seriesSnapshot.length, MAX_MULTI_SERIES);
|
|
276
|
+
|
|
259
277
|
const maxSeriesLabelWidth = dotCfg.valueLabel
|
|
260
278
|
? Math.max(
|
|
261
279
|
0,
|
|
@@ -336,7 +354,11 @@ function useLiveChartSeriesController({
|
|
|
336
354
|
nowOverride,
|
|
337
355
|
});
|
|
338
356
|
const { layoutHeight, onLayout } = useCanvasLayout(engine);
|
|
339
|
-
const linePaths = useMultiSeriesLinePaths(
|
|
357
|
+
const linePaths = useMultiSeriesLinePaths(
|
|
358
|
+
engine,
|
|
359
|
+
effectivePadding,
|
|
360
|
+
activeSeriesCount,
|
|
361
|
+
);
|
|
340
362
|
|
|
341
363
|
// Per-series colors and stroke styles, derived from the off-render snapshot
|
|
342
364
|
// (React state — so no Reanimated read-during-render). The reaction below
|
|
@@ -503,6 +525,7 @@ function useLiveChartSeriesController({
|
|
|
503
525
|
// passthrough props the render needs
|
|
504
526
|
series,
|
|
505
527
|
style,
|
|
528
|
+
canvasMode,
|
|
506
529
|
accessibilityLabel,
|
|
507
530
|
accessibilityRole,
|
|
508
531
|
emptyText,
|
|
@@ -519,6 +542,7 @@ function useLiveChartSeriesController({
|
|
|
519
542
|
degenCfg,
|
|
520
543
|
metricsCfg,
|
|
521
544
|
allRefLines,
|
|
545
|
+
refLineCustom,
|
|
522
546
|
leftEdgeFadeCfg,
|
|
523
547
|
// theme / layout / fonts
|
|
524
548
|
palette,
|
|
@@ -540,6 +564,7 @@ function useLiveChartSeriesController({
|
|
|
540
564
|
layoutHeight,
|
|
541
565
|
onLayout,
|
|
542
566
|
linePaths,
|
|
567
|
+
activeSeriesCount,
|
|
543
568
|
lineColors,
|
|
544
569
|
lineStyles,
|
|
545
570
|
degenPack,
|
|
@@ -555,6 +580,7 @@ function useLiveChartSeriesController({
|
|
|
555
580
|
markerGroupOpacity,
|
|
556
581
|
overlayScrubFade,
|
|
557
582
|
renderMarker,
|
|
583
|
+
renderReferenceLine,
|
|
558
584
|
// selection dot: resolved config + fallback color (the leading series' color)
|
|
559
585
|
selectionDot: selectionDotCfg,
|
|
560
586
|
selectionColor: lineColors[0],
|
|
@@ -610,6 +636,8 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
610
636
|
loadingStrokeWidth,
|
|
611
637
|
loadingAmplitude,
|
|
612
638
|
loadingSpeed,
|
|
639
|
+
canvasMode,
|
|
640
|
+
activeSeriesCount,
|
|
613
641
|
} = model;
|
|
614
642
|
|
|
615
643
|
return (
|
|
@@ -629,12 +657,13 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
629
657
|
</Group>
|
|
630
658
|
)}
|
|
631
659
|
|
|
632
|
-
{/*
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
`scrub.hideOverlaysOnScrub` ease the lines out
|
|
660
|
+
{/* Reference lines are index-addressed throughout the drag/press API, and
|
|
661
|
+
duplicate working orders may share all visible content. Positional keys
|
|
662
|
+
keep both orders mounted without collapsing them to one React child.
|
|
663
|
+
Fade group lets `scrub.hideOverlaysOnScrub` ease the lines out. */}
|
|
636
664
|
<Group opacity={overlayScrubFade}>
|
|
637
665
|
{allRefLines.map((rl, i) => (
|
|
666
|
+
/* react-doctor-disable-next-line react-doctor/no-array-index-as-key */
|
|
638
667
|
<ReferenceLineOverlay
|
|
639
668
|
key={i}
|
|
640
669
|
engine={engine}
|
|
@@ -654,12 +683,13 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
654
683
|
padding={effectivePadding}
|
|
655
684
|
colors={lineColors}
|
|
656
685
|
config={dotCfg.valueLine}
|
|
686
|
+
seriesCount={activeSeriesCount}
|
|
657
687
|
/>
|
|
658
688
|
</Group>
|
|
659
689
|
)}
|
|
660
690
|
|
|
661
691
|
<Group opacity={reveal.lineOpacity}>
|
|
662
|
-
{Array.from({ length:
|
|
692
|
+
{Array.from({ length: activeSeriesCount }, (_, i) => (
|
|
663
693
|
<MultiSeriesStroke
|
|
664
694
|
key={i}
|
|
665
695
|
index={i}
|
|
@@ -694,6 +724,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
694
724
|
color={dotCfg.color}
|
|
695
725
|
pulse={dotCfg.pulse}
|
|
696
726
|
viewEnd={engine.viewEnd}
|
|
727
|
+
seriesCount={activeSeriesCount}
|
|
697
728
|
/>
|
|
698
729
|
</Group>
|
|
699
730
|
)}
|
|
@@ -749,6 +780,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
749
780
|
lineStrokeWidth={loadingStrokeWidth}
|
|
750
781
|
waveAmplitude={loadingAmplitude}
|
|
751
782
|
waveSpeed={loadingSpeed}
|
|
783
|
+
opaqueCanvas={canvasMode === "opaque"}
|
|
752
784
|
/>
|
|
753
785
|
</Group>
|
|
754
786
|
);
|
|
@@ -767,6 +799,7 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
767
799
|
skiaFont,
|
|
768
800
|
reveal,
|
|
769
801
|
degenShakeTransform,
|
|
802
|
+
activeSeriesCount,
|
|
770
803
|
} = model;
|
|
771
804
|
if (!dotCfg.valueLabel) return null;
|
|
772
805
|
return (
|
|
@@ -778,6 +811,7 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
778
811
|
colors={lineColors}
|
|
779
812
|
font={skiaFont}
|
|
780
813
|
dotRadius={dotOuterRadius}
|
|
814
|
+
seriesCount={activeSeriesCount}
|
|
781
815
|
/>
|
|
782
816
|
</Group>
|
|
783
817
|
</Group>
|
|
@@ -789,6 +823,7 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
789
823
|
function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
790
824
|
const {
|
|
791
825
|
allRefLines,
|
|
826
|
+
refLineCustom,
|
|
792
827
|
engine,
|
|
793
828
|
effectivePadding,
|
|
794
829
|
palette,
|
|
@@ -796,11 +831,13 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
796
831
|
skiaFont,
|
|
797
832
|
degenShakeTransform,
|
|
798
833
|
overlayScrubFade,
|
|
834
|
+
canvasMode,
|
|
799
835
|
} = model;
|
|
800
836
|
if (allRefLines.length === 0) return null;
|
|
801
837
|
return (
|
|
802
838
|
<Group transform={degenShakeTransform} opacity={overlayScrubFade}>
|
|
803
839
|
{allRefLines.map((rl, i) => (
|
|
840
|
+
/* react-doctor-disable-next-line react-doctor/no-array-index-as-key */
|
|
804
841
|
<ReferenceLineOverlay
|
|
805
842
|
key={i}
|
|
806
843
|
engine={engine}
|
|
@@ -810,6 +847,7 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
810
847
|
formatValue={formatValue}
|
|
811
848
|
font={skiaFont}
|
|
812
849
|
badgeLayer
|
|
850
|
+
suppressTag={refLineCustom[i]}
|
|
813
851
|
/>
|
|
814
852
|
))}
|
|
815
853
|
</Group>
|
|
@@ -848,7 +886,11 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
848
886
|
markersSV,
|
|
849
887
|
markerClusterCfg,
|
|
850
888
|
renderMarker,
|
|
889
|
+
renderReferenceLine,
|
|
890
|
+
allRefLines,
|
|
891
|
+
refLineCustom,
|
|
851
892
|
overlayScrubFade,
|
|
893
|
+
canvasMode,
|
|
852
894
|
} = model;
|
|
853
895
|
|
|
854
896
|
// Mirror the Skia overlay fade onto the RN custom-marker sibling so
|
|
@@ -891,7 +933,21 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
891
933
|
else points map into a taller area and the x-axis draws past the edge. */}
|
|
892
934
|
<GestureDetector gesture={rootGesture}>
|
|
893
935
|
<View style={{ flex: 1 }} onLayout={onLayout}>
|
|
894
|
-
|
|
936
|
+
{/* Skia chooses TextureView vs SurfaceView when the native Canvas mounts. */}
|
|
937
|
+
<Canvas
|
|
938
|
+
key={canvasMode}
|
|
939
|
+
style={{ flex: 1, minHeight: layoutHeight || 1 }}
|
|
940
|
+
opaque={canvasMode === "opaque"}
|
|
941
|
+
>
|
|
942
|
+
{canvasMode === "opaque" && (
|
|
943
|
+
<Rect
|
|
944
|
+
x={0}
|
|
945
|
+
y={0}
|
|
946
|
+
width={engine.canvasWidth}
|
|
947
|
+
height={engine.canvasHeight}
|
|
948
|
+
color={backgroundColor}
|
|
949
|
+
/>
|
|
950
|
+
)}
|
|
895
951
|
<SeriesChartStack model={model} />
|
|
896
952
|
|
|
897
953
|
{/* "extrema-edge" connector lines (dot → edge readout). Outside the
|
|
@@ -910,6 +966,9 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
910
966
|
startColor={leftEdgeFadeCfg.startColor}
|
|
911
967
|
endColor={leftEdgeFadeCfg.endColor}
|
|
912
968
|
engine={engine}
|
|
969
|
+
opaqueBackgroundRgb={
|
|
970
|
+
canvasMode === "opaque" ? palette.bgRgb : undefined
|
|
971
|
+
}
|
|
913
972
|
/>
|
|
914
973
|
)}
|
|
915
974
|
|
|
@@ -932,6 +991,7 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
932
991
|
crosshairLineColor={scrubCfg.crosshairLineColor}
|
|
933
992
|
crosshairDash={scrubCfg.crosshairDash}
|
|
934
993
|
crosshairDimColor={scrubCfg.crosshairDimColor}
|
|
994
|
+
opaqueCanvas={canvasMode === "opaque"}
|
|
935
995
|
/>
|
|
936
996
|
)}
|
|
937
997
|
|
|
@@ -952,23 +1012,36 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
952
1012
|
padding={effectivePadding}
|
|
953
1013
|
/>
|
|
954
1014
|
|
|
955
|
-
{/* Custom
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
{markersActive && renderMarker
|
|
1015
|
+
{/* Custom annotations — RN views floated over the canvas (non-Skia),
|
|
1016
|
+
pinned to their live positions. As a post-Canvas sibling they render
|
|
1017
|
+
above the left-edge fade and scrub overlay. The box-none fade wrapper
|
|
1018
|
+
keeps `scrub.hideOverlaysOnScrub` behavior aligned with Skia. */}
|
|
1019
|
+
{((markersActive && renderMarker) ||
|
|
1020
|
+
(renderReferenceLine && allRefLines.length > 0)) && (
|
|
960
1021
|
<Animated.View
|
|
961
1022
|
pointerEvents="box-none"
|
|
962
1023
|
style={[StyleSheet.absoluteFill, overlayFadeStyle]}
|
|
963
1024
|
>
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1025
|
+
{markersActive && renderMarker && (
|
|
1026
|
+
<CustomMarkerOverlay
|
|
1027
|
+
markers={markersSV}
|
|
1028
|
+
renderMarker={renderMarker}
|
|
1029
|
+
engine={engine}
|
|
1030
|
+
padding={effectivePadding}
|
|
1031
|
+
series={series}
|
|
1032
|
+
cluster={markerClusterCfg}
|
|
1033
|
+
/>
|
|
1034
|
+
)}
|
|
1035
|
+
{renderReferenceLine && allRefLines.length > 0 && (
|
|
1036
|
+
<CustomReferenceLineOverlay
|
|
1037
|
+
lines={allRefLines}
|
|
1038
|
+
renderReferenceLine={renderReferenceLine}
|
|
1039
|
+
custom={refLineCustom}
|
|
1040
|
+
engine={engine}
|
|
1041
|
+
padding={effectivePadding}
|
|
1042
|
+
formatValue={formatValue}
|
|
1043
|
+
/>
|
|
1044
|
+
)}
|
|
972
1045
|
</Animated.View>
|
|
973
1046
|
)}
|
|
974
1047
|
</View>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
vec,
|
|
9
9
|
type SkFont,
|
|
10
10
|
} from "@shopify/react-native-skia";
|
|
11
|
+
import { useRef } from "react";
|
|
11
12
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
12
13
|
import {
|
|
13
14
|
BADGE_METRICS_DEFAULTS,
|
|
@@ -67,6 +68,7 @@ export function LoadingOverlay({
|
|
|
67
68
|
lineStrokeWidth,
|
|
68
69
|
waveAmplitude = 14,
|
|
69
70
|
waveSpeed = 1,
|
|
71
|
+
opaqueCanvas = false,
|
|
70
72
|
}: {
|
|
71
73
|
engine: ChartEngineLayout;
|
|
72
74
|
padding: ChartPadding;
|
|
@@ -86,6 +88,8 @@ export function LoadingOverlay({
|
|
|
86
88
|
waveAmplitude?: number;
|
|
87
89
|
/** Breathing-wave speed multiplier. */
|
|
88
90
|
waveSpeed?: number;
|
|
91
|
+
/** Paint the owned background instead of erasing destination alpha. */
|
|
92
|
+
opaqueCanvas?: boolean;
|
|
89
93
|
/** Mirror the badge prop so labels align with GridOverlay's label positions. */
|
|
90
94
|
badge?: boolean;
|
|
91
95
|
/** Whether the badge tail spike is shown; affects the left inset used for skeleton alignment. */
|
|
@@ -99,15 +103,26 @@ export function LoadingOverlay({
|
|
|
99
103
|
}) {
|
|
100
104
|
// Same left-inset formula as GridOverlay (only used when badge=true)
|
|
101
105
|
const leftInset =
|
|
102
|
-
badgeMetrics.dotGap +
|
|
106
|
+
badgeMetrics.dotGap +
|
|
107
|
+
badgeTailAndCap(font.getSize(), badgeTail, badgeMetrics);
|
|
103
108
|
|
|
104
109
|
// Loading-shell color (squiggle + skeleton placeholders) and squiggle stroke,
|
|
105
110
|
// both overridable via `loading={{ color, strokeWidth }}`.
|
|
106
111
|
const loadingColor = lineColor ?? palette.gridLine;
|
|
107
112
|
const loadingStroke = lineStrokeWidth ?? strokeWidth;
|
|
113
|
+
const [bgR, bgG, bgB] = palette.bgRgb;
|
|
114
|
+
const gapMaskColors = opaqueCanvas
|
|
115
|
+
? [
|
|
116
|
+
`rgba(${bgR},${bgG},${bgB},0)`,
|
|
117
|
+
`rgb(${bgR},${bgG},${bgB})`,
|
|
118
|
+
`rgb(${bgR},${bgG},${bgB})`,
|
|
119
|
+
`rgba(${bgR},${bgG},${bgB},0)`,
|
|
120
|
+
]
|
|
121
|
+
: ["rgba(0,0,0,0)", "rgba(0,0,0,1)", "rgba(0,0,0,1)", "rgba(0,0,0,0)"];
|
|
108
122
|
|
|
109
123
|
// Squiggly path — built into a reused PathBuilder and detach()-ed each frame.
|
|
110
124
|
const squigglyBuilder = usePathBuilder();
|
|
125
|
+
const squigglyPtsRef = useRef<number[]>([]);
|
|
111
126
|
|
|
112
127
|
// Squiggly path — animated each frame via timestamp
|
|
113
128
|
const squigglyPath = useDerivedValue(() => {
|
|
@@ -122,6 +137,7 @@ export function LoadingOverlay({
|
|
|
122
137
|
engine.timestamp.get(),
|
|
123
138
|
waveAmplitude,
|
|
124
139
|
waveSpeed,
|
|
140
|
+
squigglyPtsRef.current,
|
|
125
141
|
);
|
|
126
142
|
return buildSplineDetached(b, pts);
|
|
127
143
|
});
|
|
@@ -254,17 +270,15 @@ export function LoadingOverlay({
|
|
|
254
270
|
/>
|
|
255
271
|
|
|
256
272
|
{/* Erase a soft horizontal band through the squiggle for the label (dstOut) */}
|
|
257
|
-
<Group
|
|
273
|
+
<Group
|
|
274
|
+
opacity={showGapGroup}
|
|
275
|
+
blendMode={opaqueCanvas ? undefined : "dstOut"}
|
|
276
|
+
>
|
|
258
277
|
<Rect x={gapLeft} y={gapTop} width={gapWidth} height={gapHeight}>
|
|
259
278
|
<LinearGradient
|
|
260
279
|
start={vec(0, 0)}
|
|
261
280
|
end={gapGradEnd}
|
|
262
|
-
colors={
|
|
263
|
-
"rgba(0,0,0,0)",
|
|
264
|
-
"rgba(0,0,0,1)",
|
|
265
|
-
"rgba(0,0,0,1)",
|
|
266
|
-
"rgba(0,0,0,0)",
|
|
267
|
-
]}
|
|
281
|
+
colors={gapMaskColors}
|
|
268
282
|
positions={gapGradientPositions}
|
|
269
283
|
/>
|
|
270
284
|
</Rect>
|
|
@@ -279,7 +293,7 @@ export function LoadingOverlay({
|
|
|
279
293
|
width={RECT_W}
|
|
280
294
|
height={RECT_H}
|
|
281
295
|
r={RECT_R}
|
|
282
|
-
|
|
296
|
+
color={loadingColor}
|
|
283
297
|
/>
|
|
284
298
|
<RoundedRect
|
|
285
299
|
x={lx}
|
|
@@ -287,7 +301,7 @@ export function LoadingOverlay({
|
|
|
287
301
|
width={RECT_W}
|
|
288
302
|
height={RECT_H}
|
|
289
303
|
r={RECT_R}
|
|
290
|
-
|
|
304
|
+
color={loadingColor}
|
|
291
305
|
/>
|
|
292
306
|
<RoundedRect
|
|
293
307
|
x={lx}
|
|
@@ -295,7 +309,7 @@ export function LoadingOverlay({
|
|
|
295
309
|
width={RECT_W}
|
|
296
310
|
height={RECT_H}
|
|
297
311
|
r={RECT_R}
|
|
298
|
-
|
|
312
|
+
color={loadingColor}
|
|
299
313
|
/>
|
|
300
314
|
<RoundedRect
|
|
301
315
|
x={lx}
|
|
@@ -303,7 +317,7 @@ export function LoadingOverlay({
|
|
|
303
317
|
width={RECT_W}
|
|
304
318
|
height={RECT_H}
|
|
305
319
|
r={RECT_R}
|
|
306
|
-
|
|
320
|
+
color={loadingColor}
|
|
307
321
|
/>
|
|
308
322
|
</>
|
|
309
323
|
) : null}
|
|
@@ -351,6 +351,18 @@ export function MarkerOverlay({
|
|
|
351
351
|
if (projRef.current === null) {
|
|
352
352
|
projRef.current = { a: [], b: [], tick: false };
|
|
353
353
|
}
|
|
354
|
+
const atlasFrameRef = useRef<{
|
|
355
|
+
a: { transforms: SkRSXform[]; sprites: SkRect[] };
|
|
356
|
+
b: { transforms: SkRSXform[]; sprites: SkRect[] };
|
|
357
|
+
tick: boolean;
|
|
358
|
+
} | null>(null);
|
|
359
|
+
if (atlasFrameRef.current === null) {
|
|
360
|
+
atlasFrameRef.current = {
|
|
361
|
+
a: { transforms: [], sprites: [] },
|
|
362
|
+
b: { transforms: [], sprites: [] },
|
|
363
|
+
tick: false,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
354
366
|
|
|
355
367
|
// Single per-frame worklet: project all markers, then emit a transform +
|
|
356
368
|
// source rect for each visible atlas marker. Three mappers total regardless
|
|
@@ -377,8 +389,13 @@ export function MarkerOverlay({
|
|
|
377
389
|
lineLinear,
|
|
378
390
|
});
|
|
379
391
|
clusterMarkers(ms, buf, { config: cluster });
|
|
380
|
-
const
|
|
381
|
-
|
|
392
|
+
const atlasFrames = atlasFrameRef.current!;
|
|
393
|
+
atlasFrames.tick = !atlasFrames.tick;
|
|
394
|
+
const frame = atlasFrames.tick ? atlasFrames.a : atlasFrames.b;
|
|
395
|
+
const transforms = frame.transforms;
|
|
396
|
+
const sprites = frame.sprites;
|
|
397
|
+
transforms.length = 0;
|
|
398
|
+
sprites.length = 0;
|
|
382
399
|
for (let i = 0; i < ms.length; i++) {
|
|
383
400
|
const pt = buf[i];
|
|
384
401
|
// Collapsed-cluster members fold into their representative's badge.
|
|
@@ -455,7 +472,7 @@ export function MarkerOverlay({
|
|
|
455
472
|
);
|
|
456
473
|
sprites.push(cell.rect);
|
|
457
474
|
}
|
|
458
|
-
return
|
|
475
|
+
return frame;
|
|
459
476
|
},
|
|
460
477
|
[
|
|
461
478
|
cells,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Circle, Group } from "@shopify/react-native-skia";
|
|
2
2
|
|
|
3
3
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
4
|
-
import { MAX_MULTI_SERIES } from "../constants";
|
|
5
4
|
import type { ChartPadding } from "../draw/line";
|
|
6
5
|
import type {
|
|
7
6
|
ResolvedDotRingConfig,
|
|
@@ -118,6 +117,7 @@ export function MultiSeriesDots({
|
|
|
118
117
|
color,
|
|
119
118
|
pulse,
|
|
120
119
|
viewEnd,
|
|
120
|
+
seriesCount,
|
|
121
121
|
}: {
|
|
122
122
|
engine: MultiEngineState;
|
|
123
123
|
padding: ChartPadding;
|
|
@@ -132,10 +132,12 @@ export function MultiSeriesDots({
|
|
|
132
132
|
pulse: ResolvedPulseConfig | null;
|
|
133
133
|
/** Pan/zoom right-edge override — pulse is frozen-out while scrolled back. */
|
|
134
134
|
viewEnd?: SharedValue<number | null>;
|
|
135
|
+
/** Number of live series slots to mount. */
|
|
136
|
+
seriesCount: number;
|
|
135
137
|
}) {
|
|
136
138
|
return (
|
|
137
139
|
<Group>
|
|
138
|
-
{Array.from({ length:
|
|
140
|
+
{Array.from({ length: seriesCount }, (_, i) => (
|
|
139
141
|
<SeriesDotAtIndex
|
|
140
142
|
key={i}
|
|
141
143
|
index={i}
|
|
@@ -4,11 +4,13 @@ import {
|
|
|
4
4
|
type SkFont,
|
|
5
5
|
} from "@shopify/react-native-skia";
|
|
6
6
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
7
|
-
import { MAX_MULTI_SERIES } from "../constants";
|
|
8
7
|
import type { TooltipLayout } from "../hooks/crosshairShared";
|
|
9
8
|
import type { LiveChartPalette } from "../types";
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
// Candle tooltips have exactly O/H/L/C + time. This component is only mounted
|
|
11
|
+
// for candle mode, so reserving the multi-series capacity registered 40 unused
|
|
12
|
+
// derived-value mappers (8 empty rows × 5 mappers).
|
|
13
|
+
const TOOLTIP_STACK_SLOTS = 5;
|
|
12
14
|
|
|
13
15
|
function TooltipStackLine({
|
|
14
16
|
index,
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
} from "@shopify/react-native-skia";
|
|
6
6
|
import { useDerivedValue } from "react-native-reanimated";
|
|
7
7
|
|
|
8
|
-
import { MAX_MULTI_SERIES } from "../constants";
|
|
9
8
|
import type { ChartPadding } from "../draw/line";
|
|
10
9
|
import type { MultiEngineState } from "../core/useLiveChartEngine";
|
|
11
10
|
|
|
@@ -78,16 +77,19 @@ export function MultiSeriesValueLabels({
|
|
|
78
77
|
colors,
|
|
79
78
|
font,
|
|
80
79
|
dotRadius,
|
|
80
|
+
seriesCount,
|
|
81
81
|
}: {
|
|
82
82
|
engine: MultiEngineState;
|
|
83
83
|
padding: ChartPadding;
|
|
84
84
|
colors: string[];
|
|
85
85
|
font: SkFont;
|
|
86
86
|
dotRadius: number;
|
|
87
|
+
/** Number of live series slots to mount. */
|
|
88
|
+
seriesCount: number;
|
|
87
89
|
}) {
|
|
88
90
|
return (
|
|
89
91
|
<Group>
|
|
90
|
-
{Array.from({ length:
|
|
92
|
+
{Array.from({ length: seriesCount }, (_, i) => (
|
|
91
93
|
<SeriesValueLabelAtIndex
|
|
92
94
|
key={i}
|
|
93
95
|
index={i}
|