react-native-livechart 3.11.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/BadgeOverlay.d.ts +9 -1
- package/dist/components/BadgeOverlay.d.ts.map +1 -1
- package/dist/components/ChartOverlayLayer.d.ts +19 -0
- package/dist/components/ChartOverlayLayer.d.ts.map +1 -0
- package/dist/components/CustomMarkerOverlay.d.ts +18 -6
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +36 -0
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +5 -2
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +7 -1
- package/dist/components/DotOverlay.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/MarkerOverlay.d.ts +8 -3
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +4 -1
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/ReferenceLineGroupOverlay.d.ts +27 -0
- package/dist/components/ReferenceLineGroupOverlay.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +25 -2
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/XAxisOverlay.d.ts +7 -1
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +12 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +29 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts +18 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +56 -2
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +58 -7
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +8 -2
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +20 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/draw/volume.d.ts +25 -0
- package/dist/draw/volume.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +12 -6
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/overlayScale.d.ts +22 -0
- package/dist/hooks/overlayScale.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +9 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
- package/dist/hooks/useBadge.d.ts +22 -1
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +6 -0
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartOverlayContext.d.ts +45 -0
- package/dist/hooks/useChartOverlayContext.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +8 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -2
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useLiveDot.d.ts +6 -1
- package/dist/hooks/useLiveDot.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +12 -4
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +82 -0
- package/dist/hooks/usePanScroll.d.ts.map +1 -0
- package/dist/hooks/usePinchZoom.d.ts +74 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -0
- package/dist/hooks/useReferenceDrag.d.ts +24 -0
- package/dist/hooks/useReferenceDrag.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +23 -4
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLinePress.d.ts +5 -1
- package/dist/hooks/useReferenceLinePress.d.ts.map +1 -1
- package/dist/hooks/useVisibleRange.d.ts +39 -0
- package/dist/hooks/useVisibleRange.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/markerCluster.d.ts +47 -0
- package/dist/math/markerCluster.d.ts.map +1 -0
- package/dist/math/markers.d.ts +15 -0
- package/dist/math/markers.d.ts.map +1 -1
- package/dist/math/referenceDrag.d.ts +24 -0
- package/dist/math/referenceDrag.d.ts.map +1 -0
- package/dist/math/referenceGroup.d.ts +31 -0
- package/dist/math/referenceGroup.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +33 -7
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/types.d.ts +512 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BadgeOverlay.tsx +26 -1
- package/src/components/ChartOverlayLayer.tsx +32 -0
- package/src/components/CustomMarkerOverlay.tsx +153 -53
- package/src/components/CustomReferenceLineOverlay.tsx +250 -0
- package/src/components/CustomTooltipOverlay.tsx +27 -7
- package/src/components/DotOverlay.tsx +9 -0
- package/src/components/LiveChart.tsx +561 -34
- package/src/components/LiveChartSeries.tsx +110 -5
- package/src/components/MarkerOverlay.tsx +100 -6
- package/src/components/MultiSeriesDots.tsx +12 -3
- package/src/components/ReferenceLineGroupOverlay.tsx +207 -0
- package/src/components/ReferenceLineOverlay.tsx +105 -32
- package/src/components/XAxisOverlay.tsx +9 -2
- package/src/components/YAxisOverlay.tsx +48 -26
- package/src/constants.ts +10 -0
- package/src/core/liveChartEngineTick.ts +81 -8
- package/src/core/liveChartSeriesEngineTick.ts +50 -9
- package/src/core/resolveConfig.ts +122 -1
- package/src/core/useLiveChartEngine.ts +103 -10
- package/src/core/useLiveChartSeriesEngine.ts +26 -3
- package/src/draw/grid.ts +13 -1
- package/src/draw/markerAtlas.ts +117 -3
- package/src/draw/volume.ts +190 -0
- package/src/hooks/crosshairShared.ts +37 -9
- package/src/hooks/overlayScale.ts +59 -0
- package/src/hooks/resolveChartLayout.ts +33 -0
- package/src/hooks/useBadge.ts +89 -21
- package/src/hooks/useCandlePaths.ts +104 -7
- package/src/hooks/useChartOverlayContext.ts +93 -0
- package/src/hooks/useChartPaths.ts +14 -1
- package/src/hooks/useCrosshair.ts +29 -2
- package/src/hooks/useLiveDot.ts +16 -5
- package/src/hooks/useMarkers.ts +43 -11
- package/src/hooks/useMultiSeriesLinePaths.ts +2 -1
- package/src/hooks/usePanScroll.ts +253 -0
- package/src/hooks/usePinchZoom.ts +189 -0
- package/src/hooks/useReferenceDrag.ts +254 -0
- package/src/hooks/useReferenceLine.ts +56 -9
- package/src/hooks/useReferenceLinePress.ts +16 -2
- package/src/hooks/useVisibleRange.ts +119 -0
- package/src/index.ts +17 -1
- package/src/math/markerCluster.ts +214 -0
- package/src/math/markers.ts +54 -9
- package/src/math/referenceDrag.ts +66 -0
- package/src/math/referenceGroup.ts +65 -0
- package/src/math/referenceLines.ts +76 -19
- package/src/types.ts +532 -21
|
@@ -15,7 +15,11 @@ import {
|
|
|
15
15
|
type SharedValue,
|
|
16
16
|
} from "react-native-reanimated";
|
|
17
17
|
import { scheduleOnRN } from "react-native-worklets";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_ACCENT_COLOR,
|
|
20
|
+
HOLD_TO_SCRUB_MS,
|
|
21
|
+
MAX_MULTI_SERIES,
|
|
22
|
+
} from "../constants";
|
|
19
23
|
import {
|
|
20
24
|
lineColorsSignatureFromArray,
|
|
21
25
|
lineStyleSignatureFromArray,
|
|
@@ -29,12 +33,14 @@ import {
|
|
|
29
33
|
resolveGridStyle,
|
|
30
34
|
resolveLeftEdgeFade,
|
|
31
35
|
resolveLegend,
|
|
36
|
+
resolveMarkerCluster,
|
|
32
37
|
resolveMetrics,
|
|
33
38
|
resolveMultiSeriesDot,
|
|
34
39
|
resolveScrub,
|
|
35
40
|
resolveSelectionDot,
|
|
36
41
|
resolveXAxis,
|
|
37
42
|
resolveYAxis,
|
|
43
|
+
resolveZoom,
|
|
38
44
|
} from "../core/resolveConfig";
|
|
39
45
|
import { useLiveChartSeriesEngine } from "../core/useLiveChartSeriesEngine";
|
|
40
46
|
import { pulseRadialOutset } from "../draw/line";
|
|
@@ -46,7 +52,10 @@ import { useCrosshairSeries } from "../hooks/useCrosshairSeries";
|
|
|
46
52
|
import { useMarkers } from "../hooks/useMarkers";
|
|
47
53
|
import { useMultiSeriesDegen } from "../hooks/useMultiSeriesDegen";
|
|
48
54
|
import { useMultiSeriesLinePaths } from "../hooks/useMultiSeriesLinePaths";
|
|
55
|
+
import { usePanScroll } from "../hooks/usePanScroll";
|
|
56
|
+
import { usePinchZoom } from "../hooks/usePinchZoom";
|
|
49
57
|
import { useMultiSeriesReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
|
|
58
|
+
import { useVisibleRange } from "../hooks/useVisibleRange";
|
|
50
59
|
import { useXAxis } from "../hooks/useXAxis";
|
|
51
60
|
import { useYAxis } from "../hooks/useYAxis";
|
|
52
61
|
import {
|
|
@@ -137,22 +146,28 @@ function useLiveChartSeriesController({
|
|
|
137
146
|
metrics,
|
|
138
147
|
scrub = true,
|
|
139
148
|
selectionDot,
|
|
149
|
+
timeScroll = false,
|
|
150
|
+
zoom = false,
|
|
140
151
|
onScrub,
|
|
141
152
|
onGestureStart,
|
|
142
153
|
onGestureEnd,
|
|
154
|
+
onVisibleRangeChange,
|
|
155
|
+
onReachStart,
|
|
143
156
|
onSeriesToggle,
|
|
144
157
|
dot: dotProp,
|
|
145
158
|
legend: legendProp,
|
|
146
159
|
degen,
|
|
147
160
|
onDegenShake,
|
|
148
161
|
markers,
|
|
149
|
-
|
|
162
|
+
onMarkerPress,
|
|
150
163
|
markerHitRadius = 16,
|
|
164
|
+
markerCluster,
|
|
151
165
|
renderMarker,
|
|
152
166
|
leftEdgeFade = true,
|
|
153
167
|
}: LiveChartSeriesProps) {
|
|
154
168
|
const emptyMarkers = useSharedValue<Marker[]>([]);
|
|
155
169
|
const markersSV = markers ?? emptyMarkers;
|
|
170
|
+
const markerClusterCfg = resolveMarkerCluster(markerCluster);
|
|
156
171
|
const markersActive = markers != null;
|
|
157
172
|
const yAxisCfg = resolveYAxis(yAxis);
|
|
158
173
|
const xAxisCfg = resolveXAxis(xAxis);
|
|
@@ -160,6 +175,27 @@ function useLiveChartSeriesController({
|
|
|
160
175
|
const bottomLabelCfg = resolveAxisLabel(bottomLabel);
|
|
161
176
|
const scrubCfg = resolveScrub(scrub);
|
|
162
177
|
const scrubEnabled = scrubCfg !== null;
|
|
178
|
+
|
|
179
|
+
// Time-scroll + pinch-zoom (mirrors LiveChart). LiveChartSeries has no static
|
|
180
|
+
// mode, so these gate on the props alone. `holdToScrub` requires the scrub
|
|
181
|
+
// gesture to wait for a press-and-hold so a quick drag scrolls instead — the
|
|
182
|
+
// hold delay is threaded into `useCrosshairSeries` below.
|
|
183
|
+
const timeScrollEnabled = Boolean(timeScroll);
|
|
184
|
+
const zoomCfg = resolveZoom(zoom);
|
|
185
|
+
const zoomEnabled = zoomCfg !== null;
|
|
186
|
+
const scrollGestureMode =
|
|
187
|
+
typeof timeScroll === "object"
|
|
188
|
+
? (timeScroll.gesture ?? "holdToScrub")
|
|
189
|
+
: "holdToScrub";
|
|
190
|
+
const timeScrollHoldMs =
|
|
191
|
+
typeof timeScroll === "object" ? timeScroll.scrubHoldMs : undefined;
|
|
192
|
+
// Precedence: explicit scrubHoldMs, then scrub.panGestureDelay, then default.
|
|
193
|
+
// `||` (not `??`) skips the resolved panGestureDelay's 0 default.
|
|
194
|
+
const scrubHoldMs =
|
|
195
|
+
timeScrollEnabled && scrollGestureMode === "holdToScrub"
|
|
196
|
+
? (timeScrollHoldMs ?? (scrubCfg?.panGestureDelay || HOLD_TO_SCRUB_MS))
|
|
197
|
+
: (scrubCfg?.panGestureDelay ?? 0);
|
|
198
|
+
|
|
163
199
|
const selectionDotCfg = resolveSelectionDot(selectionDot);
|
|
164
200
|
const gridStyleCfg = resolveGridStyle(gridStyle);
|
|
165
201
|
const dotCfg = resolveMultiSeriesDot(dotProp);
|
|
@@ -323,7 +359,7 @@ function useLiveChartSeriesController({
|
|
|
323
359
|
effectivePadding,
|
|
324
360
|
scrubEnabled,
|
|
325
361
|
onScrub,
|
|
326
|
-
|
|
362
|
+
scrubHoldMs,
|
|
327
363
|
onGestureStart,
|
|
328
364
|
onGestureEnd,
|
|
329
365
|
);
|
|
@@ -336,14 +372,77 @@ function useLiveChartSeriesController({
|
|
|
336
372
|
markersSV,
|
|
337
373
|
markersActive,
|
|
338
374
|
markerHitRadius,
|
|
339
|
-
|
|
375
|
+
onMarkerPress,
|
|
340
376
|
series,
|
|
377
|
+
undefined, // lineData — multi-series anchors by seriesId
|
|
378
|
+
true, // autostart
|
|
379
|
+
false, // lineLinear — per-series curve handled in projection
|
|
380
|
+
markerClusterCfg,
|
|
341
381
|
);
|
|
342
382
|
|
|
343
|
-
|
|
383
|
+
// Earliest retained time across all series — clamps how far the window pans
|
|
384
|
+
// back, and the `onReachStart` reference. Falls back to the live edge (no
|
|
385
|
+
// scrollable history) so panning is a no-op.
|
|
386
|
+
const scrollMinTime = useDerivedValue(() => {
|
|
387
|
+
const s = engine.series.get();
|
|
388
|
+
let min = Infinity;
|
|
389
|
+
for (let i = 0; i < s.length; i++) {
|
|
390
|
+
const d = s[i].data;
|
|
391
|
+
if (d.length > 0 && d[0].time < min) min = d[0].time;
|
|
392
|
+
}
|
|
393
|
+
return min === Infinity ? engine.liveEdge.get() : min;
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const panScrollGesture = usePanScroll({
|
|
397
|
+
engine,
|
|
398
|
+
padding: effectivePadding,
|
|
399
|
+
minTime: scrollMinTime,
|
|
400
|
+
enabled: timeScrollEnabled,
|
|
401
|
+
mode: scrollGestureMode,
|
|
402
|
+
onScrollStart: () => {
|
|
403
|
+
"worklet";
|
|
404
|
+
crosshair.scrubActive.set(false);
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const pinchZoomGesture = usePinchZoom({
|
|
409
|
+
engine,
|
|
410
|
+
padding: effectivePadding,
|
|
411
|
+
minTime: scrollMinTime,
|
|
412
|
+
timeWindow,
|
|
413
|
+
enabled: zoomEnabled,
|
|
414
|
+
minTimeWindow: zoomCfg?.minTimeWindow,
|
|
415
|
+
maxTimeWindow: zoomCfg?.maxTimeWindow,
|
|
416
|
+
onZoomStart: () => {
|
|
417
|
+
"worklet";
|
|
418
|
+
crosshair.scrubActive.set(false);
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
useVisibleRange({
|
|
423
|
+
engine,
|
|
424
|
+
minTime: scrollMinTime,
|
|
425
|
+
onVisibleRangeChange,
|
|
426
|
+
onReachStart,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
let rootGesture = markersActive
|
|
344
430
|
? Gesture.Race(crosshair.gesture, markerTapGesture)
|
|
345
431
|
: crosshair.gesture;
|
|
346
432
|
|
|
433
|
+
// holdToScrub races the scrub (quick drag scrolls; press-hold scrubs);
|
|
434
|
+
// axisDrag goes first via Exclusive (fails fast outside the bottom band).
|
|
435
|
+
if (timeScrollEnabled) {
|
|
436
|
+
rootGesture =
|
|
437
|
+
scrollGestureMode === "axisDrag"
|
|
438
|
+
? Gesture.Exclusive(panScrollGesture, rootGesture)
|
|
439
|
+
: Gesture.Race(panScrollGesture, rootGesture);
|
|
440
|
+
}
|
|
441
|
+
// Pinch runs alongside (two-finger, disjoint from the one-finger gestures).
|
|
442
|
+
if (zoomEnabled) {
|
|
443
|
+
rootGesture = Gesture.Simultaneous(rootGesture, pinchZoomGesture);
|
|
444
|
+
}
|
|
445
|
+
|
|
347
446
|
const backgroundColor = `rgb(${palette.bgRgb[0]}, ${palette.bgRgb[1]}, ${palette.bgRgb[2]})`;
|
|
348
447
|
|
|
349
448
|
return {
|
|
@@ -392,6 +491,7 @@ function useLiveChartSeriesController({
|
|
|
392
491
|
rootGesture,
|
|
393
492
|
markersActive,
|
|
394
493
|
markersSV,
|
|
494
|
+
markerClusterCfg,
|
|
395
495
|
renderMarker,
|
|
396
496
|
// selection dot: resolved config + fallback color (the leading series' color)
|
|
397
497
|
selectionDot: selectionDotCfg,
|
|
@@ -436,6 +536,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
436
536
|
degenPackRevision,
|
|
437
537
|
markersActive,
|
|
438
538
|
markersSV,
|
|
539
|
+
markerClusterCfg,
|
|
439
540
|
renderMarker,
|
|
440
541
|
series,
|
|
441
542
|
emptyText,
|
|
@@ -520,6 +621,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
520
621
|
ringColor={palette.badgeOuterBg}
|
|
521
622
|
color={dotCfg.color}
|
|
522
623
|
pulse={dotCfg.pulse}
|
|
624
|
+
viewEnd={engine.viewEnd}
|
|
523
625
|
/>
|
|
524
626
|
</Group>
|
|
525
627
|
)}
|
|
@@ -553,6 +655,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
553
655
|
font={skiaFont}
|
|
554
656
|
series={series}
|
|
555
657
|
renderMarker={renderMarker}
|
|
658
|
+
cluster={markerClusterCfg}
|
|
556
659
|
/>
|
|
557
660
|
</Group>
|
|
558
661
|
)}
|
|
@@ -665,6 +768,7 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
665
768
|
bottomConnector,
|
|
666
769
|
markersActive,
|
|
667
770
|
markersSV,
|
|
771
|
+
markerClusterCfg,
|
|
668
772
|
renderMarker,
|
|
669
773
|
} = model;
|
|
670
774
|
|
|
@@ -772,6 +876,7 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
772
876
|
engine={engine}
|
|
773
877
|
padding={effectivePadding}
|
|
774
878
|
series={series}
|
|
879
|
+
cluster={markerClusterCfg}
|
|
775
880
|
/>
|
|
776
881
|
)}
|
|
777
882
|
</View>
|
|
@@ -17,8 +17,12 @@ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
|
17
17
|
import type { ChartPadding } from "../draw/line";
|
|
18
18
|
import { usePathBuilder } from "../hooks/usePathBuilder";
|
|
19
19
|
import {
|
|
20
|
+
BADGE_TEXT_SCALE,
|
|
20
21
|
buildMarkerAtlas,
|
|
21
22
|
defaultMarkerColor,
|
|
23
|
+
groupBgSig,
|
|
24
|
+
groupCountText,
|
|
25
|
+
groupGlyphSig,
|
|
22
26
|
isConnectorMarker,
|
|
23
27
|
markerAppearanceSig,
|
|
24
28
|
type AtlasCell,
|
|
@@ -29,10 +33,15 @@ import {
|
|
|
29
33
|
projectPoint,
|
|
30
34
|
type ProjectedMarker,
|
|
31
35
|
} from "../math/markers";
|
|
36
|
+
import {
|
|
37
|
+
clusterMarkers,
|
|
38
|
+
type ResolvedMarkerCluster,
|
|
39
|
+
} from "../math/markerCluster";
|
|
32
40
|
import type {
|
|
33
41
|
LiveChartPalette,
|
|
34
42
|
LiveChartPoint,
|
|
35
43
|
Marker,
|
|
44
|
+
MarkerRenderContext,
|
|
36
45
|
SeriesConfig,
|
|
37
46
|
} from "../types";
|
|
38
47
|
|
|
@@ -51,6 +60,7 @@ function ConnectorGlyph({
|
|
|
51
60
|
padding,
|
|
52
61
|
seriesSV,
|
|
53
62
|
lineDataSV,
|
|
63
|
+
lineLinear,
|
|
54
64
|
axisY,
|
|
55
65
|
}: {
|
|
56
66
|
marker: Marker;
|
|
@@ -59,6 +69,7 @@ function ConnectorGlyph({
|
|
|
59
69
|
padding: ChartPadding;
|
|
60
70
|
seriesSV?: SharedValue<SeriesConfig[]>;
|
|
61
71
|
lineDataSV?: SharedValue<LiveChartPoint[]>;
|
|
72
|
+
lineLinear?: boolean;
|
|
62
73
|
axisY: SharedValue<number>;
|
|
63
74
|
}) {
|
|
64
75
|
const { kind } = marker;
|
|
@@ -80,6 +91,7 @@ function ConnectorGlyph({
|
|
|
80
91
|
displayMax: engine.displayMax.get(),
|
|
81
92
|
series: seriesSV?.get(),
|
|
82
93
|
lineData: lineDataSV?.get(),
|
|
94
|
+
lineLinear,
|
|
83
95
|
}),
|
|
84
96
|
);
|
|
85
97
|
|
|
@@ -148,7 +160,9 @@ export function MarkerOverlay({
|
|
|
148
160
|
font,
|
|
149
161
|
series,
|
|
150
162
|
lineData,
|
|
163
|
+
lineLinear,
|
|
151
164
|
renderMarker,
|
|
165
|
+
cluster,
|
|
152
166
|
}: {
|
|
153
167
|
markers: SharedValue<Marker[]>;
|
|
154
168
|
engine: ChartEngineLayout;
|
|
@@ -159,12 +173,16 @@ export function MarkerOverlay({
|
|
|
159
173
|
series?: SharedValue<SeriesConfig[]>;
|
|
160
174
|
/** Single-series line data; anchors markers that omit `value`. */
|
|
161
175
|
lineData?: SharedValue<LiveChartPoint[]>;
|
|
176
|
+
/** Single-series line is drawn linear — anchor `lineData` markers on the chord. */
|
|
177
|
+
lineLinear?: boolean;
|
|
162
178
|
/**
|
|
163
179
|
* When provided, markers it returns an element for are rendered as RN views by
|
|
164
180
|
* `CustomMarkerOverlay` instead — so they're excluded from the atlas here to
|
|
165
181
|
* avoid a (blurry) glyph drawn behind the custom element.
|
|
166
182
|
*/
|
|
167
|
-
renderMarker?: (marker: Marker) => unknown;
|
|
183
|
+
renderMarker?: (marker: Marker, ctx: MarkerRenderContext) => unknown;
|
|
184
|
+
/** Collision config; `"stacked"` fans/collapses co-located markers. */
|
|
185
|
+
cluster: ResolvedMarkerCluster;
|
|
168
186
|
}) {
|
|
169
187
|
// Seed from the current markers at mount; the reaction below keeps it in sync.
|
|
170
188
|
const [snapshot, setSnapshot] = useState<Marker[]>(() => markers.get().slice());
|
|
@@ -192,7 +210,15 @@ export function MarkerOverlay({
|
|
|
192
210
|
let k = "";
|
|
193
211
|
for (let i = 0; i < snapshot.length; i++) {
|
|
194
212
|
const m = snapshot[i];
|
|
195
|
-
|
|
213
|
+
// Detection ctx (an element-vs-null result must not depend on cluster
|
|
214
|
+
// state — see CustomMarkerOverlay); the live ctx is applied there.
|
|
215
|
+
const ctx: MarkerRenderContext = {
|
|
216
|
+
index: i,
|
|
217
|
+
isGrouped: false,
|
|
218
|
+
groupCount: 0,
|
|
219
|
+
side: m.side ?? "center",
|
|
220
|
+
};
|
|
221
|
+
if (renderMarker(m, ctx) != null) k += `${m.id}\x1f`;
|
|
196
222
|
}
|
|
197
223
|
return k;
|
|
198
224
|
}, [snapshot, renderMarker]);
|
|
@@ -219,13 +245,22 @@ export function MarkerOverlay({
|
|
|
219
245
|
// Rasterize at the screen's device-pixel ratio so sprites stay crisp on
|
|
220
246
|
// retina canvases instead of being upscaled from a logical-sized texture.
|
|
221
247
|
const dpr = PixelRatio.get();
|
|
248
|
+
const clusterStacked = cluster.mode === "stacked";
|
|
222
249
|
const atlas = useMemo(
|
|
223
|
-
() =>
|
|
250
|
+
() =>
|
|
251
|
+
buildMarkerAtlas(
|
|
252
|
+
snapshot.filter((m) => !customIds[m.id]),
|
|
253
|
+
palette,
|
|
254
|
+
font,
|
|
255
|
+
dpr,
|
|
256
|
+
clusterStacked,
|
|
257
|
+
),
|
|
224
258
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- appearanceKey/paletteKey capture the inputs that change cell pixels
|
|
225
|
-
[appearanceKey, paletteKey, font, dpr],
|
|
259
|
+
[appearanceKey, paletteKey, font, dpr, clusterStacked],
|
|
226
260
|
);
|
|
227
261
|
const cells: Record<string, AtlasCell> = atlas.cells;
|
|
228
262
|
const atlasImage = atlas.image;
|
|
263
|
+
const digitWidths = atlas.digitWidths;
|
|
229
264
|
// Inverse of the texture's device-pixel scale; shrinks each hi-res cell back
|
|
230
265
|
// to its logical on-canvas size in the per-frame blit.
|
|
231
266
|
const invScale = 1 / atlas.scale;
|
|
@@ -263,16 +298,61 @@ export function MarkerOverlay({
|
|
|
263
298
|
displayMax: engine.displayMax.get(),
|
|
264
299
|
series: series?.get(),
|
|
265
300
|
lineData: lineData?.get(),
|
|
301
|
+
lineLinear,
|
|
266
302
|
});
|
|
303
|
+
clusterMarkers(ms, buf, { config: cluster });
|
|
267
304
|
const transforms = [];
|
|
268
305
|
const sprites = [];
|
|
269
306
|
for (let i = 0; i < ms.length; i++) {
|
|
270
307
|
const pt = buf[i];
|
|
271
|
-
|
|
308
|
+
// Collapsed-cluster members fold into their representative's badge.
|
|
309
|
+
if (!pt.visible || pt.hidden) continue;
|
|
272
310
|
const m = ms[i];
|
|
273
311
|
if (isConnectorMarker(m)) continue;
|
|
274
312
|
// Custom-rendered markers are floated as RN views, not drawn here.
|
|
275
313
|
if (customIds[m.id]) continue;
|
|
314
|
+
// Collapsed cluster: draw a count badge (bg stadium + composed digits)
|
|
315
|
+
// instead of the marker's own glyph, all within this one `drawAtlas`.
|
|
316
|
+
if (pt.isGrouped) {
|
|
317
|
+
const repColor = m.color ?? defaultMarkerColor(m.kind, palette);
|
|
318
|
+
const text = groupCountText(pt.groupCount);
|
|
319
|
+
const bg = cells[groupBgSig(repColor)];
|
|
320
|
+
if (bg) {
|
|
321
|
+
transforms.push(
|
|
322
|
+
Skia.RSXform(invScale, 0, pt.x - bg.w / 2, pt.y - bg.h / 2),
|
|
323
|
+
);
|
|
324
|
+
sprites.push(bg.rect);
|
|
325
|
+
}
|
|
326
|
+
// Lay the digits out proportionally (each by its own ink width), scaled
|
|
327
|
+
// down to fit the small round badge, and center the whole number. A
|
|
328
|
+
// negative kern (fraction of a digit) closes the font's side-bearing gap
|
|
329
|
+
// so "12" reads tight, not "1 2".
|
|
330
|
+
const S = BADGE_TEXT_SCALE;
|
|
331
|
+
let maxDW = 1;
|
|
332
|
+
for (let d = 0; d < 10; d++) {
|
|
333
|
+
const dw = digitWidths["" + d] ?? 0;
|
|
334
|
+
if (dw > maxDW) maxDW = dw;
|
|
335
|
+
}
|
|
336
|
+
const kern = -maxDW * S * 0.42;
|
|
337
|
+
let totalW = 0;
|
|
338
|
+
for (let c = 0; c < text.length; c++) {
|
|
339
|
+
totalW += (digitWidths[text[c]] ?? 0) * S + (c > 0 ? kern : 0);
|
|
340
|
+
}
|
|
341
|
+
let dx = pt.x - totalW / 2;
|
|
342
|
+
for (let c = 0; c < text.length; c++) {
|
|
343
|
+
const w = (digitWidths[text[c]] ?? 0) * S;
|
|
344
|
+
const gc = cells[groupGlyphSig(text[c])];
|
|
345
|
+
if (gc) {
|
|
346
|
+
const cx = dx + w / 2;
|
|
347
|
+
transforms.push(
|
|
348
|
+
Skia.RSXform(invScale * S, 0, cx - (gc.w * S) / 2, pt.y - (gc.h * S) / 2),
|
|
349
|
+
);
|
|
350
|
+
sprites.push(gc.rect);
|
|
351
|
+
}
|
|
352
|
+
dx += w + kern;
|
|
353
|
+
}
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
276
356
|
const cell = cells[markerAppearanceSig(m)];
|
|
277
357
|
if (!cell) continue;
|
|
278
358
|
// Center the cell on the projected point. The cell's source rect is in
|
|
@@ -284,7 +364,20 @@ export function MarkerOverlay({
|
|
|
284
364
|
}
|
|
285
365
|
return { transforms, sprites };
|
|
286
366
|
},
|
|
287
|
-
[
|
|
367
|
+
[
|
|
368
|
+
cells,
|
|
369
|
+
customIds,
|
|
370
|
+
invScale,
|
|
371
|
+
digitWidths,
|
|
372
|
+
markers,
|
|
373
|
+
engine,
|
|
374
|
+
padding,
|
|
375
|
+
palette,
|
|
376
|
+
series,
|
|
377
|
+
lineData,
|
|
378
|
+
lineLinear,
|
|
379
|
+
cluster,
|
|
380
|
+
],
|
|
288
381
|
);
|
|
289
382
|
const transforms = useDerivedValue(() => atlasData.get().transforms, [atlasData]);
|
|
290
383
|
const sprites = useDerivedValue(() => atlasData.get().sprites, [atlasData]);
|
|
@@ -309,6 +402,7 @@ export function MarkerOverlay({
|
|
|
309
402
|
padding={padding}
|
|
310
403
|
seriesSV={series}
|
|
311
404
|
lineDataSV={lineData}
|
|
405
|
+
lineLinear={lineLinear}
|
|
312
406
|
axisY={axisY}
|
|
313
407
|
/>
|
|
314
408
|
))}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Circle, Group } from "@shopify/react-native-skia";
|
|
2
2
|
|
|
3
|
-
import { useDerivedValue } from "react-native-reanimated";
|
|
3
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
4
4
|
import { MAX_MULTI_SERIES } from "../constants";
|
|
5
5
|
import type { ChartPadding } from "../draw/line";
|
|
6
6
|
import type {
|
|
@@ -21,6 +21,7 @@ function SeriesDotAtIndex({
|
|
|
21
21
|
ring,
|
|
22
22
|
ringColor,
|
|
23
23
|
pulse,
|
|
24
|
+
viewEnd,
|
|
24
25
|
}: {
|
|
25
26
|
index: number;
|
|
26
27
|
engine: MultiEngineState;
|
|
@@ -32,6 +33,8 @@ function SeriesDotAtIndex({
|
|
|
32
33
|
/** Fallback ring color when `ring.color` is unset (theme `badgeOuterBg`). */
|
|
33
34
|
ringColor: string;
|
|
34
35
|
pulse: ResolvedPulseConfig | null;
|
|
36
|
+
/** Pan/zoom right-edge override — pulse is frozen-out while scrolled back. */
|
|
37
|
+
viewEnd?: SharedValue<number | null>;
|
|
35
38
|
}) {
|
|
36
39
|
const dotX = useDerivedValue(() => {
|
|
37
40
|
const w = engine.canvasWidth.value;
|
|
@@ -62,7 +65,9 @@ function SeriesDotAtIndex({
|
|
|
62
65
|
});
|
|
63
66
|
|
|
64
67
|
const pulseRadius = useDerivedValue(() => {
|
|
65
|
-
|
|
68
|
+
// A live heartbeat on a frozen historical point is wrong (and `timestamp` is
|
|
69
|
+
// frozen, so it wouldn't animate) — suppress the pulse while scrolled back.
|
|
70
|
+
if (!pulse || viewEnd?.value != null) return 0;
|
|
66
71
|
const nowMs = engine.timestamp.value * 1000 + index * PULSE_STAGGER_MS;
|
|
67
72
|
const t = (nowMs % pulse.interval) / pulse.duration;
|
|
68
73
|
if (t >= 1) return 0;
|
|
@@ -70,7 +75,7 @@ function SeriesDotAtIndex({
|
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
const pulseOpacity = useDerivedValue(() => {
|
|
73
|
-
if (!pulse) return 0;
|
|
78
|
+
if (!pulse || viewEnd?.value != null) return 0;
|
|
74
79
|
const nowMs = engine.timestamp.value * 1000 + index * PULSE_STAGGER_MS;
|
|
75
80
|
const t = (nowMs % pulse.interval) / pulse.duration;
|
|
76
81
|
if (t >= 1) return 0;
|
|
@@ -112,6 +117,7 @@ export function MultiSeriesDots({
|
|
|
112
117
|
ringColor,
|
|
113
118
|
color,
|
|
114
119
|
pulse,
|
|
120
|
+
viewEnd,
|
|
115
121
|
}: {
|
|
116
122
|
engine: MultiEngineState;
|
|
117
123
|
padding: ChartPadding;
|
|
@@ -124,6 +130,8 @@ export function MultiSeriesDots({
|
|
|
124
130
|
/** Fill color override; falls back to each series' line color. */
|
|
125
131
|
color: string | undefined;
|
|
126
132
|
pulse: ResolvedPulseConfig | null;
|
|
133
|
+
/** Pan/zoom right-edge override — pulse is frozen-out while scrolled back. */
|
|
134
|
+
viewEnd?: SharedValue<number | null>;
|
|
127
135
|
}) {
|
|
128
136
|
return (
|
|
129
137
|
<Group>
|
|
@@ -138,6 +146,7 @@ export function MultiSeriesDots({
|
|
|
138
146
|
ring={ring}
|
|
139
147
|
ringColor={ringColor}
|
|
140
148
|
pulse={pulse}
|
|
149
|
+
viewEnd={viewEnd}
|
|
141
150
|
/>
|
|
142
151
|
))}
|
|
143
152
|
</Group>
|