react-native-livechart 4.7.0 → 4.9.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 +3 -1
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/ThresholdLineOverlay.d.ts +4 -1
- package/dist/components/ThresholdLineOverlay.d.ts.map +1 -1
- package/dist/components/ThresholdSplitShader.d.ts +26 -0
- package/dist/components/ThresholdSplitShader.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +9 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +24 -5
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +14 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +6 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useThreshold.d.ts +63 -6
- package/dist/hooks/useThreshold.d.ts.map +1 -1
- package/dist/math/threshold.d.ts +82 -0
- package/dist/math/threshold.d.ts.map +1 -1
- package/dist/types.d.ts +80 -19
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/LiveChart.tsx +284 -46
- package/src/components/LiveChartSeries.tsx +12 -3
- package/src/components/LoadingOverlay.tsx +35 -28
- package/src/components/ThresholdLineOverlay.tsx +35 -5
- package/src/components/ThresholdSplitShader.tsx +92 -0
- package/src/core/liveChartEngineTick.ts +28 -0
- package/src/core/resolveConfig.ts +35 -6
- package/src/core/useLiveChartEngine.ts +35 -0
- package/src/hooks/useChartPaths.ts +51 -7
- package/src/hooks/useThreshold.ts +267 -10
- package/src/math/threshold.ts +250 -0
- package/src/types.ts +81 -19
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { StyleSheet, View } from "react-native";
|
|
3
3
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
4
4
|
import Animated, {
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
resolveReturnToLiveMs,
|
|
49
49
|
resolveSelectionDot,
|
|
50
50
|
resolveThreshold,
|
|
51
|
+
THRESHOLD_FILL_OPACITY_DEFAULT,
|
|
51
52
|
resolveTradeStream,
|
|
52
53
|
resolveValueLine,
|
|
53
54
|
resolveVolume,
|
|
@@ -82,7 +83,11 @@ import { usePinchZoom } from "../hooks/usePinchZoom";
|
|
|
82
83
|
import { useVisibleRange } from "../hooks/useVisibleRange";
|
|
83
84
|
import { useSegmentLineGradient } from "../hooks/useSegmentLineGradient";
|
|
84
85
|
import { useSingleChartReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
|
|
85
|
-
import {
|
|
86
|
+
import {
|
|
87
|
+
useThreshold,
|
|
88
|
+
useThresholdSeries,
|
|
89
|
+
useThresholdSplitUniforms,
|
|
90
|
+
} from "../hooks/useThreshold";
|
|
86
91
|
import { useTradeStream } from "../hooks/useTradeStream";
|
|
87
92
|
import { useXAxis } from "../hooks/useXAxis";
|
|
88
93
|
import { useYAxis } from "../hooks/useYAxis";
|
|
@@ -110,6 +115,7 @@ import {
|
|
|
110
115
|
} from "../theme";
|
|
111
116
|
import type {
|
|
112
117
|
LiveChartPalette,
|
|
118
|
+
LiveChartPoint,
|
|
113
119
|
LiveChartProps,
|
|
114
120
|
Marker,
|
|
115
121
|
TradeEvent,
|
|
@@ -118,6 +124,10 @@ import {
|
|
|
118
124
|
ThresholdBadgeOverlay,
|
|
119
125
|
ThresholdLineOverlay,
|
|
120
126
|
} from "./ThresholdLineOverlay";
|
|
127
|
+
import {
|
|
128
|
+
THRESHOLD_SPLIT_AVAILABLE,
|
|
129
|
+
ThresholdSplitShader,
|
|
130
|
+
} from "./ThresholdSplitShader";
|
|
121
131
|
import { AreaDotsOverlay } from "./AreaDotsOverlay";
|
|
122
132
|
import { AxisLabelOverlay } from "./AxisLabelOverlay";
|
|
123
133
|
import {
|
|
@@ -149,8 +159,6 @@ import { ValueLineOverlay } from "./ValueLineOverlay";
|
|
|
149
159
|
import { XAxisOverlay } from "./XAxisOverlay";
|
|
150
160
|
import { YAxisOverlay } from "./YAxisOverlay";
|
|
151
161
|
|
|
152
|
-
/** Translucent fill alpha for the threshold profit/loss band. */
|
|
153
|
-
const THRESHOLD_FILL_OPACITY = 0.16;
|
|
154
162
|
|
|
155
163
|
/** Stable empty grouping result (identity-stable so downstream worklets don't
|
|
156
164
|
* re-run) used when reference-line grouping is off. */
|
|
@@ -163,9 +171,9 @@ const EMPTY_NUMS: number[] = [];
|
|
|
163
171
|
/**
|
|
164
172
|
* Color stops for the threshold's hard-split vertical gradient. Both arrays pair
|
|
165
173
|
* with the `[0, t, t, 1]` split positions: index 0–1 paint above the split,
|
|
166
|
-
* 2–3 below. `stroke` is full-strength; `fill` is the same hues at
|
|
167
|
-
* {
|
|
168
|
-
* up-green / down-red when colors are omitted.
|
|
174
|
+
* 2–3 below. `stroke` is full-strength; `fill` is the same hues at the config's
|
|
175
|
+
* band opacity (`fill: { opacity }`, default `0.16`). Defaults to the palette's
|
|
176
|
+
* semantic up-green / down-red when colors are omitted.
|
|
169
177
|
*/
|
|
170
178
|
function thresholdStops(
|
|
171
179
|
cfg: ResolvedThresholdConfig,
|
|
@@ -175,14 +183,51 @@ function thresholdStops(
|
|
|
175
183
|
const below = cfg.belowColor ?? palette.candleDown;
|
|
176
184
|
const [ar, ag, ab] = parseColorRgb(above);
|
|
177
185
|
const [br, bg, bb] = parseColorRgb(below);
|
|
178
|
-
const aboveFill = `rgba(${ar}, ${ag}, ${ab}, ${
|
|
179
|
-
const belowFill = `rgba(${br}, ${bg}, ${bb}, ${
|
|
186
|
+
const aboveFill = `rgba(${ar}, ${ag}, ${ab}, ${cfg.fillOpacity})`;
|
|
187
|
+
const belowFill = `rgba(${br}, ${bg}, ${bb}, ${cfg.fillOpacity})`;
|
|
180
188
|
return {
|
|
181
189
|
stroke: [above, above, below, below],
|
|
182
190
|
fill: [aboveFill, aboveFill, belowFill, belowFill],
|
|
183
191
|
};
|
|
184
192
|
}
|
|
185
193
|
|
|
194
|
+
/** Stand-in split color when no threshold is set (the shader is never rendered then). */
|
|
195
|
+
const THRESHOLD_FALLBACK_COLOR = [0, 0, 0, 1];
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Above/below split colors as straight-alpha `[r, g, b, a]` vec4s (channels 0..1)
|
|
199
|
+
* for the time-varying {@link ThresholdSplitShader}: full-strength `stroke*` for
|
|
200
|
+
* the line, band-opacity `fill*` for the band — the vec4 equivalent of
|
|
201
|
+
* {@link thresholdStops}' gradient color array. Takes the already
|
|
202
|
+
* palette-defaulted color strings; an rgba() alpha carries into the stroke and
|
|
203
|
+
* multiplies the band opacity (matching what the constant gradient's raw-string
|
|
204
|
+
* stroke does).
|
|
205
|
+
*/
|
|
206
|
+
function thresholdSplitColorVecs(
|
|
207
|
+
above: string,
|
|
208
|
+
below: string,
|
|
209
|
+
fillOpacity: number,
|
|
210
|
+
): {
|
|
211
|
+
strokeAbove: number[];
|
|
212
|
+
strokeBelow: number[];
|
|
213
|
+
fillAbove: number[];
|
|
214
|
+
fillBelow: number[];
|
|
215
|
+
} {
|
|
216
|
+
const [ar, ag, ab, aa] = parseColorRgba(above);
|
|
217
|
+
const [br, bg, bb, ba] = parseColorRgba(below);
|
|
218
|
+
const a = [ar / 255, ag / 255, ab / 255];
|
|
219
|
+
const b = [br / 255, bg / 255, bb / 255];
|
|
220
|
+
return {
|
|
221
|
+
strokeAbove: [a[0], a[1], a[2], aa],
|
|
222
|
+
strokeBelow: [b[0], b[1], b[2], ba],
|
|
223
|
+
fillAbove: [a[0], a[1], a[2], aa * fillOpacity],
|
|
224
|
+
fillBelow: [b[0], b[1], b[2], ba * fillOpacity],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Transparent vec4 — the band shader's "threshold ended" rest color. */
|
|
229
|
+
const TRANSPARENT_VEC4 = [0, 0, 0, 0];
|
|
230
|
+
|
|
186
231
|
/**
|
|
187
232
|
* Resolves props → configs → theme/layout → engine → per-frame derived values and
|
|
188
233
|
* overlay hooks, returning a single render model. All the chart's wiring lives
|
|
@@ -310,6 +355,9 @@ function useLiveChartController({
|
|
|
310
355
|
// Threshold split is a line-mode feature (candle bodies carry their own up/down
|
|
311
356
|
// colors), so it's inert in candle mode — same as the area gradient.
|
|
312
357
|
const thresholdCfg = isCandle ? null : resolveThreshold(threshold);
|
|
358
|
+
const thresholdSeriesSV = thresholdCfg?.series ?? null;
|
|
359
|
+
const thresholdIsSeries =
|
|
360
|
+
thresholdSeriesSV !== null || Array.isArray(thresholdCfg?.value);
|
|
313
361
|
const valueLineCfg = resolveValueLine(valueLine);
|
|
314
362
|
// Static charts run zero loops: force the pulse off so its `withRepeat`-driven
|
|
315
363
|
// ring never starts (the DotOverlay reads `pulseCfg`, so null = no pulse).
|
|
@@ -367,6 +415,11 @@ function useLiveChartController({
|
|
|
367
415
|
// range and the axis follows the finger in one motion (the committed values are
|
|
368
416
|
// already in `refValues`). `excludeFromRange` lines opt out, matching the static
|
|
369
417
|
// fit. Identity-stable (no re-fit) when nothing is draggable.
|
|
418
|
+
//
|
|
419
|
+
// `threshold.includeInRange` rides the same channel: the constant benchmark
|
|
420
|
+
// contributes its live value; a series contributes its window min/max
|
|
421
|
+
// (respecting `extendToNow`), so an off-range break-even expands the axis and
|
|
422
|
+
// stays on-plot like a reference line would.
|
|
370
423
|
const draggableRefIdx = allRefLines
|
|
371
424
|
.map((l, i) =>
|
|
372
425
|
l.draggable && !l.excludeFromRange && referenceLineForm(l) === "line"
|
|
@@ -374,13 +427,30 @@ function useLiveChartController({
|
|
|
374
427
|
: -1,
|
|
375
428
|
)
|
|
376
429
|
.filter((i) => i >= 0);
|
|
430
|
+
const thresholdInRange = thresholdCfg?.includeInRange === true;
|
|
431
|
+
// Constant benchmark → its live value rides this channel. A series threshold
|
|
432
|
+
// is folded inside the engine tick instead (`thresholdRangePoints`), which
|
|
433
|
+
// already knows the window bounds for the min/max.
|
|
434
|
+
const thresholdRangeValueSV =
|
|
435
|
+
thresholdInRange &&
|
|
436
|
+
thresholdCfg?.value != null &&
|
|
437
|
+
!Array.isArray(thresholdCfg.value)
|
|
438
|
+
? thresholdCfg.value
|
|
439
|
+
: null;
|
|
377
440
|
const liveRefValues = useDerivedValue<number[]>(() => {
|
|
378
|
-
if (draggableRefIdx.length === 0
|
|
379
|
-
|
|
441
|
+
if (draggableRefIdx.length === 0 && thresholdRangeValueSV === null)
|
|
442
|
+
return EMPTY_NUMS;
|
|
380
443
|
const out: number[] = [];
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
444
|
+
if (draggableRefIdx.length > 0) {
|
|
445
|
+
const dv = dragValues.get();
|
|
446
|
+
for (let k = 0; k < draggableRefIdx.length; k++) {
|
|
447
|
+
const v = dv[draggableRefIdx[k]];
|
|
448
|
+
if (v != null) out.push(v);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (thresholdRangeValueSV !== null) {
|
|
452
|
+
const v = thresholdRangeValueSV.get();
|
|
453
|
+
if (Number.isFinite(v)) out.push(v);
|
|
384
454
|
}
|
|
385
455
|
return out;
|
|
386
456
|
});
|
|
@@ -587,6 +657,13 @@ function useLiveChartController({
|
|
|
587
657
|
exaggerate,
|
|
588
658
|
referenceValues: refValues,
|
|
589
659
|
liveReferenceValues: liveRefValues,
|
|
660
|
+
// Series threshold with `includeInRange`: the tick folds its window min/max
|
|
661
|
+
// into the Y-range fit (the constant form rides `liveReferenceValues`).
|
|
662
|
+
thresholdRangePoints:
|
|
663
|
+
thresholdInRange && thresholdIsSeries
|
|
664
|
+
? (thresholdSeriesSV ?? (thresholdCfg?.value as LiveChartPoint[]))
|
|
665
|
+
: undefined,
|
|
666
|
+
thresholdRangeExtendToNow: thresholdCfg?.extendToNow ?? true,
|
|
590
667
|
nonNegative,
|
|
591
668
|
maxValue,
|
|
592
669
|
windowBuffer,
|
|
@@ -658,17 +735,116 @@ function useLiveChartController({
|
|
|
658
735
|
() => refGroupResult.get().hidden,
|
|
659
736
|
);
|
|
660
737
|
|
|
661
|
-
// Threshold split geometry
|
|
662
|
-
//
|
|
663
|
-
|
|
738
|
+
// Threshold split geometry. Two forms, picked at render by `Array.isArray` (no
|
|
739
|
+
// SharedValue read): a constant `SharedValue<number>` benchmark drives the
|
|
740
|
+
// vertical hard-stop gradient (`thresholdGeom`); a time-varying `LiveChartPoint[]`
|
|
741
|
+
// series drives the per-fragment split shader (`thresholdSeriesGeom`). Both hooks
|
|
742
|
+
// run unconditionally — the unused one short-circuits cheaply on the UI thread.
|
|
743
|
+
const thresholdValue = thresholdCfg?.value ?? emptyThresholdValue;
|
|
744
|
+
// An empty series (e.g. threshold history not fetched yet) must not paint: no
|
|
745
|
+
// band, no shader-forced stroke color — unlike the constant form, "no points
|
|
746
|
+
// yet" is a reachable state and should look like "no threshold". For the live
|
|
747
|
+
// `series` SharedValue form the emptiness lives on the UI thread, so mirror it
|
|
748
|
+
// to React state (fires only on empty↔non-empty transitions).
|
|
749
|
+
const [svSeriesNonEmpty, setSvSeriesNonEmpty] = useState(false);
|
|
750
|
+
useAnimatedReaction(
|
|
751
|
+
() => (thresholdSeriesSV ? thresholdSeriesSV.get().length > 0 : false),
|
|
752
|
+
/* istanbul ignore next -- Reanimated reaction; state mirrored on the JS thread, not exercised under Jest */
|
|
753
|
+
(hasPts, prev) => {
|
|
754
|
+
if (hasPts !== prev) scheduleOnRN(setSvSeriesNonEmpty, hasPts);
|
|
755
|
+
},
|
|
756
|
+
);
|
|
757
|
+
const thresholdSeriesHasPoints = thresholdSeriesSV
|
|
758
|
+
? svSeriesNonEmpty
|
|
759
|
+
: Array.isArray(thresholdCfg?.value) && thresholdCfg.value.length > 0;
|
|
760
|
+
const thresholdGeom = useThreshold(engine, effectivePadding, thresholdValue);
|
|
761
|
+
const thresholdSeriesGeom = useThresholdSeries(
|
|
664
762
|
engine,
|
|
665
763
|
effectivePadding,
|
|
666
|
-
|
|
764
|
+
thresholdValue,
|
|
765
|
+
thresholdSeriesSV,
|
|
766
|
+
thresholdCfg?.extendToNow ?? true,
|
|
667
767
|
);
|
|
668
768
|
const thresholdStopColors = thresholdCfg
|
|
669
769
|
? thresholdStops(thresholdCfg, palette)
|
|
670
770
|
: null;
|
|
671
771
|
|
|
772
|
+
// Split colors as vec4s for the series shader, memoized on the *resolved*
|
|
773
|
+
// color strings + opacity (so a threshold added after mount with default
|
|
774
|
+
// colors still computes, and the per-frame uniforms worklet isn't rebuilt
|
|
775
|
+
// every render). `strokeRest` is the plain line color painted right of the
|
|
776
|
+
// `extendToNow: false` cutoff.
|
|
777
|
+
const thresholdSplitAbove = thresholdCfg
|
|
778
|
+
? (thresholdCfg.aboveColor ?? palette.candleUp)
|
|
779
|
+
: null;
|
|
780
|
+
const thresholdSplitBelow = thresholdCfg
|
|
781
|
+
? (thresholdCfg.belowColor ?? palette.candleDown)
|
|
782
|
+
: null;
|
|
783
|
+
const thresholdFillOpacity =
|
|
784
|
+
thresholdCfg?.fillOpacity ?? THRESHOLD_FILL_OPACITY_DEFAULT;
|
|
785
|
+
const thresholdLineColorStr = lineProp?.color ?? palette.line;
|
|
786
|
+
const thresholdVecs = useMemo(
|
|
787
|
+
() =>
|
|
788
|
+
thresholdSplitAbove !== null && thresholdSplitBelow !== null
|
|
789
|
+
? {
|
|
790
|
+
...thresholdSplitColorVecs(
|
|
791
|
+
thresholdSplitAbove,
|
|
792
|
+
thresholdSplitBelow,
|
|
793
|
+
thresholdFillOpacity,
|
|
794
|
+
),
|
|
795
|
+
strokeRest: (() => {
|
|
796
|
+
const [r, g, b, a] = parseColorRgba(thresholdLineColorStr);
|
|
797
|
+
return [r / 255, g / 255, b / 255, a];
|
|
798
|
+
})(),
|
|
799
|
+
}
|
|
800
|
+
: null,
|
|
801
|
+
[
|
|
802
|
+
thresholdSplitAbove,
|
|
803
|
+
thresholdSplitBelow,
|
|
804
|
+
thresholdFillOpacity,
|
|
805
|
+
thresholdLineColorStr,
|
|
806
|
+
],
|
|
807
|
+
);
|
|
808
|
+
const thresholdStrokeUniforms = useThresholdSplitUniforms(
|
|
809
|
+
thresholdSeriesGeom.samples,
|
|
810
|
+
engine,
|
|
811
|
+
effectivePadding,
|
|
812
|
+
thresholdVecs?.strokeAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
813
|
+
thresholdVecs?.strokeBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
814
|
+
thresholdVecs?.strokeRest ?? THRESHOLD_FALLBACK_COLOR,
|
|
815
|
+
thresholdSeriesGeom.clipRightX,
|
|
816
|
+
);
|
|
817
|
+
const thresholdFillUniforms = useThresholdSplitUniforms(
|
|
818
|
+
thresholdSeriesGeom.samples,
|
|
819
|
+
engine,
|
|
820
|
+
effectivePadding,
|
|
821
|
+
thresholdVecs?.fillAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
822
|
+
thresholdVecs?.fillBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
823
|
+
TRANSPARENT_VEC4,
|
|
824
|
+
thresholdSeriesGeom.clipRightX,
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
// Marker line + badge sources: the series anchors at the value-at-now (flat-
|
|
828
|
+
// extended past its last point); the constant case at the single benchmark Y.
|
|
829
|
+
// The badge gets its own visibility — it's pinned at the value-at-now Y, which
|
|
830
|
+
// can be off-plot while older polyline segments are still visible.
|
|
831
|
+
const thresholdMarkerLineY = thresholdIsSeries
|
|
832
|
+
? thresholdSeriesGeom.currentLineY
|
|
833
|
+
: thresholdGeom.lineY;
|
|
834
|
+
const thresholdMarkerVisible = thresholdIsSeries
|
|
835
|
+
? thresholdSeriesGeom.visible
|
|
836
|
+
: thresholdGeom.visible;
|
|
837
|
+
const thresholdBadgeVisible = thresholdIsSeries
|
|
838
|
+
? thresholdSeriesGeom.currentVisible
|
|
839
|
+
: thresholdGeom.visible;
|
|
840
|
+
const thresholdMarkerValue =
|
|
841
|
+
thresholdCfg && !thresholdIsSeries && !Array.isArray(thresholdCfg.value)
|
|
842
|
+
? (thresholdCfg.value ?? thresholdSeriesGeom.currentValue)
|
|
843
|
+
: thresholdSeriesGeom.currentValue;
|
|
844
|
+
const thresholdSeriesPts = thresholdIsSeries
|
|
845
|
+
? thresholdSeriesGeom.screenPts
|
|
846
|
+
: undefined;
|
|
847
|
+
|
|
672
848
|
// Straight polyline instead of the monotone cubic when line.curve === "linear".
|
|
673
849
|
// Shared by the path builders and the marker anchoring so glyphs sit on the
|
|
674
850
|
// rendered line rather than the phantom spline.
|
|
@@ -678,8 +854,9 @@ function useLiveChartController({
|
|
|
678
854
|
engine,
|
|
679
855
|
effectivePadding,
|
|
680
856
|
reveal.morphT,
|
|
681
|
-
//
|
|
682
|
-
|
|
857
|
+
// Constant threshold band closes at a single Y (the series closes along the
|
|
858
|
+
// polyline passed as the last arg below).
|
|
859
|
+
thresholdCfg?.fill && !thresholdIsSeries ? thresholdGeom.lineY : undefined,
|
|
683
860
|
lineIsLinear,
|
|
684
861
|
// Tip the line at the view-edge price (not the live value) while scrolled,
|
|
685
862
|
// matching the live dot — so the right edge doesn't drop to the off-screen
|
|
@@ -689,6 +866,13 @@ function useLiveChartController({
|
|
|
689
866
|
// Match the standalone loading squiggle's wave during the reveal morph.
|
|
690
867
|
loadingCfg?.amplitude,
|
|
691
868
|
loadingCfg?.speed,
|
|
869
|
+
// Time-varying threshold band: bottom edge built from the shader's samples
|
|
870
|
+
// (so the band matches the shader and doesn't bleed at step risers). An empty
|
|
871
|
+
// series builds no band (its samples are all the far-below fallback, which
|
|
872
|
+
// would tint the entire area under the line).
|
|
873
|
+
thresholdCfg?.fill && thresholdSeriesHasPoints
|
|
874
|
+
? thresholdSeriesGeom.samples
|
|
875
|
+
: undefined,
|
|
692
876
|
);
|
|
693
877
|
|
|
694
878
|
// Area-dots fill shader color as a vec4 (channels 0..1), with the config
|
|
@@ -912,6 +1096,12 @@ function useLiveChartController({
|
|
|
912
1096
|
: 0,
|
|
913
1097
|
);
|
|
914
1098
|
|
|
1099
|
+
// Capture only the shared value in the worklets below. Referencing
|
|
1100
|
+
// `crosshair.scrubActive` inside a worklet closes over the whole `crosshair`
|
|
1101
|
+
// object (which holds a non-serializable `gesture`), throwing
|
|
1102
|
+
// "[Worklets] Cannot copy value of type `PanGesture`" on worklets >=0.10.
|
|
1103
|
+
const crosshairScrubActive = crosshair.scrubActive;
|
|
1104
|
+
|
|
915
1105
|
// ── Time-scroll (drag back through history) ───────────────────────────────
|
|
916
1106
|
// Experimental: a pan freezes the window at an absolute time and resumes
|
|
917
1107
|
// following once dragged back to the live edge. Pan is clamped to the earliest
|
|
@@ -929,7 +1119,7 @@ function useLiveChartController({
|
|
|
929
1119
|
// Clear any live crosshair when a scroll drag takes over.
|
|
930
1120
|
onScrollStart: () => {
|
|
931
1121
|
"worklet";
|
|
932
|
-
|
|
1122
|
+
crosshairScrubActive.set(false);
|
|
933
1123
|
},
|
|
934
1124
|
});
|
|
935
1125
|
|
|
@@ -946,7 +1136,7 @@ function useLiveChartController({
|
|
|
946
1136
|
maxTimeWindow: zoomCfg?.maxTimeWindow,
|
|
947
1137
|
onZoomStart: () => {
|
|
948
1138
|
"worklet";
|
|
949
|
-
|
|
1139
|
+
crosshairScrubActive.set(false);
|
|
950
1140
|
},
|
|
951
1141
|
});
|
|
952
1142
|
|
|
@@ -1054,7 +1244,7 @@ function useLiveChartController({
|
|
|
1054
1244
|
const liveDotOpacity = useDerivedValue(
|
|
1055
1245
|
() =>
|
|
1056
1246
|
reveal.dotOpacity.value *
|
|
1057
|
-
(selectionDotDuringScrub &&
|
|
1247
|
+
(selectionDotDuringScrub && crosshairScrubActive.value ? 0 : 1),
|
|
1058
1248
|
);
|
|
1059
1249
|
|
|
1060
1250
|
// Fade the annotation overlays (markers + reference lines) out while scrubbing
|
|
@@ -1066,7 +1256,7 @@ function useLiveChartController({
|
|
|
1066
1256
|
!isStatic && scrubCfg !== null && scrubCfg.hideOverlaysOnScrub === true;
|
|
1067
1257
|
const overlayScrubFade = useDerivedValue(() =>
|
|
1068
1258
|
fadeOverlaysOnScrub
|
|
1069
|
-
? withTiming(
|
|
1259
|
+
? withTiming(crosshairScrubActive.get() ? 0 : 1, {
|
|
1070
1260
|
duration: SCRUB_OVERLAY_FADE_MS,
|
|
1071
1261
|
})
|
|
1072
1262
|
: 1,
|
|
@@ -1127,6 +1317,17 @@ function useLiveChartController({
|
|
|
1127
1317
|
thresholdGeom,
|
|
1128
1318
|
thresholdStrokeColors: thresholdStopColors?.stroke ?? null,
|
|
1129
1319
|
thresholdFillColors: thresholdStopColors?.fill ?? null,
|
|
1320
|
+
// Time-varying threshold (a `LiveChartPoint[]` series): the per-fragment split
|
|
1321
|
+
// shader + polyline marker, vs. the constant case's gradient.
|
|
1322
|
+
thresholdIsSeries,
|
|
1323
|
+
thresholdSeriesHasPoints,
|
|
1324
|
+
thresholdStrokeUniforms,
|
|
1325
|
+
thresholdFillUniforms,
|
|
1326
|
+
thresholdMarkerLineY,
|
|
1327
|
+
thresholdMarkerVisible,
|
|
1328
|
+
thresholdBadgeVisible,
|
|
1329
|
+
thresholdMarkerValue,
|
|
1330
|
+
thresholdSeriesPts,
|
|
1130
1331
|
badgeUsesRightGutter,
|
|
1131
1332
|
// theme / layout / fonts
|
|
1132
1333
|
palette,
|
|
@@ -1144,6 +1345,7 @@ function useLiveChartController({
|
|
|
1144
1345
|
loadingStrokeWidth: loadingCfg?.strokeWidth,
|
|
1145
1346
|
loadingAmplitude: loadingCfg?.amplitude,
|
|
1146
1347
|
loadingSpeed: loadingCfg?.speed,
|
|
1348
|
+
loadingAxisLabels: loadingCfg?.axisLabels ?? true,
|
|
1147
1349
|
// derived render values
|
|
1148
1350
|
backgroundColor,
|
|
1149
1351
|
gradientEnd,
|
|
@@ -1240,6 +1442,9 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
|
|
|
1240
1442
|
thresholdGeom,
|
|
1241
1443
|
thresholdFillPath,
|
|
1242
1444
|
thresholdFillColors,
|
|
1445
|
+
thresholdIsSeries,
|
|
1446
|
+
thresholdSeriesHasPoints,
|
|
1447
|
+
thresholdFillUniforms,
|
|
1243
1448
|
} = model;
|
|
1244
1449
|
|
|
1245
1450
|
return (
|
|
@@ -1293,20 +1498,33 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
|
|
|
1293
1498
|
)}
|
|
1294
1499
|
|
|
1295
1500
|
{/* Threshold profit/loss band — the area between the line and the threshold,
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1501
|
+
split into the above/below colors. Independent of the baseline area fill
|
|
1502
|
+
above (set `gradient={false}` for the band alone). A time-varying series
|
|
1503
|
+
paints with the per-fragment split shader; a constant value with the
|
|
1504
|
+
vertical hard-stop gradient. */}
|
|
1505
|
+
{thresholdCfg?.fill &&
|
|
1506
|
+
(thresholdIsSeries ? (
|
|
1507
|
+
// The availability gate keeps a failed shader compile from filling the
|
|
1508
|
+
// band with the default paint (opaque black) — see THRESHOLD_SPLIT_AVAILABLE.
|
|
1509
|
+
thresholdSeriesHasPoints && THRESHOLD_SPLIT_AVAILABLE ? (
|
|
1510
|
+
<Group opacity={reveal.fillOpacity}>
|
|
1511
|
+
<Path path={thresholdFillPath} style="fill">
|
|
1512
|
+
<ThresholdSplitShader uniforms={thresholdFillUniforms} />
|
|
1513
|
+
</Path>
|
|
1514
|
+
</Group>
|
|
1515
|
+
) : null
|
|
1516
|
+
) : thresholdFillColors ? (
|
|
1517
|
+
<Group opacity={reveal.fillOpacity}>
|
|
1518
|
+
<Path path={thresholdFillPath} style="fill">
|
|
1519
|
+
<LinearGradient
|
|
1520
|
+
start={vec(0, 0)}
|
|
1521
|
+
end={thresholdGeom.gradientEnd}
|
|
1522
|
+
colors={thresholdFillColors}
|
|
1523
|
+
positions={thresholdGeom.splitPositions}
|
|
1524
|
+
/>
|
|
1525
|
+
</Path>
|
|
1526
|
+
</Group>
|
|
1527
|
+
) : null)}
|
|
1310
1528
|
</Group>
|
|
1311
1529
|
);
|
|
1312
1530
|
}
|
|
@@ -1335,6 +1553,14 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1335
1553
|
thresholdCfg,
|
|
1336
1554
|
thresholdGeom,
|
|
1337
1555
|
thresholdStrokeColors,
|
|
1556
|
+
thresholdIsSeries,
|
|
1557
|
+
thresholdSeriesHasPoints,
|
|
1558
|
+
thresholdStrokeUniforms,
|
|
1559
|
+
thresholdMarkerLineY,
|
|
1560
|
+
thresholdMarkerVisible,
|
|
1561
|
+
thresholdBadgeVisible,
|
|
1562
|
+
thresholdMarkerValue,
|
|
1563
|
+
thresholdSeriesPts,
|
|
1338
1564
|
formatValue,
|
|
1339
1565
|
lineGroupOpacity,
|
|
1340
1566
|
linePath,
|
|
@@ -1369,6 +1595,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1369
1595
|
overlayScrubFade,
|
|
1370
1596
|
renderMarker,
|
|
1371
1597
|
emptyText,
|
|
1598
|
+
loadingAxisLabels,
|
|
1372
1599
|
metricsCfg,
|
|
1373
1600
|
layoutWidth,
|
|
1374
1601
|
yAxisCfg,
|
|
@@ -1431,18 +1658,20 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1431
1658
|
))}
|
|
1432
1659
|
</Group>
|
|
1433
1660
|
|
|
1434
|
-
{/* Threshold marker line + label (behind the chart line).
|
|
1661
|
+
{/* Threshold marker line + label (behind the chart line). For a time-varying
|
|
1662
|
+
series it traces the threshold polyline; otherwise a horizontal line. */}
|
|
1435
1663
|
{thresholdCfg?.line && (
|
|
1436
1664
|
<ThresholdLineOverlay
|
|
1437
1665
|
engine={engine}
|
|
1438
1666
|
padding={effectivePadding}
|
|
1439
|
-
lineY={
|
|
1440
|
-
visible={
|
|
1441
|
-
value={
|
|
1667
|
+
lineY={thresholdMarkerLineY}
|
|
1668
|
+
visible={thresholdMarkerVisible}
|
|
1669
|
+
value={thresholdMarkerValue}
|
|
1442
1670
|
cfg={thresholdCfg.line}
|
|
1443
1671
|
palette={palette}
|
|
1444
1672
|
font={skiaFont}
|
|
1445
1673
|
formatValue={formatValue}
|
|
1674
|
+
seriesPts={thresholdSeriesPts}
|
|
1446
1675
|
/>
|
|
1447
1676
|
)}
|
|
1448
1677
|
|
|
@@ -1459,7 +1688,15 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1459
1688
|
strokeJoin={lineProp?.join ?? "round"}
|
|
1460
1689
|
color={lineProp?.color ?? palette.line}
|
|
1461
1690
|
>
|
|
1462
|
-
{
|
|
1691
|
+
{thresholdIsSeries ? (
|
|
1692
|
+
// Time-varying split: a per-fragment shader colors the stroke above/
|
|
1693
|
+
// below the threshold polyline. Supersedes line.colors + segments.
|
|
1694
|
+
// An empty series renders no paint child → the plain stroke color
|
|
1695
|
+
// (null here keeps the empty case out of the NaN constant gradient).
|
|
1696
|
+
thresholdSeriesHasPoints ? (
|
|
1697
|
+
<ThresholdSplitShader uniforms={thresholdStrokeUniforms} />
|
|
1698
|
+
) : null
|
|
1699
|
+
) : thresholdCfg && thresholdStrokeColors ? (
|
|
1463
1700
|
// Vertical hard split at the threshold Y — supersedes line.colors and
|
|
1464
1701
|
// segment recoloring for the stroke while a threshold is set.
|
|
1465
1702
|
<LinearGradient
|
|
@@ -1609,9 +1846,9 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1609
1846
|
<ThresholdBadgeOverlay
|
|
1610
1847
|
engine={engine}
|
|
1611
1848
|
padding={effectivePadding}
|
|
1612
|
-
lineY={
|
|
1613
|
-
visible={
|
|
1614
|
-
value={
|
|
1849
|
+
lineY={thresholdMarkerLineY}
|
|
1850
|
+
visible={thresholdBadgeVisible}
|
|
1851
|
+
value={thresholdMarkerValue}
|
|
1615
1852
|
cfg={thresholdCfg.line}
|
|
1616
1853
|
palette={palette}
|
|
1617
1854
|
font={skiaFont}
|
|
@@ -1635,6 +1872,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1635
1872
|
badgeTail={badgeCfg?.tail ?? true}
|
|
1636
1873
|
badgeMetrics={metricsCfg.badge}
|
|
1637
1874
|
emptyMetrics={metricsCfg.emptyState}
|
|
1875
|
+
showAxisLabels={loadingAxisLabels}
|
|
1638
1876
|
lineColor={loadingLineColor}
|
|
1639
1877
|
lineStrokeWidth={loadingStrokeWidth}
|
|
1640
1878
|
waveAmplitude={loadingAmplitude}
|
|
@@ -391,6 +391,12 @@ function useLiveChartSeriesController({
|
|
|
391
391
|
onGestureEnd,
|
|
392
392
|
);
|
|
393
393
|
|
|
394
|
+
// Capture only the shared value in the worklets below. Referencing
|
|
395
|
+
// `crosshair.scrubActive` inside a worklet closes over the whole `crosshair`
|
|
396
|
+
// object (which holds a non-serializable `gesture`), throwing
|
|
397
|
+
// "[Worklets] Cannot copy value of type `PanGesture`" on worklets >=0.10.
|
|
398
|
+
const crosshairScrubActive = crosshair.scrubActive;
|
|
399
|
+
|
|
394
400
|
// `projected` is used internally by the hit-test gesture; the overlay
|
|
395
401
|
// self-projects, so we only need the gesture here.
|
|
396
402
|
const { tapGesture: markerTapGesture } = useMarkers(
|
|
@@ -428,7 +434,7 @@ function useLiveChartSeriesController({
|
|
|
428
434
|
mode: scrollGestureMode,
|
|
429
435
|
onScrollStart: () => {
|
|
430
436
|
"worklet";
|
|
431
|
-
|
|
437
|
+
crosshairScrubActive.set(false);
|
|
432
438
|
},
|
|
433
439
|
});
|
|
434
440
|
|
|
@@ -442,7 +448,7 @@ function useLiveChartSeriesController({
|
|
|
442
448
|
maxTimeWindow: zoomCfg?.maxTimeWindow,
|
|
443
449
|
onZoomStart: () => {
|
|
444
450
|
"worklet";
|
|
445
|
-
|
|
451
|
+
crosshairScrubActive.set(false);
|
|
446
452
|
},
|
|
447
453
|
});
|
|
448
454
|
|
|
@@ -480,7 +486,7 @@ function useLiveChartSeriesController({
|
|
|
480
486
|
scrubCfg !== null && scrubCfg.hideOverlaysOnScrub === true;
|
|
481
487
|
const overlayScrubFade = useDerivedValue(() =>
|
|
482
488
|
fadeOverlaysOnScrub
|
|
483
|
-
? withTiming(
|
|
489
|
+
? withTiming(crosshairScrubActive.get() ? 0 : 1, {
|
|
484
490
|
duration: SCRUB_OVERLAY_FADE_MS,
|
|
485
491
|
})
|
|
486
492
|
: 1,
|
|
@@ -525,6 +531,7 @@ function useLiveChartSeriesController({
|
|
|
525
531
|
loadingStrokeWidth: loadingCfg?.strokeWidth,
|
|
526
532
|
loadingAmplitude: loadingCfg?.amplitude,
|
|
527
533
|
loadingSpeed: loadingCfg?.speed,
|
|
534
|
+
loadingAxisLabels: loadingCfg?.axisLabels ?? true,
|
|
528
535
|
effectiveSeries,
|
|
529
536
|
layoutHeight,
|
|
530
537
|
onLayout,
|
|
@@ -593,6 +600,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
593
600
|
renderMarker,
|
|
594
601
|
series,
|
|
595
602
|
emptyText,
|
|
603
|
+
loadingAxisLabels,
|
|
596
604
|
metricsCfg,
|
|
597
605
|
loadingLineColor,
|
|
598
606
|
loadingStrokeWidth,
|
|
@@ -732,6 +740,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
732
740
|
strokeWidth={strokeWidth}
|
|
733
741
|
badge={false}
|
|
734
742
|
emptyMetrics={metricsCfg.emptyState}
|
|
743
|
+
showAxisLabels={loadingAxisLabels}
|
|
735
744
|
lineColor={loadingLineColor}
|
|
736
745
|
lineStrokeWidth={loadingStrokeWidth}
|
|
737
746
|
waveAmplitude={loadingAmplitude}
|