react-native-livechart 4.9.2 → 4.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/components/CrosshairLine.d.ts +3 -1
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +3 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +4 -2
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/LeftEdgeFade.d.ts +3 -1
- package/dist/components/LeftEdgeFade.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/LoadingOverlay.d.ts +3 -1
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +3 -1
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -1
- package/dist/components/MultiSeriesValueLabels.d.ts +3 -1
- package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -1
- package/dist/components/MultiSeriesValueLines.d.ts +3 -1
- package/dist/components/MultiSeriesValueLines.d.ts.map +1 -1
- package/dist/components/ThresholdSplitShader.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +9 -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/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/markerAtlas.d.ts +2 -0
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/draw/particleAtlas.d.ts +3 -2
- package/dist/draw/particleAtlas.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +6 -6
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/delayedPanGuard.d.ts +31 -0
- package/dist/hooks/delayedPanGuard.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +4 -4
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/squiggly.d.ts +6 -3
- package/dist/math/squiggly.d.ts.map +1 -1
- package/dist/math/threshold.d.ts +2 -2
- package/dist/types.d.ts +35 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairLine.tsx +17 -0
- package/src/components/CrosshairOverlay.tsx +54 -36
- package/src/components/CustomReferenceLineOverlay.tsx +16 -7
- package/src/components/DegenParticlesOverlay.tsx +40 -5
- package/src/components/LeftEdgeFade.tsx +12 -6
- package/src/components/LiveChart.tsx +34 -16
- package/src/components/LiveChartSeries.tsx +94 -21
- package/src/components/LoadingOverlay.tsx +26 -12
- package/src/components/MarkerOverlay.tsx +31 -19
- package/src/components/MultiSeriesDots.tsx +4 -2
- package/src/components/MultiSeriesTooltipStack.tsx +4 -2
- package/src/components/MultiSeriesValueLabels.tsx +4 -2
- package/src/components/MultiSeriesValueLines.tsx +4 -2
- package/src/components/ThresholdSplitShader.tsx +5 -37
- package/src/components/YAxisOverlay.tsx +43 -1
- package/src/components/thresholdSplitShaderSource.ts +70 -0
- package/src/core/useLiveChartEngine.ts +110 -42
- package/src/core/useLiveChartSeriesEngine.ts +137 -70
- package/src/draw/markerAtlas.ts +22 -2
- package/src/draw/particleAtlas.ts +21 -10
- package/src/hooks/crosshairShared.ts +24 -18
- package/src/hooks/delayedPanGuard.ts +80 -0
- package/src/hooks/useChartPaths.ts +11 -0
- package/src/hooks/useCrosshair.ts +53 -10
- package/src/hooks/useCrosshairSeries.ts +49 -6
- package/src/hooks/useMultiSeriesLinePaths.ts +15 -9
- package/src/index.ts +1 -0
- package/src/math/squiggly.ts +28 -11
- package/src/math/threshold.ts +2 -2
- package/src/types.ts +40 -5
|
@@ -18,7 +18,11 @@ import {
|
|
|
18
18
|
} from "react-native-reanimated";
|
|
19
19
|
import { MS_PER_FRAME_60FPS, RETURN_TO_LIVE_MS } from "../constants";
|
|
20
20
|
import type { CandlePoint, LiveChartPoint, SeriesConfig } from "../types";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
tickLiveChartEngineFrame,
|
|
23
|
+
type EngineTickInput,
|
|
24
|
+
type EngineTickMutable,
|
|
25
|
+
} from "./liveChartEngineTick";
|
|
22
26
|
|
|
23
27
|
export interface EngineConfig {
|
|
24
28
|
data: SharedValue<LiveChartPoint[]>;
|
|
@@ -229,6 +233,42 @@ export interface EngineFrameRefs {
|
|
|
229
233
|
extremaMaxTime: SharedValue<number>;
|
|
230
234
|
}
|
|
231
235
|
|
|
236
|
+
/** Stable state/input containers reused by the UI-thread frame callback. */
|
|
237
|
+
export interface EngineFrameScratch {
|
|
238
|
+
state: EngineTickMutable;
|
|
239
|
+
input: EngineTickInput;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Allocate one single-series frame scratch. Call once per engine instance. */
|
|
243
|
+
export function makeEngineFrameScratch(): EngineFrameScratch {
|
|
244
|
+
return {
|
|
245
|
+
state: {
|
|
246
|
+
displayValue: 0,
|
|
247
|
+
displayMin: 0,
|
|
248
|
+
displayMax: 1,
|
|
249
|
+
displayWindow: 0,
|
|
250
|
+
timestamp: 0,
|
|
251
|
+
liveEdge: 0,
|
|
252
|
+
edgeValue: 0,
|
|
253
|
+
extremaMinValue: NaN,
|
|
254
|
+
extremaMaxValue: NaN,
|
|
255
|
+
extremaMinTime: NaN,
|
|
256
|
+
extremaMaxTime: NaN,
|
|
257
|
+
},
|
|
258
|
+
input: {
|
|
259
|
+
dt: MS_PER_FRAME_60FPS,
|
|
260
|
+
canvasWidth: 0,
|
|
261
|
+
canvasHeight: 0,
|
|
262
|
+
timeWindow: 0,
|
|
263
|
+
smoothing: 0,
|
|
264
|
+
exaggerate: false,
|
|
265
|
+
referenceValue: undefined,
|
|
266
|
+
targetValue: 0,
|
|
267
|
+
points: [],
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
232
272
|
/**
|
|
233
273
|
* Shared between the `useFrameCallback` worklet and unit tests.
|
|
234
274
|
* Mutates shared values from a snapshot tick (`tickLiveChartEngineFrame`).
|
|
@@ -236,54 +276,77 @@ export interface EngineFrameRefs {
|
|
|
236
276
|
export function applyLiveChartEngineFrame(
|
|
237
277
|
frameInfo: { timeSincePreviousFrame?: number | null },
|
|
238
278
|
sv: EngineFrameRefs,
|
|
279
|
+
scratch?: EngineFrameScratch,
|
|
239
280
|
): void {
|
|
240
281
|
"worklet";
|
|
241
282
|
const dt = frameInfo.timeSincePreviousFrame ?? MS_PER_FRAME_60FPS;
|
|
242
283
|
// One-shot settle: a `snapKey` change set this flag; consume it for this tick
|
|
243
284
|
// and clear it below so only this frame snaps (live ticks stay smoothed).
|
|
244
285
|
const snap = sv.snapSV?.value ?? false;
|
|
245
|
-
const state = {
|
|
246
|
-
displayValue:
|
|
247
|
-
displayMin:
|
|
248
|
-
displayMax:
|
|
249
|
-
displayWindow:
|
|
250
|
-
timestamp:
|
|
251
|
-
liveEdge:
|
|
252
|
-
edgeValue:
|
|
253
|
-
extremaMinValue:
|
|
254
|
-
extremaMaxValue:
|
|
255
|
-
extremaMinTime:
|
|
256
|
-
extremaMaxTime:
|
|
286
|
+
const state: EngineTickMutable = scratch?.state ?? {
|
|
287
|
+
displayValue: 0,
|
|
288
|
+
displayMin: 0,
|
|
289
|
+
displayMax: 1,
|
|
290
|
+
displayWindow: 0,
|
|
291
|
+
timestamp: 0,
|
|
292
|
+
liveEdge: 0,
|
|
293
|
+
edgeValue: 0,
|
|
294
|
+
extremaMinValue: NaN,
|
|
295
|
+
extremaMaxValue: NaN,
|
|
296
|
+
extremaMinTime: NaN,
|
|
297
|
+
extremaMaxTime: NaN,
|
|
257
298
|
};
|
|
258
|
-
|
|
299
|
+
state.displayValue = sv.displayValue.value;
|
|
300
|
+
state.displayMin = sv.displayMin.value;
|
|
301
|
+
state.displayMax = sv.displayMax.value;
|
|
302
|
+
state.displayWindow = sv.displayWindow.value;
|
|
303
|
+
state.timestamp = sv.timestamp.value;
|
|
304
|
+
state.liveEdge = sv.liveEdgeSV?.value ?? 0;
|
|
305
|
+
state.edgeValue = sv.edgeValueSV?.value ?? 0;
|
|
306
|
+
state.extremaMinValue = sv.extremaMinValue.value;
|
|
307
|
+
state.extremaMaxValue = sv.extremaMaxValue.value;
|
|
308
|
+
state.extremaMinTime = sv.extremaMinTime.value;
|
|
309
|
+
state.extremaMaxTime = sv.extremaMaxTime.value;
|
|
310
|
+
|
|
311
|
+
const input: EngineTickInput = scratch?.input ?? {
|
|
259
312
|
dt,
|
|
260
|
-
canvasWidth:
|
|
261
|
-
canvasHeight:
|
|
262
|
-
timeWindow:
|
|
263
|
-
smoothing:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
313
|
+
canvasWidth: 0,
|
|
314
|
+
canvasHeight: 0,
|
|
315
|
+
timeWindow: 0,
|
|
316
|
+
smoothing: 0,
|
|
317
|
+
exaggerate: false,
|
|
318
|
+
referenceValue: undefined,
|
|
319
|
+
targetValue: 0,
|
|
320
|
+
points: [],
|
|
321
|
+
};
|
|
322
|
+
input.dt = dt;
|
|
323
|
+
input.canvasWidth = sv.canvasWidth.value;
|
|
324
|
+
input.canvasHeight = sv.canvasHeight.value;
|
|
325
|
+
input.timeWindow = sv.timeWindow.value;
|
|
326
|
+
input.smoothing = sv.smoothing.value;
|
|
327
|
+
input.adaptiveSpeedBoost = sv.adaptiveSpeedBoostSV?.value;
|
|
328
|
+
input.exaggerate = sv.exaggerateSV.value;
|
|
329
|
+
input.referenceValue = sv.referenceValue.value;
|
|
330
|
+
input.referenceValues = sv.referenceValues?.value;
|
|
331
|
+
input.thresholdRangePoints = sv.thresholdRangePoints?.value;
|
|
332
|
+
input.thresholdRangeExtendToNow = sv.thresholdRangeExtendToNow?.value ?? true;
|
|
333
|
+
input.nonNegative = sv.nonNegativeSV?.value ?? false;
|
|
334
|
+
input.maxValue = sv.maxValueSV?.value;
|
|
335
|
+
input.nowOverride = sv.nowOverrideSV?.value;
|
|
336
|
+
input.windowBuffer = sv.windowBufferSV?.value ?? 0;
|
|
337
|
+
input.targetValue = sv.value.value;
|
|
338
|
+
input.points = sv.data.value;
|
|
339
|
+
input.nowSeconds = Date.now() / 1000;
|
|
340
|
+
input.paused = sv.pausedSV.value;
|
|
341
|
+
input.snap = snap;
|
|
342
|
+
input.viewEnd = sv.viewEndSV?.value;
|
|
343
|
+
input.returnT = sv.returnTSV?.value;
|
|
344
|
+
input.returnFrom = sv.returnFromSV?.value;
|
|
345
|
+
input.viewWindow = sv.viewWindowSV?.value;
|
|
346
|
+
input.mode = sv.modeSV.value;
|
|
347
|
+
input.candles = sv.candles?.value;
|
|
348
|
+
input.liveCandle = sv.liveCandle?.value;
|
|
349
|
+
tickLiveChartEngineFrame(state, input);
|
|
287
350
|
sv.displayValue.value = state.displayValue;
|
|
288
351
|
sv.displayMin.value = state.displayMin;
|
|
289
352
|
sv.displayMax.value = state.displayMax;
|
|
@@ -467,12 +530,16 @@ export function useLiveChartEngine(
|
|
|
467
530
|
extremaMinTime,
|
|
468
531
|
extremaMaxTime,
|
|
469
532
|
};
|
|
533
|
+
const frameScratchRef = useRef<EngineFrameScratch | null>(null);
|
|
534
|
+
if (frameScratchRef.current === null) {
|
|
535
|
+
frameScratchRef.current = makeEngineFrameScratch();
|
|
536
|
+
}
|
|
470
537
|
|
|
471
538
|
// `autostart=false` registers the frame callback without running it — the live
|
|
472
539
|
// loop is fully inert in static mode (the invariant that makes this worth it).
|
|
473
540
|
useFrameCallback((frameInfo) => {
|
|
474
541
|
"worklet";
|
|
475
|
-
applyLiveChartEngineFrame(frameInfo, frameRefs);
|
|
542
|
+
applyLiveChartEngineFrame(frameInfo, frameRefs, frameScratchRef.current!);
|
|
476
543
|
}, !config.static);
|
|
477
544
|
|
|
478
545
|
// When time-scroll is disabled while scrolled back, return the window to the
|
|
@@ -532,6 +599,7 @@ export function useLiveChartEngine(
|
|
|
532
599
|
applyLiveChartEngineFrame(
|
|
533
600
|
{ timeSincePreviousFrame: MS_PER_FRAME_60FPS },
|
|
534
601
|
frameRefs,
|
|
602
|
+
frameScratchRef.current!,
|
|
535
603
|
);
|
|
536
604
|
},
|
|
537
605
|
);
|
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
} from "react-native-reanimated";
|
|
12
12
|
import { MS_PER_FRAME_60FPS, RETURN_TO_LIVE_MS } from "../constants";
|
|
13
13
|
import type { LiveChartPoint, SeriesConfig } from "../types";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
tickLiveChartSeriesEngineFrame,
|
|
16
|
+
type MultiEngineTickInput,
|
|
17
|
+
type MultiEngineTickMutable,
|
|
18
|
+
} from "./liveChartSeriesEngineTick";
|
|
15
19
|
import type { ChartEngineScroll, MultiEngineState } from "./useLiveChartEngine";
|
|
16
20
|
|
|
17
21
|
export interface MultiSeriesEngineConfig {
|
|
@@ -94,10 +98,10 @@ export interface MultiEngineFrameRefs {
|
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
/**
|
|
97
|
-
* Reusable per-frame scratch for {@link applyLiveChartSeriesEngineFrame}. The
|
|
101
|
+
* Reusable per-frame scratch for {@link applyLiveChartSeriesEngineFrame}. The
|
|
98
102
|
* output arrays ping-pong so the assigned reference still changes each frame
|
|
99
|
-
* (Reanimated propagates on reference change)
|
|
100
|
-
*
|
|
103
|
+
* (Reanimated propagates on reference change), while the state/input containers
|
|
104
|
+
* are mutated in place. Create one per engine.
|
|
101
105
|
*/
|
|
102
106
|
export interface MultiSeriesEngineScratch {
|
|
103
107
|
dvA: number[];
|
|
@@ -105,6 +109,46 @@ export interface MultiSeriesEngineScratch {
|
|
|
105
109
|
opA: number[];
|
|
106
110
|
opB: number[];
|
|
107
111
|
tick: boolean;
|
|
112
|
+
state: MultiEngineTickMutable;
|
|
113
|
+
input: MultiEngineTickInput;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Allocate one multi-series frame scratch. Call once per engine instance. */
|
|
117
|
+
export function makeMultiSeriesEngineScratch(): MultiSeriesEngineScratch {
|
|
118
|
+
const dvA: number[] = [];
|
|
119
|
+
const dvB: number[] = [];
|
|
120
|
+
const opA: number[] = [];
|
|
121
|
+
const opB: number[] = [];
|
|
122
|
+
return {
|
|
123
|
+
dvA,
|
|
124
|
+
dvB,
|
|
125
|
+
opA,
|
|
126
|
+
opB,
|
|
127
|
+
tick: false,
|
|
128
|
+
state: {
|
|
129
|
+
displayMin: 0,
|
|
130
|
+
displayMax: 1,
|
|
131
|
+
displayWindow: 0,
|
|
132
|
+
timestamp: 0,
|
|
133
|
+
liveEdge: 0,
|
|
134
|
+
displayValues: dvA,
|
|
135
|
+
opacities: opA,
|
|
136
|
+
extremaMinValue: NaN,
|
|
137
|
+
extremaMaxValue: NaN,
|
|
138
|
+
extremaMinTime: NaN,
|
|
139
|
+
extremaMaxTime: NaN,
|
|
140
|
+
},
|
|
141
|
+
input: {
|
|
142
|
+
dt: MS_PER_FRAME_60FPS,
|
|
143
|
+
canvasWidth: 0,
|
|
144
|
+
canvasHeight: 0,
|
|
145
|
+
timeWindow: 0,
|
|
146
|
+
smoothing: 0,
|
|
147
|
+
exaggerate: false,
|
|
148
|
+
referenceValue: undefined,
|
|
149
|
+
series: [],
|
|
150
|
+
},
|
|
151
|
+
};
|
|
108
152
|
}
|
|
109
153
|
|
|
110
154
|
/**
|
|
@@ -113,8 +157,8 @@ export interface MultiSeriesEngineScratch {
|
|
|
113
157
|
* to lerp per-series display values and opacities. Extracted as a
|
|
114
158
|
* pure function so it can be called from both `useFrameCallback` and tests.
|
|
115
159
|
*
|
|
116
|
-
* Pass `scratch` to reuse the output arrays
|
|
117
|
-
*
|
|
160
|
+
* Pass `scratch` to reuse the output arrays plus state/input containers across
|
|
161
|
+
* frames; omit it (tests) to retain the standalone allocation fallback.
|
|
118
162
|
*/
|
|
119
163
|
export function applyLiveChartSeriesEngineFrame(
|
|
120
164
|
frameInfo: { timeSincePreviousFrame?: number | null },
|
|
@@ -143,42 +187,63 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
143
187
|
displayValues = curDv.slice();
|
|
144
188
|
opacities = curOp.slice();
|
|
145
189
|
}
|
|
146
|
-
const state = {
|
|
147
|
-
displayMin:
|
|
148
|
-
displayMax:
|
|
149
|
-
displayWindow:
|
|
150
|
-
timestamp:
|
|
151
|
-
liveEdge:
|
|
190
|
+
const state: MultiEngineTickMutable = scratch?.state ?? {
|
|
191
|
+
displayMin: 0,
|
|
192
|
+
displayMax: 1,
|
|
193
|
+
displayWindow: 0,
|
|
194
|
+
timestamp: 0,
|
|
195
|
+
liveEdge: 0,
|
|
152
196
|
displayValues,
|
|
153
197
|
opacities,
|
|
154
|
-
extremaMinValue:
|
|
155
|
-
extremaMaxValue:
|
|
156
|
-
extremaMinTime:
|
|
157
|
-
extremaMaxTime:
|
|
198
|
+
extremaMinValue: NaN,
|
|
199
|
+
extremaMaxValue: NaN,
|
|
200
|
+
extremaMinTime: NaN,
|
|
201
|
+
extremaMaxTime: NaN,
|
|
158
202
|
};
|
|
159
|
-
|
|
203
|
+
state.displayMin = sv.displayMin.value;
|
|
204
|
+
state.displayMax = sv.displayMax.value;
|
|
205
|
+
state.displayWindow = sv.displayWindow.value;
|
|
206
|
+
state.timestamp = sv.timestamp.value;
|
|
207
|
+
state.liveEdge = sv.liveEdgeSV?.value ?? 0;
|
|
208
|
+
state.displayValues = displayValues;
|
|
209
|
+
state.opacities = opacities;
|
|
210
|
+
state.extremaMinValue = sv.extremaMinValue.value;
|
|
211
|
+
state.extremaMaxValue = sv.extremaMaxValue.value;
|
|
212
|
+
state.extremaMinTime = sv.extremaMinTime.value;
|
|
213
|
+
state.extremaMaxTime = sv.extremaMaxTime.value;
|
|
214
|
+
|
|
215
|
+
const input: MultiEngineTickInput = scratch?.input ?? {
|
|
160
216
|
dt,
|
|
161
|
-
canvasWidth:
|
|
162
|
-
canvasHeight:
|
|
163
|
-
timeWindow:
|
|
164
|
-
smoothing:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
referenceValue: sv.referenceValue.value,
|
|
168
|
-
referenceValues: sv.referenceValues?.value,
|
|
169
|
-
nonNegative: sv.nonNegativeSV?.value ?? false,
|
|
170
|
-
maxValue: sv.maxValueSV?.value,
|
|
171
|
-
nowOverride: sv.nowOverrideSV?.value,
|
|
172
|
-
windowBuffer: sv.windowBufferSV?.value ?? 0,
|
|
217
|
+
canvasWidth: 0,
|
|
218
|
+
canvasHeight: 0,
|
|
219
|
+
timeWindow: 0,
|
|
220
|
+
smoothing: 0,
|
|
221
|
+
exaggerate: false,
|
|
222
|
+
referenceValue: undefined,
|
|
173
223
|
series: seriesSnap,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
224
|
+
};
|
|
225
|
+
input.dt = dt;
|
|
226
|
+
input.canvasWidth = sv.canvasWidth.value;
|
|
227
|
+
input.canvasHeight = sv.canvasHeight.value;
|
|
228
|
+
input.timeWindow = sv.timeWindow.value;
|
|
229
|
+
input.smoothing = sv.smoothing.value;
|
|
230
|
+
input.adaptiveSpeedBoost = sv.adaptiveSpeedBoostSV?.value;
|
|
231
|
+
input.exaggerate = sv.exaggerateSV.value;
|
|
232
|
+
input.referenceValue = sv.referenceValue.value;
|
|
233
|
+
input.referenceValues = sv.referenceValues?.value;
|
|
234
|
+
input.nonNegative = sv.nonNegativeSV?.value ?? false;
|
|
235
|
+
input.maxValue = sv.maxValueSV?.value;
|
|
236
|
+
input.nowOverride = sv.nowOverrideSV?.value;
|
|
237
|
+
input.windowBuffer = sv.windowBufferSV?.value ?? 0;
|
|
238
|
+
input.series = seriesSnap;
|
|
239
|
+
input.nowSeconds = Date.now() / 1000;
|
|
240
|
+
input.paused = sv.pausedSV.value;
|
|
241
|
+
input.snap = snap;
|
|
242
|
+
input.viewEnd = sv.viewEndSV?.value;
|
|
243
|
+
input.returnT = sv.returnTSV?.value;
|
|
244
|
+
input.returnFrom = sv.returnFromSV?.value;
|
|
245
|
+
input.viewWindow = sv.viewWindowSV?.value;
|
|
246
|
+
tickLiveChartSeriesEngineFrame(state, input);
|
|
182
247
|
sv.displayMin.value = state.displayMin;
|
|
183
248
|
sv.displayMax.value = state.displayMax;
|
|
184
249
|
sv.displayWindow.value = state.displayWindow;
|
|
@@ -272,46 +337,48 @@ export function useLiveChartSeriesEngine(
|
|
|
272
337
|
// Reused per-frame output buffers (ping-ponged) — see applyLiveChartSeriesEngineFrame.
|
|
273
338
|
const scratchRef = useRef<MultiSeriesEngineScratch | null>(null);
|
|
274
339
|
if (scratchRef.current === null) {
|
|
275
|
-
scratchRef.current =
|
|
340
|
+
scratchRef.current = makeMultiSeriesEngineScratch();
|
|
276
341
|
}
|
|
277
342
|
|
|
343
|
+
const frameRefs: MultiEngineFrameRefs = {
|
|
344
|
+
series,
|
|
345
|
+
displaySeriesValues,
|
|
346
|
+
seriesOpacities,
|
|
347
|
+
displayMin,
|
|
348
|
+
displayMax,
|
|
349
|
+
displayWindow,
|
|
350
|
+
timestamp,
|
|
351
|
+
canvasWidth,
|
|
352
|
+
canvasHeight,
|
|
353
|
+
timeWindow,
|
|
354
|
+
smoothing,
|
|
355
|
+
adaptiveSpeedBoostSV,
|
|
356
|
+
exaggerateSV,
|
|
357
|
+
referenceValue,
|
|
358
|
+
referenceValues,
|
|
359
|
+
nonNegativeSV,
|
|
360
|
+
maxValueSV,
|
|
361
|
+
nowOverrideSV,
|
|
362
|
+
windowBufferSV,
|
|
363
|
+
pausedSV,
|
|
364
|
+
viewEndSV: viewEnd,
|
|
365
|
+
returnTSV: returnT,
|
|
366
|
+
returnFromSV: returnFrom,
|
|
367
|
+
viewWindowSV: viewWindow,
|
|
368
|
+
liveEdgeSV: liveEdge,
|
|
369
|
+
snapSV,
|
|
370
|
+
extremaMinValue,
|
|
371
|
+
extremaMaxValue,
|
|
372
|
+
extremaMinTime,
|
|
373
|
+
extremaMaxTime,
|
|
374
|
+
};
|
|
375
|
+
|
|
278
376
|
useFrameCallback((frameInfo) => {
|
|
279
377
|
"worklet";
|
|
280
378
|
const scratch = scratchRef.current!;
|
|
281
379
|
applyLiveChartSeriesEngineFrame(
|
|
282
380
|
frameInfo,
|
|
283
|
-
|
|
284
|
-
series,
|
|
285
|
-
displaySeriesValues,
|
|
286
|
-
seriesOpacities,
|
|
287
|
-
displayMin,
|
|
288
|
-
displayMax,
|
|
289
|
-
displayWindow,
|
|
290
|
-
timestamp,
|
|
291
|
-
canvasWidth,
|
|
292
|
-
canvasHeight,
|
|
293
|
-
timeWindow,
|
|
294
|
-
smoothing,
|
|
295
|
-
adaptiveSpeedBoostSV,
|
|
296
|
-
exaggerateSV,
|
|
297
|
-
referenceValue,
|
|
298
|
-
referenceValues,
|
|
299
|
-
nonNegativeSV,
|
|
300
|
-
maxValueSV,
|
|
301
|
-
nowOverrideSV,
|
|
302
|
-
windowBufferSV,
|
|
303
|
-
pausedSV,
|
|
304
|
-
viewEndSV: viewEnd,
|
|
305
|
-
returnTSV: returnT,
|
|
306
|
-
returnFromSV: returnFrom,
|
|
307
|
-
viewWindowSV: viewWindow,
|
|
308
|
-
liveEdgeSV: liveEdge,
|
|
309
|
-
snapSV,
|
|
310
|
-
extremaMinValue,
|
|
311
|
-
extremaMaxValue,
|
|
312
|
-
extremaMinTime,
|
|
313
|
-
extremaMaxTime,
|
|
314
|
-
},
|
|
381
|
+
frameRefs,
|
|
315
382
|
scratch,
|
|
316
383
|
);
|
|
317
384
|
});
|
package/src/draw/markerAtlas.ts
CHANGED
|
@@ -59,6 +59,21 @@ export function groupCountText(count: number): string {
|
|
|
59
59
|
return count > 99 ? "99" : `${count}`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Width of proportionally laid-out count-badge text at its on-canvas scale. */
|
|
63
|
+
export function groupCountTextWidth(
|
|
64
|
+
text: string,
|
|
65
|
+
digitWidths: Record<string, number>,
|
|
66
|
+
letterSpacing = 0,
|
|
67
|
+
): number {
|
|
68
|
+
"worklet";
|
|
69
|
+
let width = 0;
|
|
70
|
+
for (let i = 0; i < text.length; i++) {
|
|
71
|
+
width += (digitWidths[text[i]] ?? 0) * BADGE_TEXT_SCALE;
|
|
72
|
+
if (i > 0) width += letterSpacing;
|
|
73
|
+
}
|
|
74
|
+
return width;
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
/** Default glyph color per kind when `marker.color` is unset. Worklet-safe so the
|
|
63
78
|
* per-frame overlay worklet can resolve a collapsed cluster's badge color. */
|
|
64
79
|
export function defaultMarkerColor(
|
|
@@ -333,10 +348,12 @@ function groupBgCellSpec(
|
|
|
333
348
|
bgColor: string,
|
|
334
349
|
uw: number,
|
|
335
350
|
gh: number,
|
|
351
|
+
letterSpacing: number,
|
|
336
352
|
): CellSpec {
|
|
337
353
|
const m2 = CELL_MARGIN * 2;
|
|
338
354
|
// Inner circle fits a centered two-digit string at BADGE_TEXT_SCALE plus padding.
|
|
339
|
-
const
|
|
355
|
+
const textWidth = 2 * uw * BADGE_TEXT_SCALE + Math.max(0, letterSpacing);
|
|
356
|
+
const innerR = Math.ceil(Math.max(textWidth, gh * BADGE_TEXT_SCALE)) / 2 + BADGE_PAD;
|
|
340
357
|
const outerR = innerR + BADGE_BORDER;
|
|
341
358
|
const diameter = 2 * outerR;
|
|
342
359
|
return {
|
|
@@ -429,7 +446,10 @@ export function buildMarkerAtlas(
|
|
|
429
446
|
colors.add(m.color ?? defaultMarkerColor(m.kind, palette));
|
|
430
447
|
}
|
|
431
448
|
for (const color of colors) {
|
|
432
|
-
specs.push({
|
|
449
|
+
specs.push({
|
|
450
|
+
sig: groupBgSig(color),
|
|
451
|
+
spec: groupBgCellSpec(color, bgColor, uw, gh, groupBadge?.letterSpacing ?? 0),
|
|
452
|
+
});
|
|
433
453
|
}
|
|
434
454
|
}
|
|
435
455
|
|
|
@@ -62,7 +62,8 @@ export interface ParticleInstance {
|
|
|
62
62
|
* scale = r / spriteRadius
|
|
63
63
|
* alpha = life * particleOpacity
|
|
64
64
|
* Inactive (active <= 0.5), pre-spawn (dt < 0) and fully-faded (life <= 0)
|
|
65
|
-
* slots are skipped.
|
|
65
|
+
* slots are skipped. Pass distinct `out` and `pool` arrays to retain inactive
|
|
66
|
+
* instance objects for later bursts while returning only the active prefix.
|
|
66
67
|
*/
|
|
67
68
|
export function buildParticleInstances(
|
|
68
69
|
buf: Float64Array,
|
|
@@ -71,9 +72,13 @@ export function buildParticleInstances(
|
|
|
71
72
|
burstDur: number,
|
|
72
73
|
particleOpacity: number,
|
|
73
74
|
spriteRadius: number,
|
|
75
|
+
out?: ParticleInstance[],
|
|
76
|
+
pool?: ParticleInstance[],
|
|
74
77
|
): ParticleInstance[] {
|
|
75
78
|
"worklet";
|
|
76
|
-
const out
|
|
79
|
+
const target = out ?? [];
|
|
80
|
+
const instances = pool ?? target;
|
|
81
|
+
let count = 0;
|
|
77
82
|
for (let i = 0; i < slots; i++) {
|
|
78
83
|
const base = i * DEGEN_STRIDE;
|
|
79
84
|
if (!(buf[base + 5] > 0.5)) continue;
|
|
@@ -83,13 +88,19 @@ export function buildParticleInstances(
|
|
|
83
88
|
if (life <= 0) continue;
|
|
84
89
|
const size = buf[base + 6] || 1;
|
|
85
90
|
const r = size * (0.5 + life * 0.5);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
y:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
let instance = instances[count];
|
|
92
|
+
if (!instance) {
|
|
93
|
+
instance = { x: 0, y: 0, scale: 0, alpha: 0, colorIndex: 0 };
|
|
94
|
+
instances[count] = instance;
|
|
95
|
+
}
|
|
96
|
+
instance.x = buf[base + 0];
|
|
97
|
+
instance.y = buf[base + 1];
|
|
98
|
+
instance.scale = r / spriteRadius;
|
|
99
|
+
instance.alpha = life * particleOpacity;
|
|
100
|
+
instance.colorIndex = Math.max(0, Math.floor(buf[base + 7]));
|
|
101
|
+
target[count] = instance;
|
|
102
|
+
count += 1;
|
|
93
103
|
}
|
|
94
|
-
|
|
104
|
+
target.length = count;
|
|
105
|
+
return target;
|
|
95
106
|
}
|
|
@@ -14,6 +14,22 @@ const TOOLTIP_EDGE_GAP = 4;
|
|
|
14
14
|
const TOOLTIP_TOP_MARGIN = 8;
|
|
15
15
|
const FADE_ZONE = 4;
|
|
16
16
|
|
|
17
|
+
/** Horizontal travel required before a plain scrub claims the touch. */
|
|
18
|
+
export const SCRUB_ACTIVATE_X_PX = 20;
|
|
19
|
+
/** Vertical travel that fails a plain scrub so a parent scroll gesture can win. */
|
|
20
|
+
export const SCRUB_FAIL_Y_PX = 10;
|
|
21
|
+
|
|
22
|
+
/** Measure rendered text so the pill and its content share the same centre. */
|
|
23
|
+
function measureTooltipTextWidth(
|
|
24
|
+
font: SkFont,
|
|
25
|
+
text: string,
|
|
26
|
+
monoCharWidth: number,
|
|
27
|
+
): number {
|
|
28
|
+
"worklet";
|
|
29
|
+
const measured = measureFontTextWidth(font, text);
|
|
30
|
+
return measured > 0 ? measured : text.length * monoCharWidth;
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
export interface TooltipLayout {
|
|
18
34
|
x: number;
|
|
19
35
|
y: number;
|
|
@@ -197,10 +213,7 @@ export function computeTooltipLayout(
|
|
|
197
213
|
formatValue: (v: number) => string,
|
|
198
214
|
formatTime: (t: number) => string,
|
|
199
215
|
font: SkFont,
|
|
200
|
-
/** Monospace advance width
|
|
201
|
-
* instead of a per-frame Skia `measureText` — scrubbing re-runs this worklet
|
|
202
|
-
* every frame, and `measureText` shapes text each call (a real cost,
|
|
203
|
-
* especially in the simulator). Falls back to `measureText` when 0. */
|
|
216
|
+
/** Monospace advance width fallback when font measurement returns zero. */
|
|
204
217
|
monoCharWidth = 0,
|
|
205
218
|
/** Where the pill sits relative to the scrub line. `"side"` offsets it right
|
|
206
219
|
* (flipping left near the edge); `"top"`/`"bottom"` center it over the line,
|
|
@@ -243,14 +256,11 @@ export function computeTooltipLayout(
|
|
|
243
256
|
const totalH =
|
|
244
257
|
TOOLTIP_PAD_Y * 2 + lineH * rowCount + TOOLTIP_LINE_GAP * (rowCount - 1);
|
|
245
258
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const timeW =
|
|
251
|
-
monoCharWidth > 0
|
|
252
|
-
? timeStr.length * monoCharWidth
|
|
253
|
-
: measureFontTextWidth(font, timeStr);
|
|
259
|
+
// Use actual glyph bounds for every visible-row variant. The old
|
|
260
|
+
// character-count approximation made punctuation-heavy dates/times appear
|
|
261
|
+
// off-centre even when the pill itself was positioned correctly.
|
|
262
|
+
const valueW = measureTooltipTextWidth(font, valueStr, monoCharWidth);
|
|
263
|
+
const timeW = measureTooltipTextWidth(font, timeStr, monoCharWidth);
|
|
254
264
|
// Only the visible rows contribute to the pill width.
|
|
255
265
|
const contentW = Math.max(sv ? valueW : 0, st ? timeW : 0);
|
|
256
266
|
const pillW = contentW + TOOLTIP_PAD_X * 2;
|
|
@@ -333,8 +343,7 @@ export function computeTooltipLayoutMulti(
|
|
|
333
343
|
padding: ChartPadding,
|
|
334
344
|
canvasWidth: number,
|
|
335
345
|
font: SkFont,
|
|
336
|
-
/** Monospace advance width
|
|
337
|
-
* per-frame Skia `measureText`. See {@link computeTooltipLayout}. */
|
|
346
|
+
/** Monospace advance width fallback when font measurement returns zero. */
|
|
338
347
|
monoCharWidth = 0,
|
|
339
348
|
): TooltipLayout {
|
|
340
349
|
"worklet";
|
|
@@ -349,10 +358,7 @@ export function computeTooltipLayoutMulti(
|
|
|
349
358
|
let contentW = 0;
|
|
350
359
|
const lineWidths: number[] = [];
|
|
351
360
|
for (let i = 0; i < n; i++) {
|
|
352
|
-
const w =
|
|
353
|
-
monoCharWidth > 0
|
|
354
|
-
? lines[i].text.length * monoCharWidth
|
|
355
|
-
: measureFontTextWidth(font, lines[i].text);
|
|
361
|
+
const w = measureTooltipTextWidth(font, lines[i].text, monoCharWidth);
|
|
356
362
|
lineWidths.push(w);
|
|
357
363
|
if (w > contentW) contentW = w;
|
|
358
364
|
}
|