react-native-livechart 3.12.0 → 4.1.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/BadgeOverlay.d.ts +9 -1
- package/dist/components/BadgeOverlay.d.ts.map +1 -1
- package/dist/components/ChartOverlayLayer.d.ts +19 -0
- package/dist/components/ChartOverlayLayer.d.ts.map +1 -0
- package/dist/components/CustomMarkerOverlay.d.ts +16 -6
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +36 -0
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +5 -2
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +7 -1
- package/dist/components/DotOverlay.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/MarkerOverlay.d.ts +6 -3
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +4 -1
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/ReferenceLineGroupOverlay.d.ts +27 -0
- package/dist/components/ReferenceLineGroupOverlay.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +25 -2
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/XAxisOverlay.d.ts +7 -1
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +12 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +13 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +29 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts +18 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +62 -2
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +58 -7
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +8 -2
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/grid.d.ts +13 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +20 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/draw/volume.d.ts +25 -0
- package/dist/draw/volume.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +12 -6
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/overlayScale.d.ts +22 -0
- package/dist/hooks/overlayScale.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +9 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
- package/dist/hooks/useBadge.d.ts +22 -1
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +6 -0
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartOverlayContext.d.ts +45 -0
- package/dist/hooks/useChartOverlayContext.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +8 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -2
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useLiveDot.d.ts +6 -1
- package/dist/hooks/useLiveDot.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +9 -4
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +82 -0
- package/dist/hooks/usePanScroll.d.ts.map +1 -0
- package/dist/hooks/usePinchZoom.d.ts +74 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -0
- package/dist/hooks/useReferenceDrag.d.ts +24 -0
- package/dist/hooks/useReferenceDrag.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +23 -4
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLinePress.d.ts +5 -1
- package/dist/hooks/useReferenceLinePress.d.ts.map +1 -1
- package/dist/hooks/useVisibleRange.d.ts +39 -0
- package/dist/hooks/useVisibleRange.d.ts.map +1 -0
- package/dist/hooks/useYAxis.d.ts +1 -1
- package/dist/hooks/useYAxis.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/markerCluster.d.ts +47 -0
- package/dist/math/markerCluster.d.ts.map +1 -0
- package/dist/math/markers.d.ts +10 -0
- package/dist/math/markers.d.ts.map +1 -1
- package/dist/math/referenceDrag.d.ts +24 -0
- package/dist/math/referenceDrag.d.ts.map +1 -0
- package/dist/math/referenceGroup.d.ts +31 -0
- package/dist/math/referenceGroup.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +33 -7
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/types.d.ts +524 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BadgeOverlay.tsx +26 -1
- package/src/components/ChartOverlayLayer.tsx +32 -0
- package/src/components/CustomMarkerOverlay.tsx +150 -57
- package/src/components/CustomReferenceLineOverlay.tsx +250 -0
- package/src/components/CustomTooltipOverlay.tsx +27 -7
- package/src/components/DotOverlay.tsx +9 -0
- package/src/components/LiveChart.tsx +550 -32
- package/src/components/LiveChartSeries.tsx +111 -5
- package/src/components/MarkerOverlay.tsx +92 -6
- package/src/components/MultiSeriesDots.tsx +12 -3
- package/src/components/ReferenceLineGroupOverlay.tsx +207 -0
- package/src/components/ReferenceLineOverlay.tsx +105 -32
- package/src/components/XAxisOverlay.tsx +9 -2
- package/src/components/YAxisOverlay.tsx +49 -28
- package/src/constants.ts +17 -0
- package/src/core/liveChartEngineTick.ts +81 -8
- package/src/core/liveChartSeriesEngineTick.ts +50 -9
- package/src/core/resolveConfig.ts +133 -2
- package/src/core/useLiveChartEngine.ts +111 -11
- package/src/core/useLiveChartSeriesEngine.ts +26 -3
- package/src/draw/grid.ts +71 -2
- package/src/draw/markerAtlas.ts +117 -3
- package/src/draw/volume.ts +190 -0
- package/src/hooks/crosshairShared.ts +37 -9
- package/src/hooks/overlayScale.ts +59 -0
- package/src/hooks/resolveChartLayout.ts +33 -0
- package/src/hooks/useBadge.ts +89 -21
- package/src/hooks/useCandlePaths.ts +104 -7
- package/src/hooks/useChartOverlayContext.ts +93 -0
- package/src/hooks/useChartPaths.ts +14 -1
- package/src/hooks/useCrosshair.ts +33 -2
- package/src/hooks/useLiveDot.ts +16 -5
- package/src/hooks/useMarkers.ts +39 -11
- package/src/hooks/usePanScroll.ts +253 -0
- package/src/hooks/usePinchZoom.ts +189 -0
- package/src/hooks/useReferenceDrag.ts +254 -0
- package/src/hooks/useReferenceLine.ts +56 -9
- package/src/hooks/useReferenceLinePress.ts +16 -2
- package/src/hooks/useVisibleRange.ts +119 -0
- package/src/hooks/useYAxis.ts +2 -0
- package/src/index.ts +17 -1
- package/src/math/markerCluster.ts +214 -0
- package/src/math/markers.ts +35 -3
- package/src/math/referenceDrag.ts +66 -0
- package/src/math/referenceGroup.ts +65 -0
- package/src/math/referenceLines.ts +76 -19
- package/src/types.ts +544 -21
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
LineStyleConfig,
|
|
15
15
|
LiveChartMetrics,
|
|
16
16
|
LiveChartMetricsOverride,
|
|
17
|
+
MarkerClusterConfig,
|
|
17
18
|
DotConfig,
|
|
18
19
|
DotRingConfig,
|
|
19
20
|
MultiSeriesDotConfig,
|
|
@@ -28,12 +29,15 @@ import type {
|
|
|
28
29
|
ThresholdLineConfig,
|
|
29
30
|
TradeEvent,
|
|
30
31
|
ValueLineConfig,
|
|
32
|
+
VolumeConfig,
|
|
31
33
|
XAxisConfig,
|
|
32
34
|
YAxisConfig,
|
|
35
|
+
ZoomConfig,
|
|
33
36
|
} from "../types";
|
|
34
37
|
|
|
35
38
|
import type { ComponentType, ReactElement } from "react";
|
|
36
39
|
import type { SharedValue } from "react-native-reanimated";
|
|
40
|
+
import type { ResolvedMarkerCluster } from "../math/markerCluster";
|
|
37
41
|
import {
|
|
38
42
|
BADGE_METRICS_DEFAULTS,
|
|
39
43
|
CANDLE_METRICS_DEFAULTS,
|
|
@@ -57,10 +61,41 @@ export interface ResolvedBadgeConfig {
|
|
|
57
61
|
tail: boolean;
|
|
58
62
|
position: "right" | "left";
|
|
59
63
|
background: string | undefined;
|
|
64
|
+
/** undefined → capsule (pillHeight / 2) at render time */
|
|
65
|
+
radius: number | undefined;
|
|
66
|
+
/** undefined → no border */
|
|
67
|
+
borderColor: string | undefined;
|
|
68
|
+
borderWidth: number;
|
|
69
|
+
/** undefined → variant/theme text color at render time */
|
|
70
|
+
textColor: string | undefined;
|
|
71
|
+
fontSize: number | undefined;
|
|
72
|
+
fontFamily: string | undefined;
|
|
73
|
+
fontWeight: FontWeight | undefined;
|
|
74
|
+
offsetX: number;
|
|
75
|
+
offsetY: number;
|
|
76
|
+
/** Track the visible window's right-edge price while scrolled back. */
|
|
77
|
+
followViewEdge: boolean;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
export interface ResolvedYAxisConfig {
|
|
63
81
|
minGap: number;
|
|
82
|
+
/** Fixed label count (≥ 2), or 0 for the dynamic nice-interval grid. */
|
|
83
|
+
count: number;
|
|
84
|
+
/** Float the axis over a full-width plot (no reserved right gutter). */
|
|
85
|
+
float: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ResolvedVolumeConfig {
|
|
89
|
+
/** undefined → use palette.candleUp at render time. */
|
|
90
|
+
upColor: string | undefined;
|
|
91
|
+
/** undefined → use palette.candleDown at render time. */
|
|
92
|
+
downColor: string | undefined;
|
|
93
|
+
/** Reserved band height (px) — the tallest a bar can be. */
|
|
94
|
+
maxHeight: number;
|
|
95
|
+
/** Corner radius (px) of bar tops. */
|
|
96
|
+
radius: number;
|
|
97
|
+
/** Opacity (0..1) applied to the whole band. */
|
|
98
|
+
opacity: number;
|
|
64
99
|
}
|
|
65
100
|
|
|
66
101
|
/** Resolved straight-line styling (connector, etc.). `color: undefined` → caller default. */
|
|
@@ -119,7 +154,7 @@ export interface ResolvedScrubConfig {
|
|
|
119
154
|
/** Tooltip pill corner radius in px. */
|
|
120
155
|
tooltipBorderRadius: number;
|
|
121
156
|
/** Where the tooltip pill sits relative to the scrub line. */
|
|
122
|
-
tooltipPlacement: "side" | "top" | "bottom";
|
|
157
|
+
tooltipPlacement: "side" | "top" | "bottom" | "point";
|
|
123
158
|
/** Gap (px) between the tooltip and the plot edge it's pinned to. */
|
|
124
159
|
tooltipMargin: number;
|
|
125
160
|
/** Show the value row in the default tooltip body. */
|
|
@@ -146,6 +181,8 @@ export interface ResolvedScrubActionConfig {
|
|
|
146
181
|
/** undefined → no rounding */
|
|
147
182
|
snap: number | undefined;
|
|
148
183
|
dismissOnTapOutside: boolean;
|
|
184
|
+
/** Clear the reticle once the action badge fires onScrubAction. */
|
|
185
|
+
dismissOnAction: boolean;
|
|
149
186
|
}
|
|
150
187
|
|
|
151
188
|
export interface ResolvedGradientConfig {
|
|
@@ -336,6 +373,16 @@ const BADGE_DEFAULTS: ResolvedBadgeConfig = {
|
|
|
336
373
|
tail: true,
|
|
337
374
|
position: "right",
|
|
338
375
|
background: undefined,
|
|
376
|
+
radius: undefined,
|
|
377
|
+
borderColor: undefined,
|
|
378
|
+
borderWidth: 1,
|
|
379
|
+
textColor: undefined,
|
|
380
|
+
fontSize: undefined,
|
|
381
|
+
fontFamily: undefined,
|
|
382
|
+
fontWeight: undefined,
|
|
383
|
+
offsetX: 0,
|
|
384
|
+
offsetY: 0,
|
|
385
|
+
followViewEdge: false,
|
|
339
386
|
};
|
|
340
387
|
|
|
341
388
|
/**
|
|
@@ -350,16 +397,63 @@ export function resolveBadge(
|
|
|
350
397
|
|
|
351
398
|
const Y_AXIS_DEFAULTS: ResolvedYAxisConfig = {
|
|
352
399
|
minGap: 36,
|
|
400
|
+
count: 0,
|
|
401
|
+
float: false,
|
|
353
402
|
};
|
|
354
403
|
|
|
355
404
|
/**
|
|
356
405
|
* Resolves `yAxis` prop to a fully-typed config or null (disabled).
|
|
357
406
|
* `true` → defaults, object → merged with defaults, falsy → null.
|
|
407
|
+
* `count` is normalized to a non-negative integer (the grid math clamps the
|
|
408
|
+
* upper bound to the label pool size).
|
|
358
409
|
*/
|
|
359
410
|
export function resolveYAxis(
|
|
360
411
|
prop: boolean | YAxisConfig | undefined,
|
|
361
412
|
): ResolvedYAxisConfig | null {
|
|
362
|
-
|
|
413
|
+
const resolved = resolveToggle(prop, Y_AXIS_DEFAULTS, false);
|
|
414
|
+
if (resolved === null) return null;
|
|
415
|
+
return { ...resolved, count: Math.max(0, Math.floor(resolved.count)) };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const VOLUME_DEFAULTS: ResolvedVolumeConfig = {
|
|
419
|
+
upColor: undefined,
|
|
420
|
+
downColor: undefined,
|
|
421
|
+
maxHeight: 48,
|
|
422
|
+
radius: 2,
|
|
423
|
+
opacity: 0.6,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Resolves the `volume` prop to a fully-typed config or null (disabled).
|
|
428
|
+
* `true` → defaults, object → merged with defaults, falsy → null. Colors left
|
|
429
|
+
* `undefined` fall back to the candle palette at render time.
|
|
430
|
+
*/
|
|
431
|
+
export function resolveVolume(
|
|
432
|
+
prop: boolean | VolumeConfig | undefined,
|
|
433
|
+
): ResolvedVolumeConfig | null {
|
|
434
|
+
return resolveToggle(prop, VOLUME_DEFAULTS, false);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export interface ResolvedZoomConfig {
|
|
438
|
+
/** Tightest window (max zoom-in), seconds. `undefined` → `timeWindow / 8`. */
|
|
439
|
+
minTimeWindow: number | undefined;
|
|
440
|
+
/** Widest window (max zoom-out), seconds. `undefined` → full data span. */
|
|
441
|
+
maxTimeWindow: number | undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const ZOOM_DEFAULTS: ResolvedZoomConfig = {
|
|
445
|
+
minTimeWindow: undefined,
|
|
446
|
+
maxTimeWindow: undefined,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Resolves the `zoom` prop to a config or null (disabled). `true` → defaults
|
|
451
|
+
* (bounds derived at gesture time), object → merged, falsy → null.
|
|
452
|
+
*/
|
|
453
|
+
export function resolveZoom(
|
|
454
|
+
prop: boolean | ZoomConfig | undefined,
|
|
455
|
+
): ResolvedZoomConfig | null {
|
|
456
|
+
return resolveToggle(prop, ZOOM_DEFAULTS, false);
|
|
363
457
|
}
|
|
364
458
|
|
|
365
459
|
const AXIS_LABEL_DEFAULTS: ResolvedAxisLabelConfig = {
|
|
@@ -484,6 +578,7 @@ const SCRUB_ACTION_DEFAULTS: ResolvedScrubActionConfig = {
|
|
|
484
578
|
timeBadge: false,
|
|
485
579
|
snap: undefined,
|
|
486
580
|
dismissOnTapOutside: false,
|
|
581
|
+
dismissOnAction: false,
|
|
487
582
|
};
|
|
488
583
|
|
|
489
584
|
/**
|
|
@@ -496,6 +591,42 @@ export function resolveScrubAction(
|
|
|
496
591
|
return resolveToggle(prop, SCRUB_ACTION_DEFAULTS, false);
|
|
497
592
|
}
|
|
498
593
|
|
|
594
|
+
/** Spacing + collapse-threshold defaults for `markerCluster: "stacked"`. */
|
|
595
|
+
const MARKER_CLUSTER_GAP = 2;
|
|
596
|
+
const MARKER_CLUSTER_MAX_BEFORE_GROUP = 5;
|
|
597
|
+
const MARKER_CLUSTER_OVERLAP = 0.75;
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Resolves the `markerCluster` prop (a `"anchored"`/`"stacked"` shorthand or a
|
|
601
|
+
* {@link MarkerClusterConfig} object) to a full config. Always returns a config
|
|
602
|
+
* (never null) — `"anchored"` (the default) is a valid mode the cluster pass
|
|
603
|
+
* treats as "side offsets only, no bucketing". The object form implies
|
|
604
|
+
* `"stacked"` unless `mode` is set.
|
|
605
|
+
*/
|
|
606
|
+
export function resolveMarkerCluster(
|
|
607
|
+
prop: "anchored" | "stacked" | MarkerClusterConfig | undefined,
|
|
608
|
+
): ResolvedMarkerCluster {
|
|
609
|
+
if (typeof prop === "object") {
|
|
610
|
+
return {
|
|
611
|
+
mode: prop.mode ?? "stacked",
|
|
612
|
+
overlap: clamp01(prop.overlap ?? MARKER_CLUSTER_OVERLAP),
|
|
613
|
+
gap: MARKER_CLUSTER_GAP,
|
|
614
|
+
maxBeforeGroup: prop.maxBeforeGroup ?? MARKER_CLUSTER_MAX_BEFORE_GROUP,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
mode: prop === "stacked" ? "stacked" : "anchored",
|
|
619
|
+
overlap: MARKER_CLUSTER_OVERLAP,
|
|
620
|
+
gap: MARKER_CLUSTER_GAP,
|
|
621
|
+
maxBeforeGroup: MARKER_CLUSTER_MAX_BEFORE_GROUP,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Clamp to [0, 0.95] so a glyph never fully covers its neighbor (overlap 1). */
|
|
626
|
+
function clamp01(v: number): number {
|
|
627
|
+
return v < 0 ? 0 : v > 0.95 ? 0.95 : v;
|
|
628
|
+
}
|
|
629
|
+
|
|
499
630
|
const GRADIENT_DEFAULTS: ResolvedGradientConfig = {
|
|
500
631
|
topOpacity: undefined,
|
|
501
632
|
bottomOpacity: undefined,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @see https://github.com/benjitaylor/liveline
|
|
7
7
|
*/
|
|
8
|
-
import { useState } from "react";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
9
|
import {
|
|
10
10
|
useAnimatedReaction,
|
|
11
11
|
useDerivedValue,
|
|
@@ -27,6 +27,13 @@ export interface EngineConfig {
|
|
|
27
27
|
exaggerate?: boolean;
|
|
28
28
|
referenceValue?: number;
|
|
29
29
|
referenceValues?: number[];
|
|
30
|
+
/**
|
|
31
|
+
* Live, per-frame Y values to fold into the axis-range fit on top of the static
|
|
32
|
+
* {@link referenceValues} — used so a *dragging* reference line expands the range
|
|
33
|
+
* and the axis follows the finger smoothly (the committed values already sit in
|
|
34
|
+
* `referenceValues`). Read on the UI thread each frame; omit for non-draggable charts.
|
|
35
|
+
*/
|
|
36
|
+
liveReferenceValues?: SharedValue<number[]>;
|
|
30
37
|
nonNegative?: boolean;
|
|
31
38
|
maxValue?: number;
|
|
32
39
|
nowOverride?: number;
|
|
@@ -50,12 +57,14 @@ export interface ChartEngineLayout {
|
|
|
50
57
|
/** Animating window (lerps toward {@link timeWindow}); use for positioning. */
|
|
51
58
|
displayWindow: SharedValue<number>;
|
|
52
59
|
/**
|
|
53
|
-
* The target window
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
60
|
+
* The *effective* target window — the value `displayWindow` eases toward: the
|
|
61
|
+
* pinch-zoom override ({@link ChartEngineScroll.viewWindow}) when active, else
|
|
62
|
+
* the `timeWindow` prop. Tick *selection* (which X-axis labels exist) must read
|
|
63
|
+
* this, not `displayWindow`: the lerp is asymptotic and never reaches the
|
|
64
|
+
* target, so it settles just above or just below it depending on the prior
|
|
65
|
+
* window. Bucketing that off-by-epsilon value (e.g. via `niceTimeInterval`)
|
|
66
|
+
* would otherwise make the tick cadence depend on where you came from. See
|
|
67
|
+
* issue #126.
|
|
59
68
|
*/
|
|
60
69
|
timeWindow: SharedValue<number>;
|
|
61
70
|
canvasWidth: SharedValue<number>;
|
|
@@ -76,6 +85,42 @@ export interface ChartEngineExtrema {
|
|
|
76
85
|
extremaMaxTime: SharedValue<number>;
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Time-scroll state: lets a pan gesture freeze the window at an absolute time
|
|
90
|
+
* and resume following the live edge. Returned by both engines (intersected onto
|
|
91
|
+
* the state) but kept off {@link ChartEngineLayout} so overlay components — which
|
|
92
|
+
* only read the layout — don't have to carry it.
|
|
93
|
+
*/
|
|
94
|
+
export interface ChartEngineScroll {
|
|
95
|
+
/**
|
|
96
|
+
* Absolute right-edge time (unix seconds) to freeze at, or `null` to follow
|
|
97
|
+
* the live edge. Written by the pan-scroll gesture; read by the engine tick.
|
|
98
|
+
*/
|
|
99
|
+
viewEnd: SharedValue<number | null>;
|
|
100
|
+
/**
|
|
101
|
+
* The right-edge time the engine would use if following live — advances each
|
|
102
|
+
* frame even while {@link ChartEngineLayout.timestamp} is frozen by a pan.
|
|
103
|
+
*/
|
|
104
|
+
liveEdge: SharedValue<number>;
|
|
105
|
+
/**
|
|
106
|
+
* Absolute visible-window width (seconds) to freeze at, or `null` to follow
|
|
107
|
+
* the configured `timeWindow`. Written by the pinch-zoom gesture; the
|
|
108
|
+
* symmetric counterpart of {@link viewEnd} (width vs. right edge). Folded into
|
|
109
|
+
* {@link ChartEngineLayout.timeWindow}, so downstream selection/positioning
|
|
110
|
+
* picks up the zoom for free.
|
|
111
|
+
*/
|
|
112
|
+
viewWindow: SharedValue<number | null>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Single-series only: smoothed price at the visible window's right edge. */
|
|
116
|
+
export interface ChartEngineEdge {
|
|
117
|
+
/**
|
|
118
|
+
* Smoothed value at the window's right edge — the live value while following,
|
|
119
|
+
* the price at `viewEnd` while scrolled back. For a `followViewEdge` badge.
|
|
120
|
+
*/
|
|
121
|
+
edgeValue: SharedValue<number>;
|
|
122
|
+
}
|
|
123
|
+
|
|
79
124
|
export interface SingleEngineState extends ChartEngineLayout, ChartEngineExtrema {
|
|
80
125
|
data: SharedValue<LiveChartPoint[]>;
|
|
81
126
|
value: SharedValue<number>;
|
|
@@ -119,6 +164,14 @@ export interface EngineFrameRefs {
|
|
|
119
164
|
nowOverrideSV?: SharedValue<number | undefined>;
|
|
120
165
|
windowBufferSV?: SharedValue<number>;
|
|
121
166
|
pausedSV: SharedValue<boolean>;
|
|
167
|
+
/** Pan-scroll right-edge override (null = follow live). Optional for callers/tests. */
|
|
168
|
+
viewEndSV?: SharedValue<number | null>;
|
|
169
|
+
/** Pinch-zoom window-width override (null = follow timeWindow). Optional for callers/tests. */
|
|
170
|
+
viewWindowSV?: SharedValue<number | null>;
|
|
171
|
+
/** Receives the computed live edge each frame. Optional for callers/tests. */
|
|
172
|
+
liveEdgeSV?: SharedValue<number>;
|
|
173
|
+
/** Receives the smoothed right-edge value each frame. Optional for callers/tests. */
|
|
174
|
+
edgeValueSV?: SharedValue<number>;
|
|
122
175
|
modeSV: SharedValue<"line" | "candle">;
|
|
123
176
|
candles?: SharedValue<CandlePoint[]>;
|
|
124
177
|
liveCandle?: SharedValue<CandlePoint | null>;
|
|
@@ -144,6 +197,8 @@ export function applyLiveChartEngineFrame(
|
|
|
144
197
|
displayMax: sv.displayMax.value,
|
|
145
198
|
displayWindow: sv.displayWindow.value,
|
|
146
199
|
timestamp: sv.timestamp.value,
|
|
200
|
+
liveEdge: sv.liveEdgeSV?.value ?? 0,
|
|
201
|
+
edgeValue: sv.edgeValueSV?.value ?? 0,
|
|
147
202
|
extremaMinValue: sv.extremaMinValue.value,
|
|
148
203
|
extremaMaxValue: sv.extremaMaxValue.value,
|
|
149
204
|
extremaMinTime: sv.extremaMinTime.value,
|
|
@@ -167,6 +222,8 @@ export function applyLiveChartEngineFrame(
|
|
|
167
222
|
points: sv.data.value,
|
|
168
223
|
nowSeconds: Date.now() / 1000,
|
|
169
224
|
paused: sv.pausedSV.value,
|
|
225
|
+
viewEnd: sv.viewEndSV?.value,
|
|
226
|
+
viewWindow: sv.viewWindowSV?.value,
|
|
170
227
|
mode: sv.modeSV.value,
|
|
171
228
|
candles: sv.candles?.value,
|
|
172
229
|
liveCandle: sv.liveCandle?.value,
|
|
@@ -176,15 +233,34 @@ export function applyLiveChartEngineFrame(
|
|
|
176
233
|
sv.displayMax.value = state.displayMax;
|
|
177
234
|
sv.displayWindow.value = state.displayWindow;
|
|
178
235
|
sv.timestamp.value = state.timestamp;
|
|
236
|
+
if (sv.liveEdgeSV) sv.liveEdgeSV.value = state.liveEdge;
|
|
237
|
+
if (sv.edgeValueSV) sv.edgeValueSV.value = state.edgeValue;
|
|
179
238
|
sv.extremaMinValue.value = state.extremaMinValue;
|
|
180
239
|
sv.extremaMaxValue.value = state.extremaMaxValue;
|
|
181
240
|
sv.extremaMinTime.value = state.extremaMinTime;
|
|
182
241
|
sv.extremaMaxTime.value = state.extremaMaxTime;
|
|
183
242
|
}
|
|
184
243
|
|
|
185
|
-
export function useLiveChartEngine(
|
|
186
|
-
|
|
187
|
-
|
|
244
|
+
export function useLiveChartEngine(
|
|
245
|
+
config: EngineConfig,
|
|
246
|
+
): SingleEngineState & ChartEngineScroll & ChartEngineEdge {
|
|
247
|
+
// Pinch-zoom window-width override (null = follow the configured window).
|
|
248
|
+
// Declared first so `timeWindow` below can fold it in. Defaults to null so
|
|
249
|
+
// charts without `zoom` behave exactly as before.
|
|
250
|
+
const viewWindow = useSharedValue<number | null>(null);
|
|
251
|
+
// A change to the `timeWindow` prop (a range / timeframe selector) is an explicit
|
|
252
|
+
// request to set the window, so it clears any active pinch-zoom override —
|
|
253
|
+
// otherwise the override below (`viewWindow ?? config.timeWindow`) would shadow
|
|
254
|
+
// the new prop forever, and prop-driven window changes would silently no-op.
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
viewWindow.set(null);
|
|
257
|
+
}, [config.timeWindow, viewWindow]);
|
|
258
|
+
|
|
259
|
+
// Low-frequency config → UI thread via useDerivedValue. `timeWindow` is the
|
|
260
|
+
// *effective* target window: the zoom override when set, else the prop. Both
|
|
261
|
+
// the tick's window lerp and the X-axis tick selection read it, so zoom flows
|
|
262
|
+
// downstream for free (mirrors how viewEnd drives `timestamp`).
|
|
263
|
+
const timeWindow = useDerivedValue(() => viewWindow.value ?? config.timeWindow);
|
|
188
264
|
// Static charts snap to their target in one tick (smoothing=1), so the single
|
|
189
265
|
// settle reaction below produces the final state with no per-frame easing.
|
|
190
266
|
const smoothing = useDerivedValue(() =>
|
|
@@ -193,7 +269,15 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
193
269
|
const adaptiveSpeedBoostSV = useDerivedValue(() => config.adaptiveSpeedBoost);
|
|
194
270
|
const exaggerateSV = useDerivedValue(() => config.exaggerate ?? false);
|
|
195
271
|
const referenceValue = useDerivedValue(() => config.referenceValue);
|
|
196
|
-
|
|
272
|
+
// Captured directly (not via `config.`) so Reanimated tracks the SharedValue and
|
|
273
|
+
// the merge re-runs each frame while a line is dragged; stable when idle.
|
|
274
|
+
const liveReferenceValuesSV = config.liveReferenceValues;
|
|
275
|
+
const referenceValues = useDerivedValue(() => {
|
|
276
|
+
const base = config.referenceValues;
|
|
277
|
+
const live = liveReferenceValuesSV?.value;
|
|
278
|
+
if (!live || live.length === 0) return base;
|
|
279
|
+
return base && base.length > 0 ? base.concat(live) : live;
|
|
280
|
+
});
|
|
197
281
|
const nonNegativeSV = useDerivedValue(() => config.nonNegative ?? false);
|
|
198
282
|
const maxValueSV = useDerivedValue(() => config.maxValue);
|
|
199
283
|
const nowOverrideSV = useDerivedValue(() => config.nowOverride);
|
|
@@ -212,6 +296,14 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
212
296
|
const [initialTimestamp] = useState(() => Date.now() / 1000);
|
|
213
297
|
const timestamp = useSharedValue(initialTimestamp);
|
|
214
298
|
|
|
299
|
+
// Pan-scroll state. `viewEnd` null = follow live; a number freezes the right
|
|
300
|
+
// edge at that absolute time. `liveEdge` mirrors the would-be-live timestamp
|
|
301
|
+
// each frame so the gesture can clamp / detect catch-up. Both default to
|
|
302
|
+
// "following" so charts without `timeScroll` behave exactly as before.
|
|
303
|
+
const viewEnd = useSharedValue<number | null>(null);
|
|
304
|
+
const liveEdge = useSharedValue(initialTimestamp);
|
|
305
|
+
const edgeValue = useSharedValue(0);
|
|
306
|
+
|
|
215
307
|
// Live data extrema (value + time of the visible high / low). NaN until the
|
|
216
308
|
// first tick finds data — the extrema label stays hidden until then.
|
|
217
309
|
const extremaMinValue = useSharedValue(NaN);
|
|
@@ -250,6 +342,10 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
250
342
|
nowOverrideSV,
|
|
251
343
|
windowBufferSV,
|
|
252
344
|
pausedSV,
|
|
345
|
+
viewEndSV: viewEnd,
|
|
346
|
+
viewWindowSV: viewWindow,
|
|
347
|
+
liveEdgeSV: liveEdge,
|
|
348
|
+
edgeValueSV: edgeValue,
|
|
253
349
|
modeSV,
|
|
254
350
|
candles,
|
|
255
351
|
liveCandle,
|
|
@@ -307,6 +403,10 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
307
403
|
canvasWidth,
|
|
308
404
|
canvasHeight,
|
|
309
405
|
timestamp,
|
|
406
|
+
viewEnd,
|
|
407
|
+
viewWindow,
|
|
408
|
+
liveEdge,
|
|
409
|
+
edgeValue,
|
|
310
410
|
extremaMinValue,
|
|
311
411
|
extremaMaxValue,
|
|
312
412
|
extremaMinTime,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { MS_PER_FRAME_60FPS } from "../constants";
|
|
9
9
|
import type { LiveChartPoint, SeriesConfig } from "../types";
|
|
10
10
|
import { tickLiveChartSeriesEngineFrame } from "./liveChartSeriesEngineTick";
|
|
11
|
-
import type { MultiEngineState } from "./useLiveChartEngine";
|
|
11
|
+
import type { ChartEngineScroll, MultiEngineState } from "./useLiveChartEngine";
|
|
12
12
|
|
|
13
13
|
export interface MultiSeriesEngineConfig {
|
|
14
14
|
series: SharedValue<SeriesConfig[]>;
|
|
@@ -47,6 +47,12 @@ export interface MultiEngineFrameRefs {
|
|
|
47
47
|
nowOverrideSV?: SharedValue<number | undefined>;
|
|
48
48
|
windowBufferSV?: SharedValue<number>;
|
|
49
49
|
pausedSV: SharedValue<boolean>;
|
|
50
|
+
/** Pan-scroll right-edge override (null = follow live). Optional for callers/tests. */
|
|
51
|
+
viewEndSV?: SharedValue<number | null>;
|
|
52
|
+
/** Pinch-zoom window-width override (null = follow timeWindow). Optional for callers/tests. */
|
|
53
|
+
viewWindowSV?: SharedValue<number | null>;
|
|
54
|
+
/** Receives the computed live edge each frame. Optional for callers/tests. */
|
|
55
|
+
liveEdgeSV?: SharedValue<number>;
|
|
50
56
|
extremaMinValue: SharedValue<number>;
|
|
51
57
|
extremaMaxValue: SharedValue<number>;
|
|
52
58
|
extremaMinTime: SharedValue<number>;
|
|
@@ -105,6 +111,7 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
105
111
|
displayMax: sv.displayMax.value,
|
|
106
112
|
displayWindow: sv.displayWindow.value,
|
|
107
113
|
timestamp: sv.timestamp.value,
|
|
114
|
+
liveEdge: sv.liveEdgeSV?.value ?? 0,
|
|
108
115
|
displayValues,
|
|
109
116
|
opacities,
|
|
110
117
|
extremaMinValue: sv.extremaMinValue.value,
|
|
@@ -129,11 +136,14 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
129
136
|
series: seriesSnap,
|
|
130
137
|
nowSeconds: Date.now() / 1000,
|
|
131
138
|
paused: sv.pausedSV.value,
|
|
139
|
+
viewEnd: sv.viewEndSV?.value,
|
|
140
|
+
viewWindow: sv.viewWindowSV?.value,
|
|
132
141
|
});
|
|
133
142
|
sv.displayMin.value = state.displayMin;
|
|
134
143
|
sv.displayMax.value = state.displayMax;
|
|
135
144
|
sv.displayWindow.value = state.displayWindow;
|
|
136
145
|
sv.timestamp.value = state.timestamp;
|
|
146
|
+
if (sv.liveEdgeSV) sv.liveEdgeSV.value = state.liveEdge;
|
|
137
147
|
sv.displaySeriesValues.value = state.displayValues;
|
|
138
148
|
sv.seriesOpacities.value = state.opacities;
|
|
139
149
|
sv.extremaMinValue.value = state.extremaMinValue;
|
|
@@ -148,8 +158,11 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
148
158
|
*/
|
|
149
159
|
export function useLiveChartSeriesEngine(
|
|
150
160
|
config: MultiSeriesEngineConfig,
|
|
151
|
-
): MultiEngineState {
|
|
152
|
-
|
|
161
|
+
): MultiEngineState & ChartEngineScroll {
|
|
162
|
+
// Pinch-zoom window-width override (null = follow the configured window).
|
|
163
|
+
// Declared first so `timeWindow` folds it in — see useLiveChartEngine.
|
|
164
|
+
const viewWindow = useSharedValue<number | null>(null);
|
|
165
|
+
const timeWindow = useDerivedValue(() => viewWindow.value ?? config.timeWindow);
|
|
153
166
|
const smoothing = useDerivedValue(() => config.smoothing);
|
|
154
167
|
const adaptiveSpeedBoostSV = useDerivedValue(() => config.adaptiveSpeedBoost);
|
|
155
168
|
const exaggerateSV = useDerivedValue(() => config.exaggerate ?? false);
|
|
@@ -170,6 +183,10 @@ export function useLiveChartSeriesEngine(
|
|
|
170
183
|
const [initialTimestamp] = useState(() => Date.now() / 1000);
|
|
171
184
|
const timestamp = useSharedValue(initialTimestamp);
|
|
172
185
|
|
|
186
|
+
// Pan-scroll state (see useLiveChartEngine). Defaults to following live.
|
|
187
|
+
const viewEnd = useSharedValue<number | null>(null);
|
|
188
|
+
const liveEdge = useSharedValue(initialTimestamp);
|
|
189
|
+
|
|
173
190
|
const displaySeriesValues = useSharedValue<number[]>([]);
|
|
174
191
|
const seriesOpacities = useSharedValue<number[]>([]);
|
|
175
192
|
|
|
@@ -217,6 +234,9 @@ export function useLiveChartSeriesEngine(
|
|
|
217
234
|
nowOverrideSV,
|
|
218
235
|
windowBufferSV,
|
|
219
236
|
pausedSV,
|
|
237
|
+
viewEndSV: viewEnd,
|
|
238
|
+
viewWindowSV: viewWindow,
|
|
239
|
+
liveEdgeSV: liveEdge,
|
|
220
240
|
extremaMinValue,
|
|
221
241
|
extremaMaxValue,
|
|
222
242
|
extremaMinTime,
|
|
@@ -237,6 +257,9 @@ export function useLiveChartSeriesEngine(
|
|
|
237
257
|
canvasWidth,
|
|
238
258
|
canvasHeight,
|
|
239
259
|
timestamp,
|
|
260
|
+
viewEnd,
|
|
261
|
+
viewWindow,
|
|
262
|
+
liveEdge,
|
|
240
263
|
series,
|
|
241
264
|
displaySeriesValues,
|
|
242
265
|
seriesOpacities,
|
package/src/draw/grid.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GRID_METRICS_DEFAULTS } from "../constants";
|
|
1
|
+
import { GRID_METRICS_DEFAULTS, MAX_Y_LABELS } from "../constants";
|
|
2
2
|
import { lerp } from "../math/lerp";
|
|
3
3
|
import type { GridMetrics } from "../types";
|
|
4
4
|
|
|
@@ -8,6 +8,43 @@ export interface YAxisEntry {
|
|
|
8
8
|
alpha: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Fixed-count Y-axis: place exactly `count` price labels evenly **in pixels**
|
|
13
|
+
* across the plot band — top = `displayMax`, bottom = `displayMin`. Values track
|
|
14
|
+
* the live range each frame (no nice-number rounding), so the label count stays
|
|
15
|
+
* constant while data streams in.
|
|
16
|
+
*
|
|
17
|
+
* `minGap` is a floor: the count drops to what fits when the plot is too short
|
|
18
|
+
* to space `count` labels at least `minGap` px apart. Clamped to
|
|
19
|
+
* `[2, MAX_Y_LABELS]`. Every entry is fully opaque — there's no per-line fade
|
|
20
|
+
* because the count (and thus each line's pixel slot) doesn't change.
|
|
21
|
+
*/
|
|
22
|
+
export function fixedGridEntries(
|
|
23
|
+
displayMax: number,
|
|
24
|
+
valRange: number,
|
|
25
|
+
chartH: number,
|
|
26
|
+
padTop: number,
|
|
27
|
+
count: number,
|
|
28
|
+
minGap: number,
|
|
29
|
+
formatValue: (v: number) => string,
|
|
30
|
+
): YAxisEntry[] {
|
|
31
|
+
"worklet";
|
|
32
|
+
// minGap floor: at most `floor(chartH / minGap)` gaps fit ⇒ that many + 1 labels.
|
|
33
|
+
const maxFit = Math.floor(chartH / minGap) + 1;
|
|
34
|
+
let n = Math.min(count, maxFit, MAX_Y_LABELS);
|
|
35
|
+
if (n < 2) n = 2;
|
|
36
|
+
|
|
37
|
+
const stepPx = chartH / (n - 1);
|
|
38
|
+
const stepVal = valRange / (n - 1);
|
|
39
|
+
const entries: YAxisEntry[] = [];
|
|
40
|
+
for (let i = 0; i < n; i++) {
|
|
41
|
+
const y = padTop + i * stepPx;
|
|
42
|
+
const val = displayMax - i * stepVal;
|
|
43
|
+
entries.push({ y, label: formatValue(val), alpha: 1 });
|
|
44
|
+
}
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
|
|
11
48
|
/**
|
|
12
49
|
* Pick a nice Y-axis interval using TradingView's cycling divisor approach.
|
|
13
50
|
* Hysteresis: once chosen, sticks until pixel spacing falls outside [0.5x, 4x] of minGap.
|
|
@@ -94,6 +131,7 @@ export function computeGridEntries(
|
|
|
94
131
|
dt: number,
|
|
95
132
|
minGap = 36,
|
|
96
133
|
grid: GridMetrics = GRID_METRICS_DEFAULTS,
|
|
134
|
+
count = 0,
|
|
97
135
|
): { entries: YAxisEntry[]; interval: number } {
|
|
98
136
|
"worklet";
|
|
99
137
|
const chartH = canvasHeight - padTop - padBottom;
|
|
@@ -102,6 +140,25 @@ export function computeGridEntries(
|
|
|
102
140
|
const valRange = displayMax - displayMin;
|
|
103
141
|
if (valRange <= 0) return { entries: [], interval: prevInterval };
|
|
104
142
|
|
|
143
|
+
// Fixed-count mode bypasses nice-interval picking and per-line fading. Leave
|
|
144
|
+
// `prevInterval`/`labelAlphas` untouched so toggling back to the dynamic grid
|
|
145
|
+
// resumes cleanly. Needs at least 2 labels (a high/low pair) to be meaningful;
|
|
146
|
+
// count of 0 or 1 falls through to the dynamic nice-interval grid.
|
|
147
|
+
if (count >= 2) {
|
|
148
|
+
return {
|
|
149
|
+
entries: fixedGridEntries(
|
|
150
|
+
displayMax,
|
|
151
|
+
valRange,
|
|
152
|
+
chartH,
|
|
153
|
+
padTop,
|
|
154
|
+
count,
|
|
155
|
+
minGap,
|
|
156
|
+
formatValue,
|
|
157
|
+
),
|
|
158
|
+
interval: prevInterval,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
105
162
|
const pxPerUnit = chartH / valRange;
|
|
106
163
|
|
|
107
164
|
const coarse = pickInterval(valRange, pxPerUnit, minGap, prevInterval);
|
|
@@ -114,7 +171,19 @@ export function computeGridEntries(
|
|
|
114
171
|
// Phase 1: compute target alpha for every grid line
|
|
115
172
|
const targets: Record<number, number> = {};
|
|
116
173
|
const first = Math.ceil(displayMin / fine) * fine;
|
|
117
|
-
|
|
174
|
+
// Guard against a non-advancing loop. On a near-flat (but not bit-identical)
|
|
175
|
+
// range, `fine` can drop below ulp(val), so `val += fine === val` and the loop
|
|
176
|
+
// never terminates — freezing the UI thread, since this runs in a worklet every
|
|
177
|
+
// frame. `stepResolves` skips the degenerate range (also catches fine === 0);
|
|
178
|
+
// MAX_GRID_LINES is a hard backstop in case the step stalls mid-loop (real
|
|
179
|
+
// grids only have a handful of lines, so it never bites normal data).
|
|
180
|
+
const stepResolves = first + fine !== first;
|
|
181
|
+
const MAX_GRID_LINES = 1000;
|
|
182
|
+
for (
|
|
183
|
+
let val = first, lineCount = 0;
|
|
184
|
+
stepResolves && val <= displayMax && lineCount < MAX_GRID_LINES;
|
|
185
|
+
val += fine, lineCount++
|
|
186
|
+
) {
|
|
118
187
|
const y = gridValueToY(val, displayMax, valRange, padTop, chartH);
|
|
119
188
|
/* istanbul ignore next -- y stays in chart band for v in [first, displayMax] with consistent toY */
|
|
120
189
|
if (y < padTop - 2 || y > canvasHeight - padBottom + 2) continue;
|