react-native-livechart 4.3.0 → 4.5.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/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/LoadingOverlay.d.ts +9 -1
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +35 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +9 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +5 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useChartReveal.d.ts +3 -1
- package/dist/hooks/useChartReveal.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useModeBlend.d.ts +4 -1
- package/dist/hooks/useModeBlend.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/markerCluster.d.ts +8 -1
- package/dist/math/markerCluster.d.ts.map +1 -1
- package/dist/math/squiggly.d.ts +8 -5
- package/dist/math/squiggly.d.ts.map +1 -1
- package/dist/types.d.ts +128 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/LiveChart.tsx +126 -42
- package/src/components/LiveChartSeries.tsx +95 -26
- package/src/components/LoadingOverlay.tsx +25 -6
- package/src/components/MarkerOverlay.tsx +135 -37
- package/src/constants.ts +17 -0
- package/src/core/resolveConfig.ts +67 -0
- package/src/draw/markerAtlas.ts +30 -1
- package/src/hooks/useChartPaths.ts +11 -1
- package/src/hooks/useChartReveal.ts +7 -2
- package/src/hooks/useMarkers.ts +2 -0
- package/src/hooks/useModeBlend.ts +5 -3
- package/src/index.ts +3 -0
- package/src/math/markerCluster.ts +8 -1
- package/src/math/squiggly.ts +21 -7
- package/src/types.ts +131 -2
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Canvas, Group } from "@shopify/react-native-skia";
|
|
8
8
|
import { useLayoutEffect, useState } from "react";
|
|
9
|
-
import { View } from "react-native";
|
|
9
|
+
import { StyleSheet, View } from "react-native";
|
|
10
10
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
11
|
-
import {
|
|
11
|
+
import Animated, {
|
|
12
12
|
useAnimatedReaction,
|
|
13
|
+
useAnimatedStyle,
|
|
13
14
|
useDerivedValue,
|
|
14
15
|
useSharedValue,
|
|
16
|
+
withTiming,
|
|
15
17
|
type SharedValue,
|
|
16
18
|
} from "react-native-reanimated";
|
|
17
19
|
import { scheduleOnRN } from "react-native-worklets";
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
DEFAULT_ACCENT_COLOR,
|
|
20
22
|
HOLD_TO_SCRUB_MS,
|
|
21
23
|
MAX_MULTI_SERIES,
|
|
24
|
+
SCRUB_OVERLAY_FADE_MS,
|
|
22
25
|
} from "../constants";
|
|
23
26
|
import {
|
|
24
27
|
lineColorsSignatureFromArray,
|
|
@@ -33,12 +36,14 @@ import {
|
|
|
33
36
|
resolveGridStyle,
|
|
34
37
|
resolveLeftEdgeFade,
|
|
35
38
|
resolveLegend,
|
|
39
|
+
resolveLoading,
|
|
36
40
|
resolveMarkerCluster,
|
|
37
41
|
resolveMetrics,
|
|
38
42
|
resolveMultiSeriesDot,
|
|
39
43
|
resolveReturnToLiveMs,
|
|
40
44
|
resolveScrub,
|
|
41
45
|
resolveSelectionDot,
|
|
46
|
+
resolveTransitions,
|
|
42
47
|
resolveXAxis,
|
|
43
48
|
resolveYAxis,
|
|
44
49
|
resolveZoom,
|
|
@@ -126,6 +131,7 @@ function useLiveChartSeriesController({
|
|
|
126
131
|
timeWindow = 30,
|
|
127
132
|
paused = false,
|
|
128
133
|
loading = false,
|
|
134
|
+
transitions,
|
|
129
135
|
smoothing = 0.08,
|
|
130
136
|
exaggerate = false,
|
|
131
137
|
nonNegative = false,
|
|
@@ -289,7 +295,18 @@ function useLiveChartSeriesController({
|
|
|
289
295
|
return false;
|
|
290
296
|
});
|
|
291
297
|
|
|
292
|
-
|
|
298
|
+
// Resolve the loading shell: null = not loading, else the styled config.
|
|
299
|
+
const loadingCfg = resolveLoading(loading);
|
|
300
|
+
const loadingActive = loadingCfg !== null;
|
|
301
|
+
// Multi-series is always lines, so only the reveal transition applies (no
|
|
302
|
+
// candle↔line crossfade); `transitions.mode` is accepted but inert here.
|
|
303
|
+
const transitionsCfg = resolveTransitions(transitions);
|
|
304
|
+
const reveal = useChartReveal(
|
|
305
|
+
loadingActive,
|
|
306
|
+
hasData,
|
|
307
|
+
false,
|
|
308
|
+
transitionsCfg.reveal,
|
|
309
|
+
);
|
|
293
310
|
|
|
294
311
|
const effectiveSeries = useMultiSeriesReverseMorphInputs({
|
|
295
312
|
series,
|
|
@@ -453,6 +470,23 @@ function useLiveChartSeriesController({
|
|
|
453
470
|
|
|
454
471
|
const backgroundColor = `rgb(${palette.bgRgb[0]}, ${palette.bgRgb[1]}, ${palette.bgRgb[2]})`;
|
|
455
472
|
|
|
473
|
+
// Fade markers + reference lines out while scrubbing when
|
|
474
|
+
// `scrub.hideOverlaysOnScrub` is set. Eased off the scrub-ACTIVE flag (not the
|
|
475
|
+
// crosshair edge fade, which would resurface them near the live dot); only a
|
|
476
|
+
// group opacity animates — the overlay draws stay intact. See `LiveChart`.
|
|
477
|
+
const fadeOverlaysOnScrub =
|
|
478
|
+
scrubCfg !== null && scrubCfg.hideOverlaysOnScrub === true;
|
|
479
|
+
const overlayScrubFade = useDerivedValue(() =>
|
|
480
|
+
fadeOverlaysOnScrub
|
|
481
|
+
? withTiming(crosshair.scrubActive.get() ? 0 : 1, {
|
|
482
|
+
duration: SCRUB_OVERLAY_FADE_MS,
|
|
483
|
+
})
|
|
484
|
+
: 1,
|
|
485
|
+
);
|
|
486
|
+
const markerGroupOpacity = useDerivedValue(
|
|
487
|
+
() => reveal.dotOpacity.get() * overlayScrubFade.get(),
|
|
488
|
+
);
|
|
489
|
+
|
|
456
490
|
return {
|
|
457
491
|
// passthrough props the render needs
|
|
458
492
|
series,
|
|
@@ -484,6 +518,11 @@ function useLiveChartSeriesController({
|
|
|
484
518
|
// engine + reveal
|
|
485
519
|
engine,
|
|
486
520
|
reveal,
|
|
521
|
+
// loading shell styling (null → not loading)
|
|
522
|
+
loadingLineColor: loadingCfg?.color,
|
|
523
|
+
loadingStrokeWidth: loadingCfg?.strokeWidth,
|
|
524
|
+
loadingAmplitude: loadingCfg?.amplitude,
|
|
525
|
+
loadingSpeed: loadingCfg?.speed,
|
|
487
526
|
effectiveSeries,
|
|
488
527
|
layoutHeight,
|
|
489
528
|
onLayout,
|
|
@@ -500,6 +539,8 @@ function useLiveChartSeriesController({
|
|
|
500
539
|
markersActive,
|
|
501
540
|
markersSV,
|
|
502
541
|
markerClusterCfg,
|
|
542
|
+
markerGroupOpacity,
|
|
543
|
+
overlayScrubFade,
|
|
503
544
|
renderMarker,
|
|
504
545
|
// selection dot: resolved config + fallback color (the leading series' color)
|
|
505
546
|
selectionDot: selectionDotCfg,
|
|
@@ -545,10 +586,16 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
545
586
|
markersActive,
|
|
546
587
|
markersSV,
|
|
547
588
|
markerClusterCfg,
|
|
589
|
+
markerGroupOpacity,
|
|
590
|
+
overlayScrubFade,
|
|
548
591
|
renderMarker,
|
|
549
592
|
series,
|
|
550
593
|
emptyText,
|
|
551
594
|
metricsCfg,
|
|
595
|
+
loadingLineColor,
|
|
596
|
+
loadingStrokeWidth,
|
|
597
|
+
loadingAmplitude,
|
|
598
|
+
loadingSpeed,
|
|
552
599
|
} = model;
|
|
553
600
|
|
|
554
601
|
return (
|
|
@@ -570,18 +617,21 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
570
617
|
|
|
571
618
|
{/* Index keys: reference lines are a positional array and two may share
|
|
572
619
|
value + label (e.g. duplicate working orders at the same price), which a
|
|
573
|
-
content-derived key would collapse to one.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
620
|
+
content-derived key would collapse to one. Fade group lets
|
|
621
|
+
`scrub.hideOverlaysOnScrub` ease the lines out while scrubbing. */}
|
|
622
|
+
<Group opacity={overlayScrubFade}>
|
|
623
|
+
{allRefLines.map((rl, i) => (
|
|
624
|
+
<ReferenceLineOverlay
|
|
625
|
+
key={i}
|
|
626
|
+
engine={engine}
|
|
627
|
+
padding={effectivePadding}
|
|
628
|
+
line={rl}
|
|
629
|
+
palette={palette}
|
|
630
|
+
formatValue={formatValue}
|
|
631
|
+
font={skiaFont}
|
|
632
|
+
/>
|
|
633
|
+
))}
|
|
634
|
+
</Group>
|
|
585
635
|
|
|
586
636
|
{dotCfg.valueLine && (
|
|
587
637
|
<Group opacity={reveal.lineOpacity}>
|
|
@@ -654,7 +704,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
654
704
|
)}
|
|
655
705
|
|
|
656
706
|
{markersActive && (
|
|
657
|
-
<Group opacity={
|
|
707
|
+
<Group opacity={markerGroupOpacity}>
|
|
658
708
|
<MarkerOverlay
|
|
659
709
|
markers={markersSV}
|
|
660
710
|
engine={engine}
|
|
@@ -680,6 +730,10 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
680
730
|
strokeWidth={strokeWidth}
|
|
681
731
|
badge={false}
|
|
682
732
|
emptyMetrics={metricsCfg.emptyState}
|
|
733
|
+
lineColor={loadingLineColor}
|
|
734
|
+
lineStrokeWidth={loadingStrokeWidth}
|
|
735
|
+
waveAmplitude={loadingAmplitude}
|
|
736
|
+
waveSpeed={loadingSpeed}
|
|
683
737
|
/>
|
|
684
738
|
</Group>
|
|
685
739
|
);
|
|
@@ -726,10 +780,11 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
726
780
|
formatValue,
|
|
727
781
|
skiaFont,
|
|
728
782
|
degenShakeTransform,
|
|
783
|
+
overlayScrubFade,
|
|
729
784
|
} = model;
|
|
730
785
|
if (allRefLines.length === 0) return null;
|
|
731
786
|
return (
|
|
732
|
-
<Group transform={degenShakeTransform}>
|
|
787
|
+
<Group transform={degenShakeTransform} opacity={overlayScrubFade}>
|
|
733
788
|
{allRefLines.map((rl, i) => (
|
|
734
789
|
<ReferenceLineOverlay
|
|
735
790
|
key={i}
|
|
@@ -778,8 +833,15 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
778
833
|
markersSV,
|
|
779
834
|
markerClusterCfg,
|
|
780
835
|
renderMarker,
|
|
836
|
+
overlayScrubFade,
|
|
781
837
|
} = model;
|
|
782
838
|
|
|
839
|
+
// Mirror the Skia overlay fade onto the RN custom-marker sibling so
|
|
840
|
+
// `scrub.hideOverlaysOnScrub` hides it with the Skia markers.
|
|
841
|
+
const overlayFadeStyle = useAnimatedStyle(() => ({
|
|
842
|
+
opacity: overlayScrubFade.get(),
|
|
843
|
+
}));
|
|
844
|
+
|
|
783
845
|
// Extend the scrub dim past the plot's right edge to fully cover the series
|
|
784
846
|
// dots (with their halo) and pulse rings, all centered on that edge. The
|
|
785
847
|
// gutter reserves room beyond this for the value/Y-axis labels, drawn on top.
|
|
@@ -876,16 +938,23 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
876
938
|
/>
|
|
877
939
|
|
|
878
940
|
{/* Custom-rendered markers — RN views floated over the canvas
|
|
879
|
-
(non-Skia), pinned to each marker's live position.
|
|
941
|
+
(non-Skia), pinned to each marker's live position. Box-none fade
|
|
942
|
+
wrapper so `scrub.hideOverlaysOnScrub` hides them with the Skia
|
|
943
|
+
markers (full-bleed; children keep their own absolute positions). */}
|
|
880
944
|
{markersActive && renderMarker && (
|
|
881
|
-
<
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
945
|
+
<Animated.View
|
|
946
|
+
pointerEvents="box-none"
|
|
947
|
+
style={[StyleSheet.absoluteFill, overlayFadeStyle]}
|
|
948
|
+
>
|
|
949
|
+
<CustomMarkerOverlay
|
|
950
|
+
markers={markersSV}
|
|
951
|
+
renderMarker={renderMarker}
|
|
952
|
+
engine={engine}
|
|
953
|
+
padding={effectivePadding}
|
|
954
|
+
series={series}
|
|
955
|
+
cluster={markerClusterCfg}
|
|
956
|
+
/>
|
|
957
|
+
</Animated.View>
|
|
889
958
|
)}
|
|
890
959
|
</View>
|
|
891
960
|
</GestureDetector>
|
|
@@ -62,6 +62,10 @@ export function LoadingOverlay({
|
|
|
62
62
|
badgeTail = true,
|
|
63
63
|
badgeMetrics = BADGE_METRICS_DEFAULTS,
|
|
64
64
|
emptyMetrics = EMPTY_STATE_METRICS_DEFAULTS,
|
|
65
|
+
lineColor,
|
|
66
|
+
lineStrokeWidth,
|
|
67
|
+
waveAmplitude = 14,
|
|
68
|
+
waveSpeed = 1,
|
|
65
69
|
}: {
|
|
66
70
|
engine: ChartEngineLayout;
|
|
67
71
|
padding: ChartPadding;
|
|
@@ -73,6 +77,14 @@ export function LoadingOverlay({
|
|
|
73
77
|
isEmpty: SharedValue<boolean> | { value: boolean };
|
|
74
78
|
emptyText: string;
|
|
75
79
|
strokeWidth: number;
|
|
80
|
+
/** Loading squiggle + skeleton color. Omit → theme `gridLine`. */
|
|
81
|
+
lineColor?: string;
|
|
82
|
+
/** Loading squiggle stroke width. Omit → `strokeWidth`. */
|
|
83
|
+
lineStrokeWidth?: number;
|
|
84
|
+
/** Breathing-wave base amplitude (px). */
|
|
85
|
+
waveAmplitude?: number;
|
|
86
|
+
/** Breathing-wave speed multiplier. */
|
|
87
|
+
waveSpeed?: number;
|
|
76
88
|
/** Mirror the badge prop so labels align with GridOverlay's label positions. */
|
|
77
89
|
badge?: boolean;
|
|
78
90
|
/** Whether the badge tail spike is shown; affects the left inset used for skeleton alignment. */
|
|
@@ -86,6 +98,11 @@ export function LoadingOverlay({
|
|
|
86
98
|
const leftInset =
|
|
87
99
|
badgeMetrics.dotGap + badgeTailAndCap(font.getSize(), badgeTail, badgeMetrics);
|
|
88
100
|
|
|
101
|
+
// Loading-shell color (squiggle + skeleton placeholders) and squiggle stroke,
|
|
102
|
+
// both overridable via `loading={{ color, strokeWidth }}`.
|
|
103
|
+
const loadingColor = lineColor ?? palette.gridLine;
|
|
104
|
+
const loadingStroke = lineStrokeWidth ?? strokeWidth;
|
|
105
|
+
|
|
89
106
|
// Squiggly path — built into a reused PathBuilder and detach()-ed each frame.
|
|
90
107
|
const squigglyBuilder = usePathBuilder();
|
|
91
108
|
|
|
@@ -100,6 +117,8 @@ export function LoadingOverlay({
|
|
|
100
117
|
engine.canvasHeight.get(),
|
|
101
118
|
padding,
|
|
102
119
|
engine.timestamp.get(),
|
|
120
|
+
waveAmplitude,
|
|
121
|
+
waveSpeed,
|
|
103
122
|
);
|
|
104
123
|
return buildSplineDetached(b, pts);
|
|
105
124
|
});
|
|
@@ -225,8 +244,8 @@ export function LoadingOverlay({
|
|
|
225
244
|
<Path
|
|
226
245
|
path={squigglyPath}
|
|
227
246
|
style="stroke"
|
|
228
|
-
strokeWidth={
|
|
229
|
-
color={
|
|
247
|
+
strokeWidth={loadingStroke}
|
|
248
|
+
color={loadingColor}
|
|
230
249
|
strokeCap="round"
|
|
231
250
|
strokeJoin="round"
|
|
232
251
|
/>
|
|
@@ -255,7 +274,7 @@ export function LoadingOverlay({
|
|
|
255
274
|
width={RECT_W}
|
|
256
275
|
height={RECT_H}
|
|
257
276
|
r={RECT_R}
|
|
258
|
-
color={
|
|
277
|
+
color={loadingColor}
|
|
259
278
|
/>
|
|
260
279
|
<RoundedRect
|
|
261
280
|
x={lx}
|
|
@@ -263,7 +282,7 @@ export function LoadingOverlay({
|
|
|
263
282
|
width={RECT_W}
|
|
264
283
|
height={RECT_H}
|
|
265
284
|
r={RECT_R}
|
|
266
|
-
color={
|
|
285
|
+
color={loadingColor}
|
|
267
286
|
/>
|
|
268
287
|
<RoundedRect
|
|
269
288
|
x={lx}
|
|
@@ -271,7 +290,7 @@ export function LoadingOverlay({
|
|
|
271
290
|
width={RECT_W}
|
|
272
291
|
height={RECT_H}
|
|
273
292
|
r={RECT_R}
|
|
274
|
-
color={
|
|
293
|
+
color={loadingColor}
|
|
275
294
|
/>
|
|
276
295
|
<RoundedRect
|
|
277
296
|
x={lx}
|
|
@@ -279,7 +298,7 @@ export function LoadingOverlay({
|
|
|
279
298
|
width={RECT_W}
|
|
280
299
|
height={RECT_H}
|
|
281
300
|
r={RECT_R}
|
|
282
|
-
color={
|
|
301
|
+
color={loadingColor}
|
|
283
302
|
/>
|
|
284
303
|
|
|
285
304
|
{/* Empty state label */}
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
Path,
|
|
5
5
|
Skia,
|
|
6
6
|
type SkFont,
|
|
7
|
+
type SkRect,
|
|
8
|
+
type SkRSXform,
|
|
7
9
|
} from "@shopify/react-native-skia";
|
|
8
10
|
import { useMemo, useRef, useState } from "react";
|
|
9
11
|
import { PixelRatio } from "react-native";
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
BADGE_TEXT_SCALE,
|
|
21
23
|
buildMarkerAtlas,
|
|
22
24
|
defaultMarkerColor,
|
|
25
|
+
GROUP_BADGE_SIG,
|
|
23
26
|
groupBgSig,
|
|
24
27
|
groupCountText,
|
|
25
28
|
groupGlyphSig,
|
|
@@ -47,6 +50,77 @@ import type {
|
|
|
47
50
|
|
|
48
51
|
const OFF = -9999;
|
|
49
52
|
|
|
53
|
+
/** Size of the `showGroupCount` corner badge relative to a full count badge. */
|
|
54
|
+
const GROUP_CORNER_SCALE = 0.62;
|
|
55
|
+
/** How far the corner count badge sits toward the glyph's top-right corner
|
|
56
|
+
* (1 = centered exactly on the corner; < 1 pulls it inward). */
|
|
57
|
+
const GROUP_CORNER_INSET = 0.82;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Append a collapsed-cluster count badge (round bg + centered, proportionally
|
|
61
|
+
* kerned digits) to the atlas blit lists at `(cx, cy)`, scaled by `mul` — full
|
|
62
|
+
* size as the default group badge, shrunk in a glyph's corner for `showGroupCount`.
|
|
63
|
+
*
|
|
64
|
+
* A **module-level** worklet (not a closure inside the per-frame derived value):
|
|
65
|
+
* the `transforms` / `sprites` arrays are passed in by reference so the pushes land
|
|
66
|
+
* on the real per-frame lists. A nested `"worklet"` closure would capture them by
|
|
67
|
+
* serialized copy, so its pushes would be silently dropped and the badge never drawn.
|
|
68
|
+
*/
|
|
69
|
+
function pushGroupCountBadge(
|
|
70
|
+
transforms: SkRSXform[],
|
|
71
|
+
sprites: SkRect[],
|
|
72
|
+
cells: Record<string, AtlasCell>,
|
|
73
|
+
digitWidths: Record<string, number>,
|
|
74
|
+
invScale: number,
|
|
75
|
+
cx: number,
|
|
76
|
+
cy: number,
|
|
77
|
+
repColor: string,
|
|
78
|
+
count: number,
|
|
79
|
+
mul: number,
|
|
80
|
+
): void {
|
|
81
|
+
"worklet";
|
|
82
|
+
const text = groupCountText(count);
|
|
83
|
+
const bg = cells[groupBgSig(repColor)];
|
|
84
|
+
if (bg) {
|
|
85
|
+
transforms.push(
|
|
86
|
+
Skia.RSXform(invScale * mul, 0, cx - (bg.w * mul) / 2, cy - (bg.h * mul) / 2),
|
|
87
|
+
);
|
|
88
|
+
sprites.push(bg.rect);
|
|
89
|
+
}
|
|
90
|
+
// Lay digits out proportionally (each by its own ink width), scaled to fit the
|
|
91
|
+
// small round badge, centered. A negative kern (fraction of a digit) closes the
|
|
92
|
+
// font's side-bearing gap so "12" reads tight, not "1 2".
|
|
93
|
+
const S = BADGE_TEXT_SCALE;
|
|
94
|
+
let maxDW = 1;
|
|
95
|
+
for (let d = 0; d < 10; d++) {
|
|
96
|
+
const dw = digitWidths["" + d] ?? 0;
|
|
97
|
+
if (dw > maxDW) maxDW = dw;
|
|
98
|
+
}
|
|
99
|
+
const kern = -maxDW * S * 0.42;
|
|
100
|
+
let totalW = 0;
|
|
101
|
+
for (let c = 0; c < text.length; c++) {
|
|
102
|
+
totalW += (digitWidths[text[c]] ?? 0) * S + (c > 0 ? kern : 0);
|
|
103
|
+
}
|
|
104
|
+
let dx = cx - (totalW * mul) / 2;
|
|
105
|
+
for (let c = 0; c < text.length; c++) {
|
|
106
|
+
const w = (digitWidths[text[c]] ?? 0) * S * mul;
|
|
107
|
+
const gc = cells[groupGlyphSig(text[c])];
|
|
108
|
+
if (gc) {
|
|
109
|
+
const gcx = dx + w / 2;
|
|
110
|
+
transforms.push(
|
|
111
|
+
Skia.RSXform(
|
|
112
|
+
invScale * S * mul,
|
|
113
|
+
0,
|
|
114
|
+
gcx - (gc.w * S * mul) / 2,
|
|
115
|
+
cy - (gc.h * S * mul) / 2,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
sprites.push(gc.rect);
|
|
119
|
+
}
|
|
120
|
+
dx += w + kern * mul;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
50
124
|
/**
|
|
51
125
|
* Self-projecting fallback for the axis-anchored kinds (graduation stem + flag,
|
|
52
126
|
* clawback box) that can't be fixed-size atlas sprites. Rare, so the per-glyph
|
|
@@ -246,6 +320,14 @@ export function MarkerOverlay({
|
|
|
246
320
|
// retina canvases instead of being upscaled from a logical-sized texture.
|
|
247
321
|
const dpr = PixelRatio.get();
|
|
248
322
|
const clusterStacked = cluster.mode === "stacked";
|
|
323
|
+
// A dedicated group badge (object form) bakes one extra atlas cell; its image
|
|
324
|
+
// identity + styling drive the rebuild key alongside the marker appearances.
|
|
325
|
+
const groupBadgeCfg =
|
|
326
|
+
typeof cluster.groupBadge === "object" ? cluster.groupBadge : undefined;
|
|
327
|
+
const groupBadgeImage = groupBadgeCfg?.image;
|
|
328
|
+
const groupBadgeKey = groupBadgeCfg
|
|
329
|
+
? `${groupBadgeCfg.icon ?? ""}|${groupBadgeCfg.color ?? ""}|${groupBadgeCfg.pill ? 1 : 0}|${groupBadgeCfg.size ?? ""}`
|
|
330
|
+
: "";
|
|
249
331
|
const atlas = useMemo(
|
|
250
332
|
() =>
|
|
251
333
|
buildMarkerAtlas(
|
|
@@ -254,9 +336,10 @@ export function MarkerOverlay({
|
|
|
254
336
|
font,
|
|
255
337
|
dpr,
|
|
256
338
|
clusterStacked,
|
|
339
|
+
groupBadgeCfg,
|
|
257
340
|
),
|
|
258
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- appearanceKey/paletteKey capture the inputs that change cell pixels
|
|
259
|
-
[appearanceKey, paletteKey, font, dpr, clusterStacked],
|
|
341
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- appearanceKey/paletteKey/groupBadgeKey capture the inputs that change cell pixels
|
|
342
|
+
[appearanceKey, paletteKey, font, dpr, clusterStacked, groupBadgeKey, groupBadgeImage],
|
|
260
343
|
);
|
|
261
344
|
const cells: Record<string, AtlasCell> = atlas.cells;
|
|
262
345
|
const atlasImage = atlas.image;
|
|
@@ -301,8 +384,8 @@ export function MarkerOverlay({
|
|
|
301
384
|
lineLinear,
|
|
302
385
|
});
|
|
303
386
|
clusterMarkers(ms, buf, { config: cluster });
|
|
304
|
-
const transforms = [];
|
|
305
|
-
const sprites = [];
|
|
387
|
+
const transforms: SkRSXform[] = [];
|
|
388
|
+
const sprites: SkRect[] = [];
|
|
306
389
|
for (let i = 0; i < ms.length; i++) {
|
|
307
390
|
const pt = buf[i];
|
|
308
391
|
// Collapsed-cluster members fold into their representative's badge.
|
|
@@ -311,46 +394,61 @@ export function MarkerOverlay({
|
|
|
311
394
|
if (isConnectorMarker(m)) continue;
|
|
312
395
|
// Custom-rendered markers are floated as RN views, not drawn here.
|
|
313
396
|
if (customIds[m.id]) continue;
|
|
314
|
-
// Collapsed cluster
|
|
315
|
-
//
|
|
397
|
+
// Collapsed cluster. By default a round count badge; with
|
|
398
|
+
// `groupBadge: "marker"` the representative marker's own glyph, or with a
|
|
399
|
+
// `MarkerGroupBadge` object a dedicated baked badge — either optionally
|
|
400
|
+
// carrying a corner count, all within this one `drawAtlas`.
|
|
316
401
|
if (pt.isGrouped) {
|
|
317
402
|
const repColor = m.color ?? defaultMarkerColor(m.kind, palette);
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
403
|
+
const gb = cluster.groupBadge;
|
|
404
|
+
// Pick the non-count glyph cell: a dedicated group badge (object form)
|
|
405
|
+
// or the representative marker's own appearance.
|
|
406
|
+
const glyphCell =
|
|
407
|
+
typeof gb === "object" && gb !== null
|
|
408
|
+
? cells[GROUP_BADGE_SIG]
|
|
409
|
+
: gb === "marker"
|
|
410
|
+
? cells[markerAppearanceSig(m)]
|
|
411
|
+
: undefined;
|
|
412
|
+
if (glyphCell) {
|
|
321
413
|
transforms.push(
|
|
322
|
-
Skia.RSXform(
|
|
414
|
+
Skia.RSXform(
|
|
415
|
+
invScale,
|
|
416
|
+
0,
|
|
417
|
+
pt.x - glyphCell.w / 2,
|
|
418
|
+
pt.y - glyphCell.h / 2,
|
|
419
|
+
),
|
|
323
420
|
);
|
|
324
|
-
sprites.push(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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),
|
|
421
|
+
sprites.push(glyphCell.rect);
|
|
422
|
+
if (cluster.showGroupCount) {
|
|
423
|
+
pushGroupCountBadge(
|
|
424
|
+
transforms,
|
|
425
|
+
sprites,
|
|
426
|
+
cells,
|
|
427
|
+
digitWidths,
|
|
428
|
+
invScale,
|
|
429
|
+
pt.x + (glyphCell.w / 2) * GROUP_CORNER_INSET,
|
|
430
|
+
pt.y - (glyphCell.h / 2) * GROUP_CORNER_INSET,
|
|
431
|
+
repColor,
|
|
432
|
+
pt.groupCount,
|
|
433
|
+
GROUP_CORNER_SCALE,
|
|
349
434
|
);
|
|
350
|
-
sprites.push(gc.rect);
|
|
351
435
|
}
|
|
352
|
-
|
|
436
|
+
continue;
|
|
353
437
|
}
|
|
438
|
+
// Default — or fall back here when a non-count badge has no baked cell
|
|
439
|
+
// (e.g. an empty group-badge config): the round count badge.
|
|
440
|
+
pushGroupCountBadge(
|
|
441
|
+
transforms,
|
|
442
|
+
sprites,
|
|
443
|
+
cells,
|
|
444
|
+
digitWidths,
|
|
445
|
+
invScale,
|
|
446
|
+
pt.x,
|
|
447
|
+
pt.y,
|
|
448
|
+
repColor,
|
|
449
|
+
pt.groupCount,
|
|
450
|
+
1,
|
|
451
|
+
);
|
|
354
452
|
continue;
|
|
355
453
|
}
|
|
356
454
|
const cell = cells[markerAppearanceSig(m)];
|
package/src/constants.ts
CHANGED
|
@@ -31,6 +31,23 @@ export const HOLD_TO_SCRUB_MS = 500;
|
|
|
31
31
|
*/
|
|
32
32
|
export const RETURN_TO_LIVE_MS = 450;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Duration (ms) of the fade applied to annotation overlays (markers + reference
|
|
36
|
+
* lines) when `scrub.hideOverlaysOnScrub` is on — how long they take to ease out as a
|
|
37
|
+
* scrub starts and back in on release. Driven by the scrub-active flag, eased on
|
|
38
|
+
* the UI thread. Shared by `LiveChart` and `LiveChartSeries`.
|
|
39
|
+
*/
|
|
40
|
+
export const SCRUB_OVERLAY_FADE_MS = 150;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Base wave amplitude (px) of the breathing loading squiggle — it breathes
|
|
44
|
+
* between `0.4×` and `1.0×` this. Overridable via `loading={{ amplitude }}`.
|
|
45
|
+
*/
|
|
46
|
+
export const LOADING_WAVE_AMPLITUDE = 14;
|
|
47
|
+
|
|
48
|
+
/** Default breathing-wave speed multiplier (`1` = built-in cadence). */
|
|
49
|
+
export const LOADING_WAVE_SPEED = 1;
|
|
50
|
+
|
|
34
51
|
// ─── Metric default tokens (single source of truth for LiveChartMetrics) ─────
|
|
35
52
|
// The resolved `metrics` config (see resolveMetrics) is assembled from these
|
|
36
53
|
// objects. Draw/worklet helpers default their metric params to the matching
|