react-native-livechart 4.21.1 → 4.22.1
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/CustomThresholdBadgeOverlay.d.ts +3 -1
- package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -1
- package/dist/components/ThresholdLineOverlay.d.ts +3 -1
- package/dist/components/ThresholdLineOverlay.d.ts.map +1 -1
- package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +3 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +2 -0
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +4 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/hooks/useChartSkiaFont.d.ts +2 -0
- package/dist/hooks/useChartSkiaFont.d.ts.map +1 -1
- package/dist/hooks/useReferenceLineSeries.d.ts +1 -1
- package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -1
- package/dist/hooks/useThreshold.d.ts +10 -5
- package/dist/hooks/useThreshold.d.ts.map +1 -1
- package/dist/math/referenceLineSeries.d.ts +1 -1
- package/dist/math/referenceLineSeries.d.ts.map +1 -1
- package/dist/math/threshold.d.ts +4 -3
- package/dist/math/threshold.d.ts.map +1 -1
- package/dist/types.d.ts +20 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CustomThresholdBadgeOverlay.tsx +11 -4
- package/src/components/LiveChart.tsx +17 -0
- package/src/components/ReferenceLineSeriesOverlay.tsx +2 -0
- package/src/components/ThresholdLineOverlay.tsx +16 -6
- package/src/components/thresholdSplitShaderSource.ts +4 -1
- package/src/core/liveChartEngineTick.ts +4 -0
- package/src/core/resolveConfig.ts +3 -0
- package/src/core/useLiveChartEngine.ts +10 -0
- package/src/hooks/useChartSkiaFont.ts +12 -9
- package/src/hooks/useReferenceLineSeries.ts +5 -0
- package/src/hooks/useThreshold.ts +41 -15
- package/src/math/referenceLineSeries.ts +9 -4
- package/src/math/threshold.ts +10 -3
- package/src/types.ts +20 -5
|
@@ -24,6 +24,7 @@ export function CustomThresholdBadgeOverlay({
|
|
|
24
24
|
y,
|
|
25
25
|
visible,
|
|
26
26
|
position,
|
|
27
|
+
leftAnchorX,
|
|
27
28
|
}: {
|
|
28
29
|
element: React.ReactElement;
|
|
29
30
|
engine: ChartEngineLayout;
|
|
@@ -31,6 +32,8 @@ export function CustomThresholdBadgeOverlay({
|
|
|
31
32
|
y: SharedValue<number>;
|
|
32
33
|
visible: SharedValue<boolean>;
|
|
33
34
|
position: "left" | "right";
|
|
35
|
+
/** Optional first rendered X for a non-extended series badge. */
|
|
36
|
+
leftAnchorX?: SharedValue<number>;
|
|
34
37
|
}) {
|
|
35
38
|
const size = useSharedValue({ width: 0, height: 0 });
|
|
36
39
|
const onLayout = (event: LayoutChangeEvent) => {
|
|
@@ -43,10 +46,14 @@ export function CustomThresholdBadgeOverlay({
|
|
|
43
46
|
const yy = y.get();
|
|
44
47
|
const measured = size.get();
|
|
45
48
|
const show = visible.get() && canvasWidth > 0 && Number.isFinite(yy);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
let translateX = EDGE_INSET;
|
|
50
|
+
if (position === "right") {
|
|
51
|
+
translateX = canvasWidth - padding.right - EDGE_INSET - measured.width;
|
|
52
|
+
} else if (leftAnchorX) {
|
|
53
|
+
const minX = padding.left + EDGE_INSET;
|
|
54
|
+
const maxX = canvasWidth - padding.right - EDGE_INSET - measured.width;
|
|
55
|
+
translateX = Math.max(minX, Math.min(leftAnchorX.get(), maxX));
|
|
56
|
+
}
|
|
50
57
|
return {
|
|
51
58
|
opacity: show ? 1 : 0,
|
|
52
59
|
transform: [
|
|
@@ -672,6 +672,7 @@ function useThresholdGeometryState({
|
|
|
672
672
|
thresholdValue,
|
|
673
673
|
thresholdSeriesSV,
|
|
674
674
|
thresholdCfg?.extendToNow ?? true,
|
|
675
|
+
thresholdCfg?.extendToStart ?? true,
|
|
675
676
|
thresholdCfg?.line?.labelAnchor ?? "last",
|
|
676
677
|
),
|
|
677
678
|
};
|
|
@@ -681,6 +682,7 @@ function useThresholdShaderUniforms({
|
|
|
681
682
|
engine,
|
|
682
683
|
padding,
|
|
683
684
|
samples,
|
|
685
|
+
clipLeftX,
|
|
684
686
|
clipRightX,
|
|
685
687
|
thresholdCfg,
|
|
686
688
|
palette,
|
|
@@ -689,6 +691,7 @@ function useThresholdShaderUniforms({
|
|
|
689
691
|
engine: ReturnType<typeof useLiveChartEngine>;
|
|
690
692
|
padding: ChartPadding;
|
|
691
693
|
samples: SharedValue<number[]>;
|
|
694
|
+
clipLeftX: SharedValue<number>;
|
|
692
695
|
clipRightX: SharedValue<number>;
|
|
693
696
|
thresholdCfg: ReturnType<typeof resolveThreshold>;
|
|
694
697
|
palette: LiveChartPalette;
|
|
@@ -714,6 +717,7 @@ function useThresholdShaderUniforms({
|
|
|
714
717
|
vectors?.strokeAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
715
718
|
vectors?.strokeBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
716
719
|
vectors?.strokeRest ?? THRESHOLD_FALLBACK_COLOR,
|
|
720
|
+
clipLeftX,
|
|
717
721
|
clipRightX,
|
|
718
722
|
),
|
|
719
723
|
thresholdFillUniforms: useThresholdSplitUniforms(
|
|
@@ -723,6 +727,7 @@ function useThresholdShaderUniforms({
|
|
|
723
727
|
vectors?.fillAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
724
728
|
vectors?.fillBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
725
729
|
TRANSPARENT_VEC4,
|
|
730
|
+
clipLeftX,
|
|
726
731
|
clipRightX,
|
|
727
732
|
),
|
|
728
733
|
};
|
|
@@ -778,6 +783,10 @@ function useThresholdBadgeProjection({
|
|
|
778
783
|
thresholdSeriesPts: thresholdIsSeries
|
|
779
784
|
? thresholdSeriesGeom.screenPts
|
|
780
785
|
: undefined,
|
|
786
|
+
thresholdBadgeLeftAnchorX:
|
|
787
|
+
thresholdIsSeries && thresholdCfg?.extendToStart === false
|
|
788
|
+
? thresholdSeriesGeom.clipLeftX
|
|
789
|
+
: undefined,
|
|
781
790
|
};
|
|
782
791
|
}
|
|
783
792
|
|
|
@@ -816,6 +825,7 @@ function useLiveChartThresholdModel({
|
|
|
816
825
|
engine,
|
|
817
826
|
padding,
|
|
818
827
|
samples: thresholdSeriesGeom.samples,
|
|
828
|
+
clipLeftX: thresholdSeriesGeom.clipLeftX,
|
|
819
829
|
clipRightX: thresholdSeriesGeom.clipRightX,
|
|
820
830
|
thresholdCfg,
|
|
821
831
|
palette,
|
|
@@ -1074,6 +1084,7 @@ function resolveLiveEngineModeInputs({
|
|
|
1074
1084
|
thresholdInRange && thresholdIsSeries
|
|
1075
1085
|
? (thresholdSeriesSV ?? (thresholdCfg?.value as LiveChartPoint[]))
|
|
1076
1086
|
: undefined,
|
|
1087
|
+
thresholdRangeExtendToStart: thresholdCfg?.extendToStart ?? true,
|
|
1077
1088
|
thresholdRangeExtendToNow: thresholdCfg?.extendToNow ?? true,
|
|
1078
1089
|
candles: isCandle ? candlesEngine : candles,
|
|
1079
1090
|
liveCandle: isCandle ? liveEngine : liveCandle,
|
|
@@ -1787,6 +1798,7 @@ function useLiveChartController({
|
|
|
1787
1798
|
thresholdMarkerValue,
|
|
1788
1799
|
thresholdCustomBadge,
|
|
1789
1800
|
thresholdSeriesPts,
|
|
1801
|
+
thresholdBadgeLeftAnchorX,
|
|
1790
1802
|
thresholdFillLineY,
|
|
1791
1803
|
thresholdFillSamples,
|
|
1792
1804
|
} = useLiveChartThresholdModel({
|
|
@@ -2214,6 +2226,7 @@ function useLiveChartController({
|
|
|
2214
2226
|
thresholdMarkerValue,
|
|
2215
2227
|
thresholdCustomBadge,
|
|
2216
2228
|
thresholdSeriesPts,
|
|
2229
|
+
thresholdBadgeLeftAnchorX,
|
|
2217
2230
|
badgeUsesRightGutter,
|
|
2218
2231
|
// theme / layout / fonts
|
|
2219
2232
|
palette,
|
|
@@ -3016,6 +3029,7 @@ function ChartStack({
|
|
|
3016
3029
|
thresholdMarkerValue,
|
|
3017
3030
|
thresholdCustomBadge,
|
|
3018
3031
|
thresholdSeriesPts,
|
|
3032
|
+
thresholdBadgeLeftAnchorX,
|
|
3019
3033
|
formatValue,
|
|
3020
3034
|
lineGroupOpacity,
|
|
3021
3035
|
seriesOpacity,
|
|
@@ -3185,6 +3199,7 @@ function ChartStack({
|
|
|
3185
3199
|
palette={palette}
|
|
3186
3200
|
font={skiaFont}
|
|
3187
3201
|
formatValue={formatValue}
|
|
3202
|
+
leftAnchorX={thresholdBadgeLeftAnchorX}
|
|
3188
3203
|
/>
|
|
3189
3204
|
)}
|
|
3190
3205
|
|
|
@@ -3767,6 +3782,7 @@ function ChartNativeOverlays({ model }: { model: LiveChartModel }) {
|
|
|
3767
3782
|
thresholdCfg,
|
|
3768
3783
|
thresholdMarkerLineY,
|
|
3769
3784
|
thresholdBadgeVisible,
|
|
3785
|
+
thresholdBadgeLeftAnchorX,
|
|
3770
3786
|
markersActive,
|
|
3771
3787
|
renderMarker,
|
|
3772
3788
|
renderReferenceLine,
|
|
@@ -3803,6 +3819,7 @@ function ChartNativeOverlays({ model }: { model: LiveChartModel }) {
|
|
|
3803
3819
|
y={thresholdMarkerLineY}
|
|
3804
3820
|
visible={thresholdBadgeVisible}
|
|
3805
3821
|
position={thresholdCfg.line.labelPosition}
|
|
3822
|
+
leftAnchorX={thresholdBadgeLeftAnchorX}
|
|
3806
3823
|
/>
|
|
3807
3824
|
) : null}
|
|
3808
3825
|
{hasAnnotations ? <ChartCustomAnnotations model={model} /> : null}
|
|
@@ -38,11 +38,13 @@ export function ReferenceLineSeriesOverlay({
|
|
|
38
38
|
badgeLayer: boolean;
|
|
39
39
|
}) {
|
|
40
40
|
const points = line.series ?? [];
|
|
41
|
+
const extendToStart = line.extendToStart ?? true;
|
|
41
42
|
const extendToNow = line.extendToNow ?? true;
|
|
42
43
|
const geometry = useReferenceLineSeries(
|
|
43
44
|
engine,
|
|
44
45
|
padding,
|
|
45
46
|
points,
|
|
47
|
+
extendToStart,
|
|
46
48
|
extendToNow,
|
|
47
49
|
);
|
|
48
50
|
const color = line.color ?? palette.refLine;
|
|
@@ -36,6 +36,8 @@ interface ThresholdMarkerProps {
|
|
|
36
36
|
/** When set, draw the marker as this time-varying threshold polyline (`[x, y, …]`)
|
|
37
37
|
* instead of a horizontal line at `lineY`. */
|
|
38
38
|
seriesPts?: SharedValue<number[]>;
|
|
39
|
+
/** Optional first rendered X for a non-extended series badge. */
|
|
40
|
+
leftAnchorX?: SharedValue<number>;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -126,6 +128,7 @@ export function ThresholdBadgeOverlay({
|
|
|
126
128
|
palette,
|
|
127
129
|
font,
|
|
128
130
|
formatValue,
|
|
131
|
+
leftAnchorX,
|
|
129
132
|
}: ThresholdMarkerProps) {
|
|
130
133
|
const { label, labelPosition, showValue } = cfg;
|
|
131
134
|
const badgeBackground = palette.tooltipBg;
|
|
@@ -155,12 +158,19 @@ export function ThresholdBadgeOverlay({
|
|
|
155
158
|
const pillW = useDerivedValue(
|
|
156
159
|
() => measureFontTextWidth(font, labelText.get()) + BADGE_PAD_X * 2,
|
|
157
160
|
);
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
// A non-extended series' left badge follows its first rendered point, while
|
|
162
|
+
// staying fully inside the plot near the live edge. Other left badges retain
|
|
163
|
+
// the legacy canvas-edge position.
|
|
164
|
+
const pillX = useDerivedValue(() => {
|
|
165
|
+
if (labelPosition === "right") {
|
|
166
|
+
return engine.canvasWidth.get() - padding.right - BADGE_EDGE_INSET - pillW.get();
|
|
167
|
+
}
|
|
168
|
+
if (!leftAnchorX) return BADGE_EDGE_INSET;
|
|
169
|
+
const minX = padding.left + BADGE_EDGE_INSET;
|
|
170
|
+
const maxX =
|
|
171
|
+
engine.canvasWidth.get() - padding.right - BADGE_EDGE_INSET - pillW.get();
|
|
172
|
+
return Math.max(minX, Math.min(leftAnchorX.get(), maxX));
|
|
173
|
+
});
|
|
164
174
|
const pillY = useDerivedValue(
|
|
165
175
|
() => lineY.get() - baselineOffset + fontAscent - BADGE_PAD_Y,
|
|
166
176
|
);
|
|
@@ -37,6 +37,9 @@ export function buildThresholdSplitShaderSource(sampleCount: number): string {
|
|
|
37
37
|
// and glides with the window so features move fluidly, frame to frame.
|
|
38
38
|
uniform float sampleLeft;
|
|
39
39
|
uniform float sampleRight;
|
|
40
|
+
// X where the threshold starts (extendToStart: false → the first point's
|
|
41
|
+
// pixel-X, else a large negative sentinel). Fragments left of it use restColor.
|
|
42
|
+
uniform float clipLeft;
|
|
40
43
|
// X where the threshold ends (extendToNow: false → the last point's pixel-X,
|
|
41
44
|
// else a huge sentinel). Fragments right of it paint restColor: the plain line
|
|
42
45
|
// color for the stroke, transparent for the band.
|
|
@@ -51,7 +54,7 @@ ${lookup}
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
half4 main(vec2 xy) {
|
|
54
|
-
if (xy.x > clipRight) {
|
|
57
|
+
if (xy.x < clipLeft || xy.x > clipRight) {
|
|
55
58
|
return half4(half3(restColor.rgb) * restColor.a, restColor.a);
|
|
56
59
|
}
|
|
57
60
|
float span = sampleRight - sampleLeft;
|
|
@@ -61,6 +61,9 @@ export interface EngineTickInput {
|
|
|
61
61
|
* but windowed each tick.
|
|
62
62
|
*/
|
|
63
63
|
thresholdRangePoints?: LiveChartPoint[];
|
|
64
|
+
/** Whether {@link thresholdRangePoints} extends flat before its first point to
|
|
65
|
+
* the visible window start (`threshold.extendToStart`). Default `true`. */
|
|
66
|
+
thresholdRangeExtendToStart?: boolean;
|
|
64
67
|
/** Whether {@link thresholdRangePoints} extends flat past its last point to
|
|
65
68
|
* "now" (`threshold.extendToNow`). Default `true`. */
|
|
66
69
|
thresholdRangeExtendToNow?: boolean;
|
|
@@ -367,6 +370,7 @@ export function tickLiveChartEngineFrame(
|
|
|
367
370
|
thrPts,
|
|
368
371
|
state.timestamp,
|
|
369
372
|
state.displayWindow,
|
|
373
|
+
input.thresholdRangeExtendToStart ?? true,
|
|
370
374
|
input.thresholdRangeExtendToNow ?? true,
|
|
371
375
|
THRESHOLD_RANGE_SCRATCH,
|
|
372
376
|
);
|
|
@@ -447,6 +447,8 @@ export interface ResolvedThresholdConfig {
|
|
|
447
447
|
fillOpacity: number;
|
|
448
448
|
/** Fold the threshold into the Y-axis range fit. */
|
|
449
449
|
includeInRange: boolean;
|
|
450
|
+
/** Series forms: extend flat before the first point to the visible window's start. */
|
|
451
|
+
extendToStart: boolean;
|
|
450
452
|
/** Series forms: extend flat past the last point to "now". */
|
|
451
453
|
extendToNow: boolean;
|
|
452
454
|
line: ResolvedThresholdLineConfig | null;
|
|
@@ -495,6 +497,7 @@ export function resolveThreshold(
|
|
|
495
497
|
(typeof prop.fill === "object" ? prop.fill.opacity : undefined) ??
|
|
496
498
|
THRESHOLD_FILL_OPACITY_DEFAULT,
|
|
497
499
|
includeInRange: prop.includeInRange ?? false,
|
|
500
|
+
extendToStart: prop.extendToStart ?? true,
|
|
498
501
|
extendToNow: prop.extendToNow ?? true,
|
|
499
502
|
line: resolveThresholdLine(prop.line),
|
|
500
503
|
};
|
|
@@ -54,6 +54,9 @@ export interface EngineConfig {
|
|
|
54
54
|
* `LiveChartPoint[]` form.
|
|
55
55
|
*/
|
|
56
56
|
thresholdRangePoints?: SharedValue<LiveChartPoint[]> | LiveChartPoint[];
|
|
57
|
+
/** With {@link thresholdRangePoints}: whether the series extends flat before
|
|
58
|
+
* its first point to the visible window start. Default `true`. */
|
|
59
|
+
thresholdRangeExtendToStart?: boolean;
|
|
57
60
|
/** With {@link thresholdRangePoints}: whether the series extends flat past its
|
|
58
61
|
* last point to "now" (`threshold.extendToNow`). Default `true`. */
|
|
59
62
|
thresholdRangeExtendToNow?: boolean;
|
|
@@ -217,6 +220,7 @@ export interface EngineFrameRefs {
|
|
|
217
220
|
referenceValues?: SharedValue<number[] | undefined>;
|
|
218
221
|
/** Series threshold folded into the range fit (`threshold.includeInRange`). */
|
|
219
222
|
thresholdRangePoints?: SharedValue<LiveChartPoint[] | undefined>;
|
|
223
|
+
thresholdRangeExtendToStart?: SharedValue<boolean>;
|
|
220
224
|
thresholdRangeExtendToNow?: SharedValue<boolean>;
|
|
221
225
|
nonNegativeSV?: SharedValue<boolean>;
|
|
222
226
|
maxValueSV?: SharedValue<number | undefined>;
|
|
@@ -353,6 +357,8 @@ export function applyLiveChartEngineFrame(
|
|
|
353
357
|
input.referenceValue = sv.referenceValue.value;
|
|
354
358
|
input.referenceValues = sv.referenceValues?.value;
|
|
355
359
|
input.thresholdRangePoints = sv.thresholdRangePoints?.value;
|
|
360
|
+
input.thresholdRangeExtendToStart =
|
|
361
|
+
sv.thresholdRangeExtendToStart?.value ?? true;
|
|
356
362
|
input.thresholdRangeExtendToNow = sv.thresholdRangeExtendToNow?.value ?? true;
|
|
357
363
|
input.nonNegative = sv.nonNegativeSV?.value ?? false;
|
|
358
364
|
input.maxValue = sv.maxValueSV?.value;
|
|
@@ -453,6 +459,9 @@ export function useLiveChartEngine(
|
|
|
453
459
|
const thresholdRangeExtendToNow = useDerivedValue(
|
|
454
460
|
() => config.thresholdRangeExtendToNow ?? true,
|
|
455
461
|
);
|
|
462
|
+
const thresholdRangeExtendToStart = useDerivedValue(
|
|
463
|
+
() => config.thresholdRangeExtendToStart ?? true,
|
|
464
|
+
);
|
|
456
465
|
const nonNegativeSV = useDerivedValue(() => config.nonNegative ?? false);
|
|
457
466
|
const maxValueSV = useDerivedValue(() => config.maxValue);
|
|
458
467
|
const nowOverrideSV = useDerivedValue(() => config.nowOverride);
|
|
@@ -559,6 +568,7 @@ export function useLiveChartEngine(
|
|
|
559
568
|
referenceValue,
|
|
560
569
|
referenceValues,
|
|
561
570
|
thresholdRangePoints,
|
|
571
|
+
thresholdRangeExtendToStart,
|
|
562
572
|
thresholdRangeExtendToNow,
|
|
563
573
|
nonNegativeSV,
|
|
564
574
|
maxValueSV,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type SkFont,
|
|
6
6
|
type SkFontMgr,
|
|
7
7
|
} from "@shopify/react-native-skia";
|
|
8
|
+
import { useMemo } from "react";
|
|
8
9
|
|
|
9
10
|
import { resolveFontConfig } from "../core/resolveConfig";
|
|
10
11
|
import type { FontConfig } from "../types";
|
|
@@ -15,7 +16,7 @@ import type { FontConfig } from "../types";
|
|
|
15
16
|
* fonts per render, so a screen full of sparklines would otherwise re-match
|
|
16
17
|
* the same `{family, size, weight}` dozens of times. `SkFont` instances are
|
|
17
18
|
* immutable and safe to share across canvases. Custom `fontManager`s bypass
|
|
18
|
-
*
|
|
19
|
+
* this module-wide cache; their fonts are memoized per hook instead.
|
|
19
20
|
*/
|
|
20
21
|
const systemFontCache = new Map<string, SkFont>();
|
|
21
22
|
let systemFontMgr: SkFontMgr | null = null;
|
|
@@ -38,6 +39,8 @@ function matchSystemFont(
|
|
|
38
39
|
* Resolves Skia text for charts: optional bundled `typeface` via `useFont`, otherwise
|
|
39
40
|
* `matchFont` with optional custom `fontManager`. System-font matches are cached
|
|
40
41
|
* module-wide, so many charts sharing a family/size/weight resolve it once.
|
|
42
|
+
* Custom-manager matches are reused while the manager and resolved inputs stay
|
|
43
|
+
* unchanged, even if the caller passes a new font config object each render.
|
|
41
44
|
*/
|
|
42
45
|
export function useChartSkiaFont(
|
|
43
46
|
fontProp: FontConfig | undefined,
|
|
@@ -49,12 +52,12 @@ export function useChartSkiaFont(
|
|
|
49
52
|
const typefaceSource = fontProp?.typeface ?? null;
|
|
50
53
|
const customFont = useFont(typefaceSource, fontSize);
|
|
51
54
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
const fontManager = fontProp?.fontManager;
|
|
56
|
+
return useMemo(() => {
|
|
57
|
+
// A loaded typeface needs no fallback, so avoid matching an unused font.
|
|
58
|
+
if (typefaceSource != null && customFont) return customFont;
|
|
59
|
+
return fontManager
|
|
60
|
+
? matchFont({ fontFamily, fontSize, fontWeight }, fontManager)
|
|
61
|
+
: matchSystemFont(fontFamily, fontSize, fontWeight);
|
|
62
|
+
}, [typefaceSource, customFont, fontManager, fontFamily, fontSize, fontWeight]);
|
|
60
63
|
}
|
|
@@ -34,6 +34,7 @@ export function useReferenceLineSeries(
|
|
|
34
34
|
engine: ChartEngineLayout,
|
|
35
35
|
padding: ChartPadding,
|
|
36
36
|
points: LiveChartPoint[],
|
|
37
|
+
extendToStart: boolean,
|
|
37
38
|
extendToNow: boolean,
|
|
38
39
|
): ReferenceLineSeriesGeometry {
|
|
39
40
|
const cacheRef = useRef<{
|
|
@@ -57,6 +58,7 @@ export function useReferenceLineSeries(
|
|
|
57
58
|
engine.canvasWidth.get(),
|
|
58
59
|
engine.canvasHeight.get(),
|
|
59
60
|
padding,
|
|
61
|
+
extendToStart,
|
|
60
62
|
extendToNow,
|
|
61
63
|
cache.tick ? cache.a : cache.b,
|
|
62
64
|
);
|
|
@@ -86,6 +88,9 @@ export function useReferenceLineSeries(
|
|
|
86
88
|
);
|
|
87
89
|
const currentVisible = useDerivedValue(() => {
|
|
88
90
|
if (points.length === 0) return false;
|
|
91
|
+
if (!extendToStart && points[0].time > engine.timestamp.get()) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
89
94
|
if (!extendToNow && points[points.length - 1].time < engine.timestamp.get()) {
|
|
90
95
|
return false;
|
|
91
96
|
}
|
|
@@ -22,6 +22,8 @@ export type ThresholdValue = SharedValue<number> | LiveChartPoint[];
|
|
|
22
22
|
|
|
23
23
|
/** `clipRight` sentinel when the threshold extends to "now" (no forward cutoff). */
|
|
24
24
|
export const THRESHOLD_NO_CLIP = 1e9;
|
|
25
|
+
/** `clipLeft` sentinel when the threshold extends to the window start. */
|
|
26
|
+
export const THRESHOLD_NO_CLIP_LEFT = -1e9;
|
|
25
27
|
|
|
26
28
|
export interface ThresholdGeometry {
|
|
27
29
|
/** Threshold pixel-Y within the canvas, or NaN before layout / degenerate range. */
|
|
@@ -104,6 +106,9 @@ export interface ThresholdSeriesGeometry {
|
|
|
104
106
|
* part of the threshold. (`visible` can still be true when that one endpoint
|
|
105
107
|
* is off-plot.) */
|
|
106
108
|
badgeVisible: SharedValue<boolean>;
|
|
109
|
+
/** Pixel-X where the threshold starts: the first point's X with
|
|
110
|
+
* `extendToStart` off, else {@link THRESHOLD_NO_CLIP_LEFT}. */
|
|
111
|
+
clipLeftX: SharedValue<number>;
|
|
107
112
|
/** Pixel-X where the threshold ends: the last point's X with `extendToNow`
|
|
108
113
|
* off, else {@link THRESHOLD_NO_CLIP}. The shader paints its plain
|
|
109
114
|
* `restColor` right of it; the marker polyline stops there. */
|
|
@@ -119,7 +124,7 @@ const EMPTY_SAMPLES: number[] = new Array(THRESHOLD_SAMPLE_COUNT).fill(0);
|
|
|
119
124
|
* `LiveChartPoint[]` `value` or a live `SharedValue<LiveChartPoint[]>` `series`
|
|
120
125
|
* (which wins when both are given): the screen polyline (marker line +
|
|
121
126
|
* fill-band bottom), the shader's pixel-Y `samples[]`, the configured endpoint
|
|
122
|
-
* value/anchor for the badge, and the
|
|
127
|
+
* value/anchor for the badge, and the endpoint cutoff X values. The array buffers
|
|
123
128
|
* ping-pong (Reanimated only re-notifies subscribers when the returned reference
|
|
124
129
|
* changes). When the threshold is a constant `SharedValue<number>` every
|
|
125
130
|
* worklet short-circuits cheaply and {@link useThreshold} drives the render
|
|
@@ -131,6 +136,7 @@ export function useThresholdSeries(
|
|
|
131
136
|
value: ThresholdValue,
|
|
132
137
|
series: SharedValue<LiveChartPoint[]> | null = null,
|
|
133
138
|
extendToNow = true,
|
|
139
|
+
extendToStart = true,
|
|
134
140
|
labelAnchor: "first" | "last" = "last",
|
|
135
141
|
): ThresholdSeriesGeometry {
|
|
136
142
|
const cacheRef = useRef<{
|
|
@@ -176,6 +182,19 @@ export function useThresholdSeries(
|
|
|
176
182
|
);
|
|
177
183
|
});
|
|
178
184
|
|
|
185
|
+
const clipLeftX = useDerivedValue(() => {
|
|
186
|
+
if (extendToStart) return THRESHOLD_NO_CLIP_LEFT;
|
|
187
|
+
const pts = series ? series.get() : Array.isArray(value) ? value : null;
|
|
188
|
+
if (pts === null || pts.length === 0) return THRESHOLD_NO_CLIP_LEFT;
|
|
189
|
+
const win = engine.displayWindow.get();
|
|
190
|
+
const plotLeft = padding.left;
|
|
191
|
+
const plotRight = engine.canvasWidth.get() - padding.right;
|
|
192
|
+
if (!(win > 0) || plotRight <= plotLeft) return THRESHOLD_NO_CLIP_LEFT;
|
|
193
|
+
const winStart = engine.timestamp.get() - win;
|
|
194
|
+
const firstT = pts[0].time;
|
|
195
|
+
return plotLeft + ((firstT - winStart) / win) * (plotRight - plotLeft);
|
|
196
|
+
});
|
|
197
|
+
|
|
179
198
|
const clipRightX = useDerivedValue(() => {
|
|
180
199
|
if (extendToNow) return THRESHOLD_NO_CLIP;
|
|
181
200
|
const pts = series ? series.get() : Array.isArray(value) ? value : null;
|
|
@@ -206,8 +225,9 @@ export function useThresholdSeries(
|
|
|
206
225
|
if (n < 2) return EMPTY_PTS;
|
|
207
226
|
const plotLeft = padding.left;
|
|
208
227
|
const plotRight = engine.canvasWidth.get() - padding.right;
|
|
228
|
+
const startX = Math.max(plotLeft, clipLeftX.get());
|
|
209
229
|
const endX = Math.min(plotRight, clipRightX.get());
|
|
210
|
-
if (endX <=
|
|
230
|
+
if (endX <= startX) return EMPTY_PTS;
|
|
211
231
|
const cache = cacheRef.current!;
|
|
212
232
|
cache.ptsTick = !cache.ptsTick;
|
|
213
233
|
const buf = cache.ptsTick ? cache.ptsA : cache.ptsB;
|
|
@@ -219,11 +239,11 @@ export function useThresholdSeries(
|
|
|
219
239
|
plotRight,
|
|
220
240
|
n,
|
|
221
241
|
);
|
|
222
|
-
buf.push(
|
|
242
|
+
buf.push(startX, sampleThresholdYAt(s, x0, x1, startX));
|
|
223
243
|
const step = (x1 - x0) / (n - 1);
|
|
224
244
|
for (let i = 0; i < n; i++) {
|
|
225
245
|
const sx = x0 + step * i;
|
|
226
|
-
if (sx >
|
|
246
|
+
if (sx > startX && sx < endX) buf.push(sx, s[i]);
|
|
227
247
|
}
|
|
228
248
|
buf.push(endX, sampleThresholdYAt(s, x0, x1, endX));
|
|
229
249
|
return buf;
|
|
@@ -242,10 +262,13 @@ export function useThresholdSeries(
|
|
|
242
262
|
const badgeValue = useDerivedValue(() => {
|
|
243
263
|
const pts = series ? series.get() : Array.isArray(value) ? value : null;
|
|
244
264
|
if (pts === null) return NaN;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
265
|
+
let time = engine.timestamp.get();
|
|
266
|
+
if (labelAnchor === "first") {
|
|
267
|
+
time -= engine.displayWindow.get();
|
|
268
|
+
if (!extendToStart && pts.length > 0 && pts[0].time > time) {
|
|
269
|
+
time = pts[0].time;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
249
272
|
return interpolateAtTime(pts, time) ?? NaN;
|
|
250
273
|
});
|
|
251
274
|
|
|
@@ -261,11 +284,11 @@ export function useThresholdSeries(
|
|
|
261
284
|
);
|
|
262
285
|
|
|
263
286
|
const badgeVisible = useDerivedValue(() => {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
287
|
+
// There is no endpoint to label when the series does not intersect the
|
|
288
|
+
// effective [first point, last point] window, even though interpolation can
|
|
289
|
+
// still clamp a value outside it.
|
|
290
|
+
if (screenPts.get().length < 4) return false;
|
|
291
|
+
if (labelAnchor === "last" && !extendToNow) {
|
|
269
292
|
// The threshold ends at its last point — no badge past it.
|
|
270
293
|
const pts = series ? series.get() : Array.isArray(value) ? value : null;
|
|
271
294
|
if (pts === null || pts.length === 0) return false;
|
|
@@ -286,6 +309,7 @@ export function useThresholdSeries(
|
|
|
286
309
|
badgeValue,
|
|
287
310
|
badgeLineY,
|
|
288
311
|
badgeVisible,
|
|
312
|
+
clipLeftX,
|
|
289
313
|
clipRightX,
|
|
290
314
|
};
|
|
291
315
|
}
|
|
@@ -295,8 +319,8 @@ export function useThresholdSeries(
|
|
|
295
319
|
* uniforms (one `SharedValue` per paint — stroke vs. the alpha-reduced fill band).
|
|
296
320
|
* The object is rebuilt each frame so the shader re-paints as `samples` advance;
|
|
297
321
|
* `sampleLeft`/`sampleRight` are the gliding pixel-X span of the sample grid and
|
|
298
|
-
* `clipRight`/`restColor` implement the
|
|
299
|
-
* color for the stroke, transparent for the band).
|
|
322
|
+
* `clipLeft`/`clipRight`/`restColor` implement the series endpoint cutoffs
|
|
323
|
+
* (plain line color for the stroke, transparent for the band).
|
|
300
324
|
*/
|
|
301
325
|
export function useThresholdSplitUniforms(
|
|
302
326
|
samples: SharedValue<number[]>,
|
|
@@ -305,6 +329,7 @@ export function useThresholdSplitUniforms(
|
|
|
305
329
|
aboveColor: number[],
|
|
306
330
|
belowColor: number[],
|
|
307
331
|
restColor: number[],
|
|
332
|
+
clipLeftX: SharedValue<number>,
|
|
308
333
|
clipRightX: SharedValue<number>,
|
|
309
334
|
): SharedValue<Uniforms> {
|
|
310
335
|
return useDerivedValue<Uniforms>(() => {
|
|
@@ -319,6 +344,7 @@ export function useThresholdSplitUniforms(
|
|
|
319
344
|
return {
|
|
320
345
|
sampleLeft: x0,
|
|
321
346
|
sampleRight: x1,
|
|
347
|
+
clipLeft: clipLeftX.get(),
|
|
322
348
|
clipRight: clipRightX.get(),
|
|
323
349
|
aboveColor,
|
|
324
350
|
belowColor,
|
|
@@ -20,6 +20,7 @@ export function buildReferenceLineSeriesPoints(
|
|
|
20
20
|
canvasWidth: number,
|
|
21
21
|
canvasHeight: number,
|
|
22
22
|
padding: ChartPadding,
|
|
23
|
+
extendToStart: boolean,
|
|
23
24
|
extendToNow: boolean,
|
|
24
25
|
out?: number[],
|
|
25
26
|
): number[] {
|
|
@@ -43,11 +44,15 @@ export function buildReferenceLineSeriesPoints(
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
const windowStart = now - windowSecs;
|
|
47
|
+
const firstTime = points[0].time;
|
|
46
48
|
const lastTime = points[points.length - 1].time;
|
|
49
|
+
const startTime = extendToStart
|
|
50
|
+
? windowStart
|
|
51
|
+
: Math.max(windowStart, firstTime);
|
|
47
52
|
const endTime = extendToNow ? now : Math.min(now, lastTime);
|
|
48
|
-
if (endTime <
|
|
53
|
+
if (endTime < startTime) return result;
|
|
49
54
|
|
|
50
|
-
const startValue = interpolateAtTime(points,
|
|
55
|
+
const startValue = interpolateAtTime(points, startTime);
|
|
51
56
|
const endValue = interpolateAtTime(points, endTime);
|
|
52
57
|
if (
|
|
53
58
|
startValue === null ||
|
|
@@ -70,10 +75,10 @@ export function buildReferenceLineSeriesPoints(
|
|
|
70
75
|
padding.bottom,
|
|
71
76
|
);
|
|
72
77
|
|
|
73
|
-
result.push(
|
|
78
|
+
result.push(toX(startTime), toY(startValue));
|
|
74
79
|
for (let i = 0; i < points.length; i++) {
|
|
75
80
|
const point = points[i];
|
|
76
|
-
if (point.time <=
|
|
81
|
+
if (point.time <= startTime) continue;
|
|
77
82
|
if (point.time >= endTime) break;
|
|
78
83
|
if (!Number.isFinite(point.time) || !Number.isFinite(point.value)) continue;
|
|
79
84
|
result.push(toX(point.time), toY(point.value));
|
package/src/math/threshold.ts
CHANGED
|
@@ -237,8 +237,9 @@ export function thresholdDashPhase(
|
|
|
237
237
|
* Min/max of a threshold series over the visible window `[now - windowSecs,
|
|
238
238
|
* now]` — the values folded into the engine's Y-range fit when
|
|
239
239
|
* `threshold.includeInRange` is set (like reference-line values). Uses the
|
|
240
|
-
* clamped window-edge values plus every interior point
|
|
241
|
-
* the window
|
|
240
|
+
* clamped window-edge values plus every interior point. With `extendToStart`
|
|
241
|
+
* off, the window start clamps to the series' first point; with `extendToNow`
|
|
242
|
+
* off, the window end clamps to its last point.
|
|
242
243
|
* Writes `[min, max]` into `out` and returns it, or returns null when the
|
|
243
244
|
* series is empty / entirely outside the effective window / non-finite.
|
|
244
245
|
*/
|
|
@@ -246,18 +247,24 @@ export function thresholdRangeMinMax(
|
|
|
246
247
|
points: LiveChartPoint[],
|
|
247
248
|
now: number,
|
|
248
249
|
windowSecs: number,
|
|
250
|
+
extendToStart: boolean,
|
|
249
251
|
extendToNow: boolean,
|
|
250
252
|
out: [number, number],
|
|
251
253
|
): [number, number] | null {
|
|
252
254
|
"worklet";
|
|
253
255
|
if (points.length === 0 || !(windowSecs > 0)) return null;
|
|
254
|
-
|
|
256
|
+
let tStart = now - windowSecs;
|
|
255
257
|
let tEnd = now;
|
|
258
|
+
if (!extendToStart) {
|
|
259
|
+
const firstT = points[0].time;
|
|
260
|
+
if (firstT > tStart) tStart = firstT;
|
|
261
|
+
}
|
|
256
262
|
if (!extendToNow) {
|
|
257
263
|
const lastT = points[points.length - 1].time;
|
|
258
264
|
if (lastT < tStart) return null;
|
|
259
265
|
if (lastT < tEnd) tEnd = lastT;
|
|
260
266
|
}
|
|
267
|
+
if (tEnd < tStart) return null;
|
|
261
268
|
let mn = interpolateAtTime(points, tStart)!;
|
|
262
269
|
let mx = mn;
|
|
263
270
|
const vEnd = interpolateAtTime(points, tEnd)!;
|
package/src/types.ts
CHANGED
|
@@ -74,10 +74,16 @@ export interface ReferenceLine {
|
|
|
74
74
|
/**
|
|
75
75
|
* Form B — a time-varying reference line. Points use unix-second timestamps
|
|
76
76
|
* and should be sorted oldest to newest. The first value extends to the left
|
|
77
|
-
* edge
|
|
78
|
-
* `false`. Supported by line and
|
|
77
|
+
* edge unless {@link extendToStart} is `false`; the last value extends to the
|
|
78
|
+
* live edge unless {@link extendToNow} is `false`. Supported by line and
|
|
79
|
+
* candle charts.
|
|
79
80
|
*/
|
|
80
81
|
series?: LiveChartPoint[];
|
|
82
|
+
/**
|
|
83
|
+
* Form B — extend the series' first value flat to the chart's left edge.
|
|
84
|
+
* Set `false` to start at the first point. Default `true`.
|
|
85
|
+
*/
|
|
86
|
+
extendToStart?: boolean;
|
|
81
87
|
/**
|
|
82
88
|
* Form B — extend the series' last value flat to the chart's live edge.
|
|
83
89
|
* Set `false` to stop at the last point. Default `true`.
|
|
@@ -493,8 +499,8 @@ export interface ThresholdConfig {
|
|
|
493
499
|
* - **`LiveChartPoint[]`** — a *time-varying* threshold (e.g. a historical
|
|
494
500
|
* break-even that steps up as you average in). The stroke split, fill band
|
|
495
501
|
* and marker line follow the series point-for-point. The series clamps to its
|
|
496
|
-
* first/last value outside its own time range, so
|
|
497
|
-
*
|
|
502
|
+
* first/last value outside its own time range, so it extends flat to the
|
|
503
|
+
* visible edges by default (see {@link extendToStart} and
|
|
498
504
|
* {@link extendToNow}). Flows in on re-render — pass a stable (memoized)
|
|
499
505
|
* array and reserve it for thresholds that change occasionally; for a
|
|
500
506
|
* threshold series that updates live, use {@link series} instead.
|
|
@@ -531,10 +537,19 @@ export interface ThresholdConfig {
|
|
|
531
537
|
* Fold the threshold into the Y-axis range fit — like reference lines — so a
|
|
532
538
|
* benchmark outside the data's own range stays on-plot instead of rendering
|
|
533
539
|
* invisibly (marker off-plot, whole line one color). For a series, the values
|
|
534
|
-
* visible in the current window count (respecting {@link
|
|
540
|
+
* visible in the current window count (respecting {@link extendToStart} and
|
|
541
|
+
* {@link extendToNow}).
|
|
535
542
|
* Default `false` (range fits the data only).
|
|
536
543
|
*/
|
|
537
544
|
includeInRange?: boolean;
|
|
545
|
+
/**
|
|
546
|
+
* Series forms only: extend the threshold **flat before its first point to the
|
|
547
|
+
* visible window's left edge**, carrying the first known benchmark backward.
|
|
548
|
+
* Set `false` when the benchmark did not exist yet (for example, a break-even
|
|
549
|
+
* before the first trade) — left of the first point the stroke keeps its plain
|
|
550
|
+
* line color and the band / marker do not begin. Default `true`.
|
|
551
|
+
*/
|
|
552
|
+
extendToStart?: boolean;
|
|
538
553
|
/**
|
|
539
554
|
* Series forms only: extend the threshold **flat past its last point to
|
|
540
555
|
* "now"**, carrying the last known benchmark forward. Set `false` for a
|