react-native-livechart 4.10.0 → 4.12.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 +5 -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 +19 -12
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +3 -5
- package/dist/components/DotOverlay.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/PerSeriesTooltipOverlay.d.ts +19 -0
- package/dist/components/PerSeriesTooltipOverlay.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +20 -6
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ThresholdSplitShader.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +3 -1
- package/dist/components/YAxisOverlay.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/liveIndicatorVisibility.d.ts +9 -0
- package/dist/core/liveIndicatorVisibility.d.ts.map +1 -0
- package/dist/core/resolveConfig.d.ts +29 -0
- package/dist/core/resolveConfig.d.ts.map +1 -1
- 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/crosshairSeries.d.ts +10 -2
- package/dist/hooks/crosshairSeries.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +37 -2
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/delayedPanGuard.d.ts +66 -8
- package/dist/hooks/delayedPanGuard.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +5 -9
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +7 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts +22 -3
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useMomentum.d.ts +1 -1
- package/dist/hooks/useMomentum.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +4 -4
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +15 -1
- package/dist/hooks/usePanScroll.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/lerp.d.ts.map +1 -1
- package/dist/math/momentum.d.ts +1 -1
- package/dist/math/momentum.d.ts.map +1 -1
- package/dist/math/referenceLines.d.ts +7 -0
- package/dist/math/referenceLines.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 +184 -16
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairLine.tsx +21 -1
- package/src/components/CrosshairOverlay.tsx +48 -35
- package/src/components/CustomReferenceLineOverlay.tsx +58 -20
- package/src/components/DegenParticlesOverlay.tsx +40 -5
- package/src/components/DotOverlay.tsx +24 -8
- package/src/components/LeftEdgeFade.tsx +12 -6
- package/src/components/LiveChart.tsx +186 -53
- package/src/components/LiveChartSeries.tsx +214 -29
- 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/PerSeriesTooltipOverlay.tsx +287 -0
- package/src/components/ReferenceLineOverlay.tsx +274 -141
- package/src/components/ThresholdSplitShader.tsx +5 -37
- package/src/components/YAxisOverlay.tsx +5 -1
- package/src/components/thresholdSplitShaderSource.ts +70 -0
- package/src/core/liveIndicatorVisibility.ts +25 -0
- package/src/core/resolveConfig.ts +86 -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/crosshairSeries.ts +262 -33
- package/src/hooks/crosshairShared.ts +41 -2
- package/src/hooks/delayedPanGuard.ts +111 -6
- package/src/hooks/useChartPaths.ts +36 -15
- package/src/hooks/useCrosshair.ts +73 -14
- package/src/hooks/useCrosshairSeries.ts +106 -13
- package/src/hooks/useMomentum.ts +8 -1
- package/src/hooks/useMultiSeriesLinePaths.ts +15 -9
- package/src/hooks/usePanScroll.ts +46 -5
- package/src/index.ts +2 -0
- package/src/math/lerp.ts +7 -0
- package/src/math/momentum.ts +22 -5
- package/src/math/referenceLines.ts +38 -0
- package/src/math/squiggly.ts +28 -11
- package/src/math/threshold.ts +2 -2
- package/src/types.ts +197 -17
|
@@ -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";
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
lineStyleSignatureFromArray,
|
|
29
29
|
resolveMultiSeriesLineColorsSnapshot,
|
|
30
30
|
resolveMultiSeriesLineStylesSnapshot,
|
|
31
|
-
type SeriesLineStyle,
|
|
32
31
|
} from "../core/multiSeriesLayout";
|
|
33
32
|
import {
|
|
34
33
|
resolveAxisLabel,
|
|
@@ -53,6 +52,7 @@ import { pulseRadialOutset } from "../draw/line";
|
|
|
53
52
|
import { resolveChartLayout } from "../hooks/resolveChartLayout";
|
|
54
53
|
import { useCanvasLayout } from "../hooks/useCanvasLayout";
|
|
55
54
|
import { useChartReveal } from "../hooks/useChartReveal";
|
|
55
|
+
import { useChartOverlayContext } from "../hooks/useChartOverlayContext";
|
|
56
56
|
import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
|
|
57
57
|
import { useCrosshairSeries } from "../hooks/useCrosshairSeries";
|
|
58
58
|
import { useMarkers } from "../hooks/useMarkers";
|
|
@@ -70,7 +70,10 @@ import {
|
|
|
70
70
|
} from "../lib/format";
|
|
71
71
|
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
72
72
|
import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
|
|
73
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
collectReferenceValues,
|
|
75
|
+
referenceLineReactKeys,
|
|
76
|
+
} from "../math/referenceLines";
|
|
74
77
|
import {
|
|
75
78
|
applyPaletteOverride,
|
|
76
79
|
leftEdgeFadeColorsFromBgRgb,
|
|
@@ -78,11 +81,16 @@ import {
|
|
|
78
81
|
} from "../theme";
|
|
79
82
|
import type { LiveChartSeriesProps, Marker, SeriesConfig } from "../types";
|
|
80
83
|
import { AxisLabelOverlay } from "./AxisLabelOverlay";
|
|
84
|
+
import { ChartOverlayLayer } from "./ChartOverlayLayer";
|
|
81
85
|
import {
|
|
82
86
|
ExtremaConnectorOverlay,
|
|
83
87
|
labelConnector,
|
|
84
88
|
} from "./ExtremaConnectorOverlay";
|
|
85
89
|
import { CustomMarkerOverlay } from "./CustomMarkerOverlay";
|
|
90
|
+
import {
|
|
91
|
+
CustomReferenceLineOverlay,
|
|
92
|
+
customReferenceLineFlags,
|
|
93
|
+
} from "./CustomReferenceLineOverlay";
|
|
86
94
|
import { CrosshairLine } from "./CrosshairLine";
|
|
87
95
|
import { DegenParticlesOverlay } from "./DegenParticlesOverlay";
|
|
88
96
|
import { LeftEdgeFade } from "./LeftEdgeFade";
|
|
@@ -92,6 +100,7 @@ import { MultiSeriesDots } from "./MultiSeriesDots";
|
|
|
92
100
|
import { MultiSeriesStroke } from "./MultiSeriesStroke";
|
|
93
101
|
import { MultiSeriesValueLabels } from "./MultiSeriesValueLabels";
|
|
94
102
|
import { MultiSeriesValueLines } from "./MultiSeriesValueLines";
|
|
103
|
+
import { PerSeriesTooltipOverlay } from "./PerSeriesTooltipOverlay";
|
|
95
104
|
import { ReferenceLineOverlay } from "./ReferenceLineOverlay";
|
|
96
105
|
import { SeriesToggleChips } from "./SeriesToggleChips";
|
|
97
106
|
import { XAxisOverlay } from "./XAxisOverlay";
|
|
@@ -128,6 +137,7 @@ function useLiveChartSeriesController({
|
|
|
128
137
|
font: fontProp,
|
|
129
138
|
insets,
|
|
130
139
|
style,
|
|
140
|
+
canvasMode = "transparent",
|
|
131
141
|
timeWindow = 30,
|
|
132
142
|
paused = false,
|
|
133
143
|
loading = false,
|
|
@@ -172,6 +182,9 @@ function useLiveChartSeriesController({
|
|
|
172
182
|
markerHitRadius = 16,
|
|
173
183
|
markerCluster,
|
|
174
184
|
renderMarker,
|
|
185
|
+
renderOverlay,
|
|
186
|
+
renderReferenceLine,
|
|
187
|
+
renderOffAxisReferenceLine,
|
|
175
188
|
leftEdgeFade = true,
|
|
176
189
|
}: LiveChartSeriesProps) {
|
|
177
190
|
const emptyMarkers = useSharedValue<Marker[]>([]);
|
|
@@ -184,6 +197,10 @@ function useLiveChartSeriesController({
|
|
|
184
197
|
const bottomLabelCfg = resolveAxisLabel(bottomLabel);
|
|
185
198
|
const scrubCfg = resolveScrub(scrub);
|
|
186
199
|
const scrubEnabled = scrubCfg !== null;
|
|
200
|
+
// Opt-in and subordinate to the existing master `tooltip` switch. Keeping
|
|
201
|
+
// this null by default preserves LiveChartSeries' historical guide-only scrub.
|
|
202
|
+
const seriesTooltipCfg =
|
|
203
|
+
scrubCfg?.tooltip === true ? scrubCfg.seriesTooltip : null;
|
|
187
204
|
|
|
188
205
|
// Time-scroll + pinch-zoom (mirrors LiveChart). LiveChartSeries has no static
|
|
189
206
|
// mode, so these gate on the props alone. `holdToScrub` requires the scrub
|
|
@@ -210,8 +227,8 @@ function useLiveChartSeriesController({
|
|
|
210
227
|
|
|
211
228
|
// Multi-series defaults the scrub selection dot OFF: it can only track one
|
|
212
229
|
// line (the leading series), which reads as a bug next to the other series.
|
|
213
|
-
// The crosshair line
|
|
214
|
-
// Passing `selectionDot` explicitly
|
|
230
|
+
// The crosshair line (and the opt-in tooltip's per-series intersection dots)
|
|
231
|
+
// mark the scrub point. Passing `selectionDot` explicitly still opts it in.
|
|
215
232
|
const selectionDotCfg = resolveSelectionDot(selectionDot ?? false);
|
|
216
233
|
const gridStyleCfg = resolveGridStyle(gridStyle);
|
|
217
234
|
const dotCfg = resolveMultiSeriesDot(dotProp);
|
|
@@ -224,6 +241,24 @@ function useLiveChartSeriesController({
|
|
|
224
241
|
|
|
225
242
|
const allRefLines = referenceLines ?? [];
|
|
226
243
|
const refValues = collectReferenceValues(allRefLines);
|
|
244
|
+
// Form-A lines a custom renderer owns keep their line stroke but suppress the
|
|
245
|
+
// built-in Skia tag. The callback is probed per line on the JS thread, matching
|
|
246
|
+
// LiveChart and CustomMarkerOverlay's per-item fallback model.
|
|
247
|
+
const refLineCustom = customReferenceLineFlags(
|
|
248
|
+
allRefLines,
|
|
249
|
+
renderReferenceLine,
|
|
250
|
+
);
|
|
251
|
+
// A full custom tag owns both states, so it takes precedence over an
|
|
252
|
+
// off-axis-only renderer for the same line.
|
|
253
|
+
const refLineOffAxisCustom = customReferenceLineFlags(
|
|
254
|
+
allRefLines,
|
|
255
|
+
renderOffAxisReferenceLine,
|
|
256
|
+
"off-axis",
|
|
257
|
+
).map((custom, index) => custom && !refLineCustom[index]);
|
|
258
|
+
const refLineKeys = referenceLineReactKeys(allRefLines);
|
|
259
|
+
// RN custom tags report their measured widths here so the Skia connector can
|
|
260
|
+
// start after the native badge instead of the hidden built-in pill.
|
|
261
|
+
const refLineCustomTagWidths = useSharedValue<number[]>([]);
|
|
227
262
|
|
|
228
263
|
const palette = applyPaletteOverride(
|
|
229
264
|
resolveTheme(accentColor, theme),
|
|
@@ -253,9 +288,15 @@ function useLiveChartSeriesController({
|
|
|
253
288
|
// render scope for memoization, which trips Reanimated's strict-mode warning;
|
|
254
289
|
// it leaves the `.get()` method call inside the effect.
|
|
255
290
|
// react-doctor-disable-next-line react-hooks-js/set-state-in-effect -- Reanimated: seeding from a SharedValue off render is the warning-free path
|
|
291
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- Reanimated: seed from the SharedValue outside render to avoid strict-mode access warnings
|
|
256
292
|
setSeriesSnapshot(series.get().slice());
|
|
257
293
|
}, [series]);
|
|
258
294
|
|
|
295
|
+
// Mount per-series drawing worklets only for real series. The previous fixed
|
|
296
|
+
// 12-slot render kept 144 derived-value mappers alive for the default stroke,
|
|
297
|
+
// dot, and value-label layers even when a chart contained only one line.
|
|
298
|
+
const activeSeriesCount = Math.min(seriesSnapshot.length, MAX_MULTI_SERIES);
|
|
299
|
+
|
|
259
300
|
const maxSeriesLabelWidth = dotCfg.valueLabel
|
|
260
301
|
? Math.max(
|
|
261
302
|
0,
|
|
@@ -336,7 +377,11 @@ function useLiveChartSeriesController({
|
|
|
336
377
|
nowOverride,
|
|
337
378
|
});
|
|
338
379
|
const { layoutHeight, onLayout } = useCanvasLayout(engine);
|
|
339
|
-
const linePaths = useMultiSeriesLinePaths(
|
|
380
|
+
const linePaths = useMultiSeriesLinePaths(
|
|
381
|
+
engine,
|
|
382
|
+
effectivePadding,
|
|
383
|
+
activeSeriesCount,
|
|
384
|
+
);
|
|
340
385
|
|
|
341
386
|
// Per-series colors and stroke styles, derived from the off-render snapshot
|
|
342
387
|
// (React state — so no Reanimated read-during-render). The reaction below
|
|
@@ -385,6 +430,20 @@ function useLiveChartSeriesController({
|
|
|
385
430
|
skiaFont,
|
|
386
431
|
);
|
|
387
432
|
|
|
433
|
+
// Cross-gesture arbitration for the one-finger touch. `Gesture.Race` below is
|
|
434
|
+
// NOT arbitration — RNGH's Race adds no relation between its children, so both
|
|
435
|
+
// pans recognize independently and each can activate while the other already
|
|
436
|
+
// owns the touch. This latch (written by the scroll pan, read by the scrub's
|
|
437
|
+
// long-press guard) makes "the scroll already won" a hard fact; `scrubActive`
|
|
438
|
+
// (written by the crosshair, read by the scroll pan) is the mirror image.
|
|
439
|
+
const scrollActive = useSharedValue(false);
|
|
440
|
+
// `liveEdge` includes the optional right breathing-room buffer. The time pill
|
|
441
|
+
// must clamp against the real "now" anchor so its live bucket never ends in
|
|
442
|
+
// that future buffer.
|
|
443
|
+
const tooltipMaxTime = useDerivedValue(
|
|
444
|
+
() => engine.liveEdge.get() - windowBuffer * timeWindow,
|
|
445
|
+
);
|
|
446
|
+
|
|
388
447
|
const crosshair = useCrosshairSeries(
|
|
389
448
|
engine,
|
|
390
449
|
effectivePadding,
|
|
@@ -393,6 +452,17 @@ function useLiveChartSeriesController({
|
|
|
393
452
|
scrubHoldMs,
|
|
394
453
|
onGestureStart,
|
|
395
454
|
onGestureEnd,
|
|
455
|
+
scrollActive,
|
|
456
|
+
seriesTooltipCfg
|
|
457
|
+
? {
|
|
458
|
+
config: seriesTooltipCfg,
|
|
459
|
+
formatValue,
|
|
460
|
+
formatTime,
|
|
461
|
+
font: skiaFont,
|
|
462
|
+
colors: lineColors,
|
|
463
|
+
maxTime: tooltipMaxTime,
|
|
464
|
+
}
|
|
465
|
+
: undefined,
|
|
396
466
|
);
|
|
397
467
|
|
|
398
468
|
// Capture only the shared value in the worklets below. Referencing
|
|
@@ -436,6 +506,10 @@ function useLiveChartSeriesController({
|
|
|
436
506
|
minTime: scrollMinTime,
|
|
437
507
|
enabled: timeScrollEnabled,
|
|
438
508
|
mode: scrollGestureMode,
|
|
509
|
+
scrollActive,
|
|
510
|
+
// Once a scrub is engaged the chart is locked: scrolling goes inert so the
|
|
511
|
+
// finger only moves the price indicator across a fixed window.
|
|
512
|
+
scrubActive: crosshairScrubActive,
|
|
439
513
|
onScrollStart: () => {
|
|
440
514
|
"worklet";
|
|
441
515
|
crosshairScrubActive.set(false);
|
|
@@ -503,6 +577,7 @@ function useLiveChartSeriesController({
|
|
|
503
577
|
// passthrough props the render needs
|
|
504
578
|
series,
|
|
505
579
|
style,
|
|
580
|
+
canvasMode,
|
|
506
581
|
accessibilityLabel,
|
|
507
582
|
accessibilityRole,
|
|
508
583
|
emptyText,
|
|
@@ -512,6 +587,7 @@ function useLiveChartSeriesController({
|
|
|
512
587
|
yAxisCfg,
|
|
513
588
|
xAxisCfg,
|
|
514
589
|
scrubCfg,
|
|
590
|
+
seriesTooltipCfg,
|
|
515
591
|
gridStyleCfg,
|
|
516
592
|
dotCfg,
|
|
517
593
|
dotOuterRadius,
|
|
@@ -519,6 +595,10 @@ function useLiveChartSeriesController({
|
|
|
519
595
|
degenCfg,
|
|
520
596
|
metricsCfg,
|
|
521
597
|
allRefLines,
|
|
598
|
+
refLineKeys,
|
|
599
|
+
refLineCustom,
|
|
600
|
+
refLineOffAxisCustom,
|
|
601
|
+
refLineCustomTagWidths,
|
|
522
602
|
leftEdgeFadeCfg,
|
|
523
603
|
// theme / layout / fonts
|
|
524
604
|
palette,
|
|
@@ -540,6 +620,7 @@ function useLiveChartSeriesController({
|
|
|
540
620
|
layoutHeight,
|
|
541
621
|
onLayout,
|
|
542
622
|
linePaths,
|
|
623
|
+
activeSeriesCount,
|
|
543
624
|
lineColors,
|
|
544
625
|
lineStyles,
|
|
545
626
|
degenPack,
|
|
@@ -555,6 +636,9 @@ function useLiveChartSeriesController({
|
|
|
555
636
|
markerGroupOpacity,
|
|
556
637
|
overlayScrubFade,
|
|
557
638
|
renderMarker,
|
|
639
|
+
renderOverlay,
|
|
640
|
+
renderReferenceLine,
|
|
641
|
+
renderOffAxisReferenceLine,
|
|
558
642
|
// selection dot: resolved config + fallback color (the leading series' color)
|
|
559
643
|
selectionDot: selectionDotCfg,
|
|
560
644
|
selectionColor: lineColors[0],
|
|
@@ -610,6 +694,9 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
610
694
|
loadingStrokeWidth,
|
|
611
695
|
loadingAmplitude,
|
|
612
696
|
loadingSpeed,
|
|
697
|
+
canvasMode,
|
|
698
|
+
activeSeriesCount,
|
|
699
|
+
refLineKeys,
|
|
613
700
|
} = model;
|
|
614
701
|
|
|
615
702
|
return (
|
|
@@ -629,14 +716,12 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
629
716
|
</Group>
|
|
630
717
|
)}
|
|
631
718
|
|
|
632
|
-
{/*
|
|
633
|
-
|
|
634
|
-
content-derived key would collapse to one. Fade group lets
|
|
635
|
-
`scrub.hideOverlaysOnScrub` ease the lines out while scrubbing. */}
|
|
719
|
+
{/* Fade group lets `scrub.hideOverlaysOnScrub` ease the lines out. Explicit
|
|
720
|
+
ids keep each reference line mounted when the caller reorders it. */}
|
|
636
721
|
<Group opacity={overlayScrubFade}>
|
|
637
722
|
{allRefLines.map((rl, i) => (
|
|
638
723
|
<ReferenceLineOverlay
|
|
639
|
-
key={i}
|
|
724
|
+
key={refLineKeys[i]}
|
|
640
725
|
engine={engine}
|
|
641
726
|
padding={effectivePadding}
|
|
642
727
|
line={rl}
|
|
@@ -654,12 +739,13 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
654
739
|
padding={effectivePadding}
|
|
655
740
|
colors={lineColors}
|
|
656
741
|
config={dotCfg.valueLine}
|
|
742
|
+
seriesCount={activeSeriesCount}
|
|
657
743
|
/>
|
|
658
744
|
</Group>
|
|
659
745
|
)}
|
|
660
746
|
|
|
661
747
|
<Group opacity={reveal.lineOpacity}>
|
|
662
|
-
{Array.from({ length:
|
|
748
|
+
{Array.from({ length: activeSeriesCount }, (_, i) => (
|
|
663
749
|
<MultiSeriesStroke
|
|
664
750
|
key={i}
|
|
665
751
|
index={i}
|
|
@@ -694,6 +780,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
694
780
|
color={dotCfg.color}
|
|
695
781
|
pulse={dotCfg.pulse}
|
|
696
782
|
viewEnd={engine.viewEnd}
|
|
783
|
+
seriesCount={activeSeriesCount}
|
|
697
784
|
/>
|
|
698
785
|
</Group>
|
|
699
786
|
)}
|
|
@@ -749,6 +836,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
749
836
|
lineStrokeWidth={loadingStrokeWidth}
|
|
750
837
|
waveAmplitude={loadingAmplitude}
|
|
751
838
|
waveSpeed={loadingSpeed}
|
|
839
|
+
opaqueCanvas={canvasMode === "opaque"}
|
|
752
840
|
/>
|
|
753
841
|
</Group>
|
|
754
842
|
);
|
|
@@ -767,6 +855,7 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
767
855
|
skiaFont,
|
|
768
856
|
reveal,
|
|
769
857
|
degenShakeTransform,
|
|
858
|
+
activeSeriesCount,
|
|
770
859
|
} = model;
|
|
771
860
|
if (!dotCfg.valueLabel) return null;
|
|
772
861
|
return (
|
|
@@ -778,6 +867,7 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
778
867
|
colors={lineColors}
|
|
779
868
|
font={skiaFont}
|
|
780
869
|
dotRadius={dotOuterRadius}
|
|
870
|
+
seriesCount={activeSeriesCount}
|
|
781
871
|
/>
|
|
782
872
|
</Group>
|
|
783
873
|
</Group>
|
|
@@ -789,6 +879,10 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
789
879
|
function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
790
880
|
const {
|
|
791
881
|
allRefLines,
|
|
882
|
+
refLineKeys,
|
|
883
|
+
refLineCustom,
|
|
884
|
+
refLineOffAxisCustom,
|
|
885
|
+
refLineCustomTagWidths,
|
|
792
886
|
engine,
|
|
793
887
|
effectivePadding,
|
|
794
888
|
palette,
|
|
@@ -802,7 +896,7 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
802
896
|
<Group transform={degenShakeTransform} opacity={overlayScrubFade}>
|
|
803
897
|
{allRefLines.map((rl, i) => (
|
|
804
898
|
<ReferenceLineOverlay
|
|
805
|
-
key={i}
|
|
899
|
+
key={refLineKeys[i]}
|
|
806
900
|
engine={engine}
|
|
807
901
|
padding={effectivePadding}
|
|
808
902
|
line={rl}
|
|
@@ -810,12 +904,22 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
810
904
|
formatValue={formatValue}
|
|
811
905
|
font={skiaFont}
|
|
812
906
|
badgeLayer
|
|
907
|
+
suppressTag={refLineCustom[i]}
|
|
908
|
+
suppressTagWhenOffAxis={refLineOffAxisCustom[i]}
|
|
909
|
+
customTagWidths={refLineCustomTagWidths}
|
|
813
910
|
/>
|
|
814
911
|
))}
|
|
815
912
|
</Group>
|
|
816
913
|
);
|
|
817
914
|
}
|
|
818
915
|
|
|
916
|
+
/** Owns the price/time projection worklets for the multi-series custom overlay. */
|
|
917
|
+
function SeriesCustomConsumerOverlay({ model }: { model: LiveChartSeriesModel }) {
|
|
918
|
+
const { engine, effectivePadding, renderOverlay } = model;
|
|
919
|
+
const overlayContext = useChartOverlayContext(engine, effectivePadding);
|
|
920
|
+
return <ChartOverlayLayer render={renderOverlay!} context={overlayContext} />;
|
|
921
|
+
}
|
|
922
|
+
|
|
819
923
|
export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
820
924
|
const model = useLiveChartSeriesController(props);
|
|
821
925
|
const {
|
|
@@ -833,6 +937,7 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
833
937
|
effectivePadding,
|
|
834
938
|
engine,
|
|
835
939
|
scrubCfg,
|
|
940
|
+
seriesTooltipCfg,
|
|
836
941
|
crosshair,
|
|
837
942
|
palette,
|
|
838
943
|
dotCfg,
|
|
@@ -848,7 +953,17 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
848
953
|
markersSV,
|
|
849
954
|
markerClusterCfg,
|
|
850
955
|
renderMarker,
|
|
956
|
+
renderOverlay,
|
|
957
|
+
renderReferenceLine,
|
|
958
|
+
renderOffAxisReferenceLine,
|
|
959
|
+
allRefLines,
|
|
960
|
+
refLineCustom,
|
|
961
|
+
refLineOffAxisCustom,
|
|
962
|
+
refLineCustomTagWidths,
|
|
851
963
|
overlayScrubFade,
|
|
964
|
+
canvasMode,
|
|
965
|
+
activeSeriesCount,
|
|
966
|
+
skiaFont,
|
|
852
967
|
} = model;
|
|
853
968
|
|
|
854
969
|
// Mirror the Skia overlay fade onto the RN custom-marker sibling so
|
|
@@ -891,7 +1006,21 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
891
1006
|
else points map into a taller area and the x-axis draws past the edge. */}
|
|
892
1007
|
<GestureDetector gesture={rootGesture}>
|
|
893
1008
|
<View style={{ flex: 1 }} onLayout={onLayout}>
|
|
894
|
-
|
|
1009
|
+
{/* Skia chooses TextureView vs SurfaceView when the native Canvas mounts. */}
|
|
1010
|
+
<Canvas
|
|
1011
|
+
key={canvasMode}
|
|
1012
|
+
style={{ flex: 1, minHeight: layoutHeight || 1 }}
|
|
1013
|
+
opaque={canvasMode === "opaque"}
|
|
1014
|
+
>
|
|
1015
|
+
{canvasMode === "opaque" && (
|
|
1016
|
+
<Rect
|
|
1017
|
+
x={0}
|
|
1018
|
+
y={0}
|
|
1019
|
+
width={engine.canvasWidth}
|
|
1020
|
+
height={engine.canvasHeight}
|
|
1021
|
+
color={backgroundColor}
|
|
1022
|
+
/>
|
|
1023
|
+
)}
|
|
895
1024
|
<SeriesChartStack model={model} />
|
|
896
1025
|
|
|
897
1026
|
{/* "extrema-edge" connector lines (dot → edge readout). Outside the
|
|
@@ -910,6 +1039,9 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
910
1039
|
startColor={leftEdgeFadeCfg.startColor}
|
|
911
1040
|
endColor={leftEdgeFadeCfg.endColor}
|
|
912
1041
|
engine={engine}
|
|
1042
|
+
opaqueBackgroundRgb={
|
|
1043
|
+
canvasMode === "opaque" ? palette.bgRgb : undefined
|
|
1044
|
+
}
|
|
913
1045
|
/>
|
|
914
1046
|
)}
|
|
915
1047
|
|
|
@@ -929,15 +1061,37 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
929
1061
|
selectionColor={selectionColor}
|
|
930
1062
|
dimOpacity={scrubCfg.dimOpacity}
|
|
931
1063
|
liveDotExtent={liveDotExtent}
|
|
932
|
-
crosshairLineColor={
|
|
933
|
-
|
|
1064
|
+
crosshairLineColor={
|
|
1065
|
+
seriesTooltipCfg?.guideColor ??
|
|
1066
|
+
scrubCfg.crosshairLineColor
|
|
1067
|
+
}
|
|
1068
|
+
crosshairLineWidth={seriesTooltipCfg?.guideWidth}
|
|
1069
|
+
crosshairDash={
|
|
1070
|
+
seriesTooltipCfg
|
|
1071
|
+
? seriesTooltipCfg.guideDashPattern
|
|
1072
|
+
: scrubCfg.crosshairDash
|
|
1073
|
+
}
|
|
934
1074
|
crosshairDimColor={scrubCfg.crosshairDimColor}
|
|
1075
|
+
opaqueCanvas={canvasMode === "opaque"}
|
|
935
1076
|
/>
|
|
936
1077
|
)}
|
|
937
1078
|
|
|
938
1079
|
{/* Per-series value labels on top of the scrub dim so the dim never
|
|
939
1080
|
clips them (they track each series' live value, not the scrub). */}
|
|
940
1081
|
<SeriesValueLabelLayer model={model} />
|
|
1082
|
+
|
|
1083
|
+
{seriesTooltipCfg && (
|
|
1084
|
+
<PerSeriesTooltipOverlay
|
|
1085
|
+
layout={crosshair.tooltipLayout}
|
|
1086
|
+
font={skiaFont}
|
|
1087
|
+
palette={palette}
|
|
1088
|
+
config={seriesTooltipCfg}
|
|
1089
|
+
seriesCount={activeSeriesCount}
|
|
1090
|
+
tooltipBackground={scrubCfg?.tooltipBackground}
|
|
1091
|
+
tooltipColor={scrubCfg?.tooltipColor}
|
|
1092
|
+
tooltipBorderColor={scrubCfg?.tooltipBorderColor}
|
|
1093
|
+
/>
|
|
1094
|
+
)}
|
|
941
1095
|
</Canvas>
|
|
942
1096
|
|
|
943
1097
|
{/* RN labels floated over the canvas (sibling of <Canvas>, an RN
|
|
@@ -952,25 +1106,56 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
952
1106
|
padding={effectivePadding}
|
|
953
1107
|
/>
|
|
954
1108
|
|
|
955
|
-
{/* Custom
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
{markersActive && renderMarker
|
|
1109
|
+
{/* Custom annotations — RN views floated over the canvas (non-Skia),
|
|
1110
|
+
pinned to their live positions. As a post-Canvas sibling they render
|
|
1111
|
+
above the left-edge fade and scrub overlay. The box-none fade wrapper
|
|
1112
|
+
keeps `scrub.hideOverlaysOnScrub` behavior aligned with Skia. */}
|
|
1113
|
+
{((markersActive && renderMarker) ||
|
|
1114
|
+
((renderReferenceLine || renderOffAxisReferenceLine) &&
|
|
1115
|
+
allRefLines.length > 0)) && (
|
|
960
1116
|
<Animated.View
|
|
961
1117
|
pointerEvents="box-none"
|
|
962
1118
|
style={[StyleSheet.absoluteFill, overlayFadeStyle]}
|
|
963
1119
|
>
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1120
|
+
{markersActive && renderMarker && (
|
|
1121
|
+
<CustomMarkerOverlay
|
|
1122
|
+
markers={markersSV}
|
|
1123
|
+
renderMarker={renderMarker}
|
|
1124
|
+
engine={engine}
|
|
1125
|
+
padding={effectivePadding}
|
|
1126
|
+
series={series}
|
|
1127
|
+
cluster={markerClusterCfg}
|
|
1128
|
+
/>
|
|
1129
|
+
)}
|
|
1130
|
+
{renderReferenceLine && allRefLines.length > 0 && (
|
|
1131
|
+
<CustomReferenceLineOverlay
|
|
1132
|
+
lines={allRefLines}
|
|
1133
|
+
renderReferenceLine={renderReferenceLine}
|
|
1134
|
+
custom={refLineCustom}
|
|
1135
|
+
engine={engine}
|
|
1136
|
+
padding={effectivePadding}
|
|
1137
|
+
formatValue={formatValue}
|
|
1138
|
+
tagWidths={refLineCustomTagWidths}
|
|
1139
|
+
/>
|
|
1140
|
+
)}
|
|
1141
|
+
{renderOffAxisReferenceLine && allRefLines.length > 0 && (
|
|
1142
|
+
<CustomReferenceLineOverlay
|
|
1143
|
+
lines={allRefLines}
|
|
1144
|
+
renderReferenceLine={renderOffAxisReferenceLine}
|
|
1145
|
+
custom={refLineOffAxisCustom}
|
|
1146
|
+
engine={engine}
|
|
1147
|
+
padding={effectivePadding}
|
|
1148
|
+
formatValue={formatValue}
|
|
1149
|
+
tagWidths={refLineCustomTagWidths}
|
|
1150
|
+
offAxisOnly
|
|
1151
|
+
/>
|
|
1152
|
+
)}
|
|
972
1153
|
</Animated.View>
|
|
973
1154
|
)}
|
|
1155
|
+
|
|
1156
|
+
{/* Custom consumer overlay — topmost RN sibling with the live plot
|
|
1157
|
+
rect, including this multi-series chart's resolved axis inset. */}
|
|
1158
|
+
{renderOverlay && <SeriesCustomConsumerOverlay model={model} />}
|
|
974
1159
|
</View>
|
|
975
1160
|
</GestureDetector>
|
|
976
1161
|
{legendCfg.position === "bottom" ? legend : null}
|
|
@@ -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,
|