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
|
@@ -43,6 +43,7 @@ export function CrosshairOverlay({
|
|
|
43
43
|
tooltipBorderRadius = 5,
|
|
44
44
|
tooltipShowValue = true,
|
|
45
45
|
tooltipShowTime = true,
|
|
46
|
+
opaqueCanvas = false,
|
|
46
47
|
}: {
|
|
47
48
|
scrubX: SharedValue<number>;
|
|
48
49
|
crosshairOpacity: SharedValue<number>;
|
|
@@ -94,6 +95,8 @@ export function CrosshairOverlay({
|
|
|
94
95
|
tooltipShowValue?: boolean;
|
|
95
96
|
/** Draw the time row of the default tooltip body. Default true. */
|
|
96
97
|
tooltipShowTime?: boolean;
|
|
98
|
+
/** Paint the owned background instead of erasing destination alpha. */
|
|
99
|
+
opaqueCanvas?: boolean;
|
|
97
100
|
}) {
|
|
98
101
|
// Explicit dependency arrays: with React Compiler enabled, Reanimated's
|
|
99
102
|
// auto-detected worklet dependencies can change array size between renders
|
|
@@ -101,18 +104,15 @@ export function CrosshairOverlay({
|
|
|
101
104
|
// React's "final argument changed size between renders" error. Listing the
|
|
102
105
|
// captured plain values keeps the dependency array a constant size. SharedValue
|
|
103
106
|
// reads stay reactive regardless of this list.
|
|
104
|
-
const p1 = useDerivedValue(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
},
|
|
114
|
-
[scrubX, padding.top, lineTop],
|
|
115
|
-
);
|
|
107
|
+
const p1 = useDerivedValue(() => {
|
|
108
|
+
// A top-pinned custom tooltip pushes the line's start down to its measured
|
|
109
|
+
// bottom (lineTop) so the line stops at the label; -1 → no top tooltip.
|
|
110
|
+
const lt = lineTop?.value ?? -1;
|
|
111
|
+
return {
|
|
112
|
+
x: scrubX.value,
|
|
113
|
+
y: lt >= 0 ? lt : padding.top,
|
|
114
|
+
};
|
|
115
|
+
}, [scrubX, padding.top, lineTop]);
|
|
116
116
|
const p2 = useDerivedValue(
|
|
117
117
|
() => ({
|
|
118
118
|
x: scrubX.value,
|
|
@@ -153,6 +153,11 @@ export function CrosshairOverlay({
|
|
|
153
153
|
() => `rgba(0,0,0,${(1 - dimOpacity) * crosshairOpacity.value})`,
|
|
154
154
|
[dimOpacity, crosshairOpacity],
|
|
155
155
|
);
|
|
156
|
+
const opaqueDimOpacity = useDerivedValue(
|
|
157
|
+
() => (1 - dimOpacity) * crosshairOpacity.value,
|
|
158
|
+
[dimOpacity, crosshairOpacity],
|
|
159
|
+
);
|
|
160
|
+
const backgroundColor = `rgb(${palette.bgRgb[0]},${palette.bgRgb[1]},${palette.bgRgb[2]})`;
|
|
156
161
|
|
|
157
162
|
return (
|
|
158
163
|
<>
|
|
@@ -167,6 +172,15 @@ export function CrosshairOverlay({
|
|
|
167
172
|
color={crosshairDimColor}
|
|
168
173
|
/>
|
|
169
174
|
</Group>
|
|
175
|
+
) : dimOpacity < 1 && opaqueCanvas ? (
|
|
176
|
+
<Rect
|
|
177
|
+
x={scrubX}
|
|
178
|
+
y={padding.top}
|
|
179
|
+
width={dimWidth}
|
|
180
|
+
height={dimHeight}
|
|
181
|
+
color={backgroundColor}
|
|
182
|
+
opacity={opaqueDimOpacity}
|
|
183
|
+
/>
|
|
170
184
|
) : dimOpacity < 1 ? (
|
|
171
185
|
// Erase the trailing content's alpha so it fades to the real background
|
|
172
186
|
// (works on any background color, unlike a colored mask).
|
|
@@ -203,29 +217,28 @@ export function CrosshairOverlay({
|
|
|
203
217
|
/>
|
|
204
218
|
|
|
205
219
|
{showTooltip && (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
220
|
+
<>
|
|
221
|
+
<RoundedRect
|
|
222
|
+
x={tipX}
|
|
223
|
+
y={tipY}
|
|
224
|
+
width={tipW}
|
|
225
|
+
height={tipH}
|
|
226
|
+
r={tooltipBorderRadius}
|
|
227
|
+
color={tooltipBackground ?? palette.tooltipBg}
|
|
228
|
+
/>
|
|
215
229
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
<RoundedRect
|
|
231
|
+
x={tipX}
|
|
232
|
+
y={tipY}
|
|
233
|
+
width={tipW}
|
|
234
|
+
height={tipH}
|
|
235
|
+
r={tooltipBorderRadius}
|
|
236
|
+
color={tooltipBorderColor ?? palette.tooltipBorder}
|
|
237
|
+
style="stroke"
|
|
238
|
+
strokeWidth={1}
|
|
239
|
+
/>
|
|
226
240
|
|
|
227
|
-
|
|
228
|
-
renderTooltip?.() ?? (
|
|
241
|
+
{children ?? renderTooltip?.() ?? (
|
|
229
242
|
<Group>
|
|
230
243
|
{tooltipShowValue && (
|
|
231
244
|
<SkiaText
|
|
@@ -247,8 +260,8 @@ export function CrosshairOverlay({
|
|
|
247
260
|
)}
|
|
248
261
|
</Group>
|
|
249
262
|
)}
|
|
250
|
-
|
|
251
|
-
|
|
263
|
+
</>
|
|
264
|
+
)}
|
|
252
265
|
</Group>
|
|
253
266
|
</>
|
|
254
267
|
);
|
|
@@ -12,6 +12,7 @@ import { computeScrubDotY } from "../hooks/crosshairShared";
|
|
|
12
12
|
import {
|
|
13
13
|
classifyReferenceEdge,
|
|
14
14
|
referenceLineForm,
|
|
15
|
+
referenceLineReactKeys,
|
|
15
16
|
resolveReferenceBadge,
|
|
16
17
|
} from "../math/referenceLines";
|
|
17
18
|
import type { ReferenceLine, ReferenceLineRenderProps } from "../types";
|
|
@@ -39,20 +40,24 @@ function stub<T>(v: T): SharedValue<T> {
|
|
|
39
40
|
return { value: v, get: () => v } as unknown as SharedValue<T>;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/** Whether a custom tag owns every state or only an off-axis tag. */
|
|
44
|
+
export type CustomReferenceLineMode = "always" | "off-axis";
|
|
45
|
+
|
|
42
46
|
/**
|
|
43
|
-
* Which Form-A reference lines a
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* the marker-atlas exclusion set). Index-aligned with `lines`.
|
|
47
|
+
* Which Form-A reference lines a custom renderer returns an element for. This
|
|
48
|
+
* initial probe determines a line's static eligibility only; `"off-axis"` still
|
|
49
|
+
* switches the built-in tag and the RN tag on the UI thread as the edge changes.
|
|
50
|
+
* Index-aligned with `lines`.
|
|
48
51
|
*/
|
|
49
52
|
export function customReferenceLineFlags(
|
|
50
53
|
lines: ReferenceLine[],
|
|
51
54
|
render?: (
|
|
52
55
|
ctx: ReferenceLineRenderProps,
|
|
53
56
|
) => React.ReactElement | null | undefined,
|
|
57
|
+
mode: CustomReferenceLineMode = "always",
|
|
54
58
|
): boolean[] {
|
|
55
59
|
if (!render) return lines.map(() => false);
|
|
60
|
+
const offAxisOnly = mode === "off-axis";
|
|
56
61
|
return lines.map((line, index) => {
|
|
57
62
|
if (referenceLineForm(line) !== "line") return false;
|
|
58
63
|
return (
|
|
@@ -62,8 +67,10 @@ export function customReferenceLineFlags(
|
|
|
62
67
|
value: stub(line.value ?? 0),
|
|
63
68
|
valueStr: stub(""),
|
|
64
69
|
y: stub(-1),
|
|
65
|
-
inRange: stub(
|
|
66
|
-
edge: stub<"above" | "in" | "below">(
|
|
70
|
+
inRange: stub(!offAxisOnly),
|
|
71
|
+
edge: stub<"above" | "in" | "below">(
|
|
72
|
+
offAxisOnly ? "above" : "in",
|
|
73
|
+
),
|
|
67
74
|
dragging: stub(false),
|
|
68
75
|
}) != null
|
|
69
76
|
);
|
|
@@ -88,6 +95,8 @@ function CustomReferenceLineView({
|
|
|
88
95
|
formatValue,
|
|
89
96
|
dragValues,
|
|
90
97
|
dragActive,
|
|
98
|
+
tagWidths,
|
|
99
|
+
offAxisOnly,
|
|
91
100
|
}: {
|
|
92
101
|
line: ReferenceLine;
|
|
93
102
|
index: number;
|
|
@@ -97,14 +106,20 @@ function CustomReferenceLineView({
|
|
|
97
106
|
engine: ChartEngineLayout;
|
|
98
107
|
padding: ChartPadding;
|
|
99
108
|
formatValue: (v: number) => string;
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
/** Optional live overrides used by draggable single-series lines. */
|
|
110
|
+
dragValues?: SharedValue<number[]>;
|
|
111
|
+
/** Optional drag state used by draggable single-series lines. */
|
|
112
|
+
dragActive?: SharedValue<boolean[]>;
|
|
113
|
+
/** Measured custom-tag widths, index-aligned for the Skia connector. */
|
|
114
|
+
tagWidths?: SharedValue<number[]>;
|
|
115
|
+
/** Show this custom tag only while its line is pinned above or below the plot. */
|
|
116
|
+
offAxisOnly: boolean;
|
|
102
117
|
}) {
|
|
103
118
|
const staticValue = line.value ?? 0;
|
|
104
119
|
|
|
105
120
|
// Live value: a drag override (if present) else the static prop value.
|
|
106
121
|
const value = useDerivedValue<number>(() => {
|
|
107
|
-
const dv = dragValues
|
|
122
|
+
const dv = dragValues?.get()[index];
|
|
108
123
|
return dv != null ? dv : staticValue;
|
|
109
124
|
});
|
|
110
125
|
const valueStr = useDerivedValue(() => formatValue(value.get()));
|
|
@@ -116,12 +131,18 @@ function CustomReferenceLineView({
|
|
|
116
131
|
),
|
|
117
132
|
);
|
|
118
133
|
const inRange = useDerivedValue(() => edge.get() === "in");
|
|
119
|
-
const dragging = useDerivedValue(
|
|
134
|
+
const dragging = useDerivedValue(
|
|
135
|
+
() => dragActive?.get()[index] === true,
|
|
136
|
+
);
|
|
120
137
|
|
|
121
138
|
// Canvas Y of the value (the element's vertical center). Off-screen values pin
|
|
122
139
|
// to the nearest plot edge (inset), matching the built-in off-axis badge.
|
|
123
140
|
const y = useDerivedValue(() => {
|
|
124
141
|
const ch = engine.canvasHeight.get();
|
|
142
|
+
// `computeScrubDotY` also returns negative values for legitimate prices above
|
|
143
|
+
// the visible range, so use the layout dimensions—not the projected Y—to
|
|
144
|
+
// distinguish an unmeasured canvas from an off-axis line.
|
|
145
|
+
if (ch - padding.top - padding.bottom <= 0) return -1;
|
|
125
146
|
const raw = computeScrubDotY(
|
|
126
147
|
value.get(),
|
|
127
148
|
engine.displayMin.get(),
|
|
@@ -130,7 +151,6 @@ function CustomReferenceLineView({
|
|
|
130
151
|
padding.top,
|
|
131
152
|
padding.bottom,
|
|
132
153
|
);
|
|
133
|
-
if (raw < 0) return -1;
|
|
134
154
|
const top = padding.top + EDGE_INSET;
|
|
135
155
|
const bottom = ch - padding.bottom - EDGE_INSET;
|
|
136
156
|
return Math.min(bottom, Math.max(top, raw));
|
|
@@ -140,7 +160,14 @@ function CustomReferenceLineView({
|
|
|
140
160
|
const size = useSharedValue({ width: 0, height: 0 });
|
|
141
161
|
const onLayout = (e: LayoutChangeEvent) => {
|
|
142
162
|
const { width, height } = e.nativeEvent.layout;
|
|
143
|
-
size.
|
|
163
|
+
size.set({ width, height });
|
|
164
|
+
if (tagWidths) {
|
|
165
|
+
const previous = tagWidths.get();
|
|
166
|
+
if (previous[index] === width) return;
|
|
167
|
+
const widths = previous.slice();
|
|
168
|
+
widths[index] = width;
|
|
169
|
+
tagWidths.set(widths);
|
|
170
|
+
}
|
|
144
171
|
};
|
|
145
172
|
|
|
146
173
|
const anchor = resolveAnchor(line);
|
|
@@ -148,7 +175,7 @@ function CustomReferenceLineView({
|
|
|
148
175
|
const yy = y.get();
|
|
149
176
|
const s = size.get();
|
|
150
177
|
const w = engine.canvasWidth.get();
|
|
151
|
-
const visible = yy >= 0 && w > 0;
|
|
178
|
+
const visible = yy >= 0 && w > 0 && (!offAxisOnly || edge.get() !== "in");
|
|
152
179
|
const x1 = padding.left;
|
|
153
180
|
const x2 = w - padding.right;
|
|
154
181
|
let tx: number;
|
|
@@ -188,9 +215,9 @@ function CustomReferenceLineView({
|
|
|
188
215
|
* React Native overlay (NOT Skia) that floats `renderReferenceLine` elements over
|
|
189
216
|
* the canvas, one per Form-A line the consumer customizes. Rendered as a sibling
|
|
190
217
|
* of `<Canvas>` (like {@link CustomMarkerOverlay}) so the tags can be any RN view
|
|
191
|
-
* and stay crisp at native resolution. Lines whose render returns an element
|
|
192
|
-
* their built-in
|
|
193
|
-
*
|
|
218
|
+
* and stay crisp at native resolution. Lines whose render returns an element hide
|
|
219
|
+
* their built-in pill / label upstream (see {@link customReferenceLineFlags}); a
|
|
220
|
+
* badged line's Skia connector remains, measured against the native tag's width.
|
|
194
221
|
*/
|
|
195
222
|
export function CustomReferenceLineOverlay({
|
|
196
223
|
lines,
|
|
@@ -201,6 +228,8 @@ export function CustomReferenceLineOverlay({
|
|
|
201
228
|
formatValue,
|
|
202
229
|
dragValues,
|
|
203
230
|
dragActive,
|
|
231
|
+
tagWidths,
|
|
232
|
+
offAxisOnly = false,
|
|
204
233
|
}: {
|
|
205
234
|
lines: ReferenceLine[];
|
|
206
235
|
renderReferenceLine: (
|
|
@@ -215,15 +244,22 @@ export function CustomReferenceLineOverlay({
|
|
|
215
244
|
engine: ChartEngineLayout;
|
|
216
245
|
padding: ChartPadding;
|
|
217
246
|
formatValue: (v: number) => string;
|
|
218
|
-
|
|
219
|
-
|
|
247
|
+
/** Optional live overrides used by draggable single-series lines. */
|
|
248
|
+
dragValues?: SharedValue<number[]>;
|
|
249
|
+
/** Optional drag state used by draggable single-series lines. */
|
|
250
|
+
dragActive?: SharedValue<boolean[]>;
|
|
251
|
+
/** Measured custom-tag widths, index-aligned for the Skia connector. */
|
|
252
|
+
tagWidths?: SharedValue<number[]>;
|
|
253
|
+
/** Keep the built-in in-range tag and show this RN tag only off-axis. */
|
|
254
|
+
offAxisOnly?: boolean;
|
|
220
255
|
}) {
|
|
221
256
|
const children: React.ReactElement[] = [];
|
|
257
|
+
const lineKeys = referenceLineReactKeys(lines);
|
|
222
258
|
for (let i = 0; i < lines.length; i++) {
|
|
223
259
|
if (!custom[i]) continue;
|
|
224
260
|
children.push(
|
|
225
261
|
<CustomReferenceLineView
|
|
226
|
-
key={i}
|
|
262
|
+
key={lineKeys[i]}
|
|
227
263
|
line={lines[i]}
|
|
228
264
|
index={i}
|
|
229
265
|
render={renderReferenceLine}
|
|
@@ -232,6 +268,8 @@ export function CustomReferenceLineOverlay({
|
|
|
232
268
|
formatValue={formatValue}
|
|
233
269
|
dragValues={dragValues}
|
|
234
270
|
dragActive={dragActive}
|
|
271
|
+
tagWidths={tagWidths}
|
|
272
|
+
offAxisOnly={offAxisOnly}
|
|
235
273
|
/>,
|
|
236
274
|
);
|
|
237
275
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type SkRSXform,
|
|
6
6
|
type SkRect,
|
|
7
7
|
} from "@shopify/react-native-skia";
|
|
8
|
+
import { useRef } from "react";
|
|
8
9
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
9
10
|
import {
|
|
10
11
|
buildParticleInstances,
|
|
@@ -63,14 +64,46 @@ export function DegenParticlesOverlay({
|
|
|
63
64
|
// memoizes this on `colorList`.
|
|
64
65
|
const colorRgb = colorList.map((c) => parseColorRgb(c));
|
|
65
66
|
|
|
67
|
+
const poolRef = useRef<{
|
|
68
|
+
a: { transforms: SkRSXform[]; sprites: SkRect[]; colors: SkColor[] };
|
|
69
|
+
b: { transforms: SkRSXform[]; sprites: SkRect[]; colors: SkColor[] };
|
|
70
|
+
tick: boolean;
|
|
71
|
+
instances: ReturnType<typeof buildParticleInstances>;
|
|
72
|
+
instancePool: ReturnType<typeof buildParticleInstances>;
|
|
73
|
+
spriteRect: SkRect;
|
|
74
|
+
} | null>(null);
|
|
75
|
+
if (poolRef.current === null) {
|
|
76
|
+
const instancePool = Array.from({ length: particleSlotCount }, () => ({
|
|
77
|
+
x: 0,
|
|
78
|
+
y: 0,
|
|
79
|
+
scale: 0,
|
|
80
|
+
alpha: 0,
|
|
81
|
+
colorIndex: 0,
|
|
82
|
+
}));
|
|
83
|
+
poolRef.current = {
|
|
84
|
+
a: { transforms: [], sprites: [], colors: [] },
|
|
85
|
+
b: { transforms: [], sprites: [], colors: [] },
|
|
86
|
+
tick: false,
|
|
87
|
+
instances: [],
|
|
88
|
+
instancePool,
|
|
89
|
+
spriteRect: Skia.XYWHRect(0, 0, sprite.size, sprite.size),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
66
93
|
// Single per-frame worklet. Reads `packRevision` so it re-runs each frame
|
|
67
94
|
// while a burst is alive (the pack buffer is mutated in place, so its
|
|
68
95
|
// reference is stable and wouldn't otherwise re-notify).
|
|
69
96
|
const atlasData = useDerivedValue(() => {
|
|
70
97
|
const rev = packRevision.get();
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
const
|
|
98
|
+
const pool = poolRef.current!;
|
|
99
|
+
pool.tick = !pool.tick;
|
|
100
|
+
const frame = pool.tick ? pool.a : pool.b;
|
|
101
|
+
const transforms = frame.transforms;
|
|
102
|
+
const sprites = frame.sprites;
|
|
103
|
+
const colorsOut = frame.colors;
|
|
104
|
+
transforms.length = 0;
|
|
105
|
+
sprites.length = 0;
|
|
106
|
+
colorsOut.length = 0;
|
|
74
107
|
if (rev >= 0) {
|
|
75
108
|
const instances = buildParticleInstances(
|
|
76
109
|
pack.get(),
|
|
@@ -79,6 +112,8 @@ export function DegenParticlesOverlay({
|
|
|
79
112
|
particleBurstDurationSec,
|
|
80
113
|
particleOpacity,
|
|
81
114
|
sprite.radius,
|
|
115
|
+
pool.instances,
|
|
116
|
+
pool.instancePool,
|
|
82
117
|
);
|
|
83
118
|
const half = sprite.size / 2;
|
|
84
119
|
for (let i = 0; i < instances.length; i++) {
|
|
@@ -92,12 +127,12 @@ export function DegenParticlesOverlay({
|
|
|
92
127
|
inst.y - inst.scale * half,
|
|
93
128
|
),
|
|
94
129
|
);
|
|
95
|
-
sprites.push(
|
|
130
|
+
sprites.push(pool.spriteRect);
|
|
96
131
|
const [r, g, b] = colorRgb[inst.colorIndex % colorRgb.length];
|
|
97
132
|
colorsOut.push(Skia.Color(`rgba(${r}, ${g}, ${b}, ${inst.alpha})`));
|
|
98
133
|
}
|
|
99
134
|
}
|
|
100
|
-
return
|
|
135
|
+
return frame;
|
|
101
136
|
}, [
|
|
102
137
|
pack,
|
|
103
138
|
packRevision,
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
1
2
|
import { Circle, Group } from "@shopify/react-native-skia";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useDerivedValue,
|
|
5
|
+
useFrameCallback,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
type SharedValue,
|
|
8
|
+
} from "react-native-reanimated";
|
|
3
9
|
import type {
|
|
4
10
|
ResolvedDotRingConfig,
|
|
5
11
|
ResolvedPulseConfig,
|
|
6
12
|
} from "../core/resolveConfig";
|
|
7
13
|
import type { LiveChartPalette } from "../types";
|
|
8
|
-
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
9
14
|
|
|
10
15
|
const MIN_PULSE_RADIUS = 9;
|
|
11
16
|
|
|
@@ -19,7 +24,6 @@ export function DotOverlay({
|
|
|
19
24
|
dotX,
|
|
20
25
|
dotY,
|
|
21
26
|
palette,
|
|
22
|
-
engine,
|
|
23
27
|
pulse,
|
|
24
28
|
radius,
|
|
25
29
|
ring,
|
|
@@ -29,7 +33,6 @@ export function DotOverlay({
|
|
|
29
33
|
dotX: SharedValue<number>;
|
|
30
34
|
dotY: SharedValue<number>;
|
|
31
35
|
palette: LiveChartPalette;
|
|
32
|
-
engine: ChartEngineLayout;
|
|
33
36
|
pulse: ResolvedPulseConfig | null;
|
|
34
37
|
/** Radius of the color-filled dot in pixels. */
|
|
35
38
|
radius: number;
|
|
@@ -39,17 +42,30 @@ export function DotOverlay({
|
|
|
39
42
|
color: string | undefined;
|
|
40
43
|
/**
|
|
41
44
|
* Time-scroll right edge (`null` = following live). While scrolled back the
|
|
42
|
-
* pulse is suppressed
|
|
43
|
-
*
|
|
45
|
+
* pulse is suppressed because a "live" heartbeat on a historical point is
|
|
46
|
+
* misleading.
|
|
44
47
|
*/
|
|
45
48
|
viewEnd?: SharedValue<number | null>;
|
|
46
49
|
}) {
|
|
47
50
|
const dotColor = color ?? palette.line;
|
|
48
51
|
|
|
52
|
+
// Pulse clock: wall time, not `engine.timestamp` — a `nowOverride` freezes
|
|
53
|
+
// the engine clock between data updates, which freezes the pulse with it.
|
|
54
|
+
// Runs only while a pulse is configured (resolvePulse returns null when the
|
|
55
|
+
// chart is static, so a suspended chart burns no frames here).
|
|
56
|
+
const pulseClockMs = useSharedValue(0);
|
|
57
|
+
/* istanbul ignore next -- frame-callback worklet runs on the UI thread, not in Jest */
|
|
58
|
+
const pulseClock = useFrameCallback((frame) => {
|
|
59
|
+
pulseClockMs.value = frame.timestamp;
|
|
60
|
+
}, pulse != null);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
pulseClock.setActive(pulse != null);
|
|
63
|
+
}, [pulse, pulseClock]);
|
|
64
|
+
|
|
49
65
|
const pulseRadius = useDerivedValue(() => {
|
|
50
66
|
if (!pulse) return 0;
|
|
51
67
|
if (viewEnd?.value != null) return 0; // scrolled back — no live pulse
|
|
52
|
-
const nowMs =
|
|
68
|
+
const nowMs = pulseClockMs.value;
|
|
53
69
|
const t = (nowMs % pulse.interval) / pulse.duration;
|
|
54
70
|
if (t >= 1) return 0;
|
|
55
71
|
return MIN_PULSE_RADIUS + t * (pulse.maxRadius - MIN_PULSE_RADIUS);
|
|
@@ -58,7 +74,7 @@ export function DotOverlay({
|
|
|
58
74
|
const pulseOpacity = useDerivedValue(() => {
|
|
59
75
|
if (!pulse) return 0;
|
|
60
76
|
if (viewEnd?.value != null) return 0; // scrolled back — no live pulse
|
|
61
|
-
const nowMs =
|
|
77
|
+
const nowMs = pulseClockMs.value;
|
|
62
78
|
const t = (nowMs % pulse.interval) / pulse.duration;
|
|
63
79
|
if (t >= 1) return 0;
|
|
64
80
|
return pulse.opacity * (1 - t);
|
|
@@ -2,6 +2,7 @@ import { Group, LinearGradient, Rect, vec } from "@shopify/react-native-skia";
|
|
|
2
2
|
|
|
3
3
|
import { useDerivedValue } from "react-native-reanimated";
|
|
4
4
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
5
|
+
import { parseColorRgba } from "../theme";
|
|
5
6
|
|
|
6
7
|
export function LeftEdgeFade({
|
|
7
8
|
paddingLeft,
|
|
@@ -9,27 +10,32 @@ export function LeftEdgeFade({
|
|
|
9
10
|
startColor,
|
|
10
11
|
endColor,
|
|
11
12
|
engine,
|
|
13
|
+
opaqueBackgroundRgb,
|
|
12
14
|
}: {
|
|
13
15
|
paddingLeft: number;
|
|
14
16
|
fadeWidth: number;
|
|
15
17
|
startColor: string;
|
|
16
18
|
endColor: string;
|
|
17
19
|
engine: ChartEngineLayout;
|
|
20
|
+
/** When set, paint the owned background instead of erasing destination alpha. */
|
|
21
|
+
opaqueBackgroundRgb?: [number, number, number];
|
|
18
22
|
}) {
|
|
19
23
|
const rectWidth = paddingLeft + fadeWidth;
|
|
20
24
|
const height = useDerivedValue(() => engine.canvasHeight.value);
|
|
21
25
|
|
|
22
26
|
const gStart = vec(paddingLeft, 0);
|
|
23
27
|
const gEnd = vec(paddingLeft + fadeWidth, 0);
|
|
28
|
+
const colors = opaqueBackgroundRgb
|
|
29
|
+
? [
|
|
30
|
+
`rgba(${opaqueBackgroundRgb[0]},${opaqueBackgroundRgb[1]},${opaqueBackgroundRgb[2]},${parseColorRgba(startColor)[3]})`,
|
|
31
|
+
`rgba(${opaqueBackgroundRgb[0]},${opaqueBackgroundRgb[1]},${opaqueBackgroundRgb[2]},${parseColorRgba(endColor)[3]})`,
|
|
32
|
+
]
|
|
33
|
+
: [startColor, endColor];
|
|
24
34
|
|
|
25
35
|
return (
|
|
26
|
-
<Group blendMode="dstOut">
|
|
36
|
+
<Group blendMode={opaqueBackgroundRgb ? undefined : "dstOut"}>
|
|
27
37
|
<Rect x={0} y={0} width={rectWidth} height={height}>
|
|
28
|
-
<LinearGradient
|
|
29
|
-
start={gStart}
|
|
30
|
-
end={gEnd}
|
|
31
|
-
colors={[startColor, endColor]}
|
|
32
|
-
/>
|
|
38
|
+
<LinearGradient start={gStart} end={gEnd} colors={colors} />
|
|
33
39
|
</Rect>
|
|
34
40
|
</Group>
|
|
35
41
|
);
|