react-native-livechart 1.0.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/LICENSE +33 -0
- package/README.md +198 -0
- package/dist/components/AnimatedLabel.d.ts +21 -0
- package/dist/components/AnimatedLabel.d.ts.map +1 -0
- package/dist/components/BadgeOverlay.d.ts +16 -0
- package/dist/components/BadgeOverlay.d.ts.map +1 -0
- package/dist/components/CrosshairLine.d.ts +20 -0
- package/dist/components/CrosshairLine.d.ts.map +1 -0
- package/dist/components/CrosshairOverlay.d.ts +29 -0
- package/dist/components/CrosshairOverlay.d.ts.map +1 -0
- package/dist/components/DegenParticlesOverlay.d.ts +16 -0
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -0
- package/dist/components/DotOverlay.d.ts +17 -0
- package/dist/components/DotOverlay.d.ts.map +1 -0
- package/dist/components/LeftEdgeFade.d.ts +9 -0
- package/dist/components/LeftEdgeFade.d.ts.map +1 -0
- package/dist/components/LiveChart.d.ts +3 -0
- package/dist/components/LiveChart.d.ts.map +1 -0
- package/dist/components/LiveChartSeries.d.ts +3 -0
- package/dist/components/LiveChartSeries.d.ts.map +1 -0
- package/dist/components/LiveChartTransition.d.ts +28 -0
- package/dist/components/LiveChartTransition.d.ts.map +1 -0
- package/dist/components/LoadingOverlay.d.ts +24 -0
- package/dist/components/LoadingOverlay.d.ts.map +1 -0
- package/dist/components/MarkerOverlay.d.ts +25 -0
- package/dist/components/MarkerOverlay.d.ts.map +1 -0
- package/dist/components/MultiSeriesDots.d.ts +11 -0
- package/dist/components/MultiSeriesDots.d.ts.map +1 -0
- package/dist/components/MultiSeriesStroke.d.ts +14 -0
- package/dist/components/MultiSeriesStroke.d.ts.map +1 -0
- package/dist/components/MultiSeriesTooltipStack.d.ts +10 -0
- package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -0
- package/dist/components/MultiSeriesValueLabels.d.ts +11 -0
- package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -0
- package/dist/components/MultiSeriesValueLines.d.ts +10 -0
- package/dist/components/MultiSeriesValueLines.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +19 -0
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -0
- package/dist/components/SeriesToggleChips.d.ts +15 -0
- package/dist/components/SeriesToggleChips.d.ts.map +1 -0
- package/dist/components/TradeStreamOverlay.d.ts +14 -0
- package/dist/components/TradeStreamOverlay.d.ts.map +1 -0
- package/dist/components/ValueLineOverlay.d.ts +16 -0
- package/dist/components/ValueLineOverlay.d.ts.map +1 -0
- package/dist/components/ValueTextOverlay.d.ts +20 -0
- package/dist/components/ValueTextOverlay.d.ts.map +1 -0
- package/dist/components/XAxisOverlay.d.ts +14 -0
- package/dist/components/XAxisOverlay.d.ts.map +1 -0
- package/dist/components/YAxisOverlay.d.ts +23 -0
- package/dist/components/YAxisOverlay.d.ts.map +1 -0
- package/dist/components/seriesMetaSig.d.ts +4 -0
- package/dist/components/seriesMetaSig.d.ts.map +1 -0
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +45 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts +38 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -0
- package/dist/core/multiSeriesLayout.d.ts +22 -0
- package/dist/core/multiSeriesLayout.d.ts.map +1 -0
- package/dist/core/resolveConfig.d.ts +188 -0
- package/dist/core/resolveConfig.d.ts.map +1 -0
- package/dist/core/useLiveChartEngine.d.ts +79 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts +68 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -0
- package/dist/draw/candle.d.ts +25 -0
- package/dist/draw/candle.d.ts.map +1 -0
- package/dist/draw/grid.d.ts +24 -0
- package/dist/draw/grid.d.ts.map +1 -0
- package/dist/draw/line.d.ts +86 -0
- package/dist/draw/line.d.ts.map +1 -0
- package/dist/draw/trade.d.ts +38 -0
- package/dist/draw/trade.d.ts.map +1 -0
- package/dist/hooks/crosshairSeries.d.ts +13 -0
- package/dist/hooks/crosshairSeries.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +72 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +24 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +42 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -0
- package/dist/hooks/useBadge.d.ts +14 -0
- package/dist/hooks/useBadge.d.ts.map +1 -0
- package/dist/hooks/useCandlePaths.d.ts +19 -0
- package/dist/hooks/useCandlePaths.d.ts.map +1 -0
- package/dist/hooks/useCanvasLayout.d.ts +12 -0
- package/dist/hooks/useCanvasLayout.d.ts.map +1 -0
- package/dist/hooks/useChartColors.d.ts +20 -0
- package/dist/hooks/useChartColors.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +19 -0
- package/dist/hooks/useChartPaths.d.ts.map +1 -0
- package/dist/hooks/useChartReveal.d.ts +30 -0
- package/dist/hooks/useChartReveal.d.ts.map +1 -0
- package/dist/hooks/useChartSkiaFont.d.ts +8 -0
- package/dist/hooks/useChartSkiaFont.d.ts.map +1 -0
- package/dist/hooks/useCrosshair.d.ts +20 -0
- package/dist/hooks/useCrosshair.d.ts.map +1 -0
- package/dist/hooks/useCrosshairSeries.d.ts +10 -0
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -0
- package/dist/hooks/useDegen.d.ts +17 -0
- package/dist/hooks/useDegen.d.ts.map +1 -0
- package/dist/hooks/useLiveChartHasData.d.ts +17 -0
- package/dist/hooks/useLiveChartHasData.d.ts.map +1 -0
- package/dist/hooks/useLiveDot.d.ts +12 -0
- package/dist/hooks/useLiveDot.d.ts.map +1 -0
- package/dist/hooks/useMarkers.d.ts +15 -0
- package/dist/hooks/useMarkers.d.ts.map +1 -0
- package/dist/hooks/useModeBlend.d.ts +18 -0
- package/dist/hooks/useModeBlend.d.ts.map +1 -0
- package/dist/hooks/useMomentum.d.ts +16 -0
- package/dist/hooks/useMomentum.d.ts.map +1 -0
- package/dist/hooks/useMultiSeriesDegen.d.ts +25 -0
- package/dist/hooks/useMultiSeriesDegen.d.ts.map +1 -0
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +15 -0
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +32 -0
- package/dist/hooks/useReferenceLine.d.ts.map +1 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts +24 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -0
- package/dist/hooks/useTradeStream.d.ts +12 -0
- package/dist/hooks/useTradeStream.d.ts.map +1 -0
- package/dist/hooks/useXAxis.d.ts +32 -0
- package/dist/hooks/useXAxis.d.ts.map +1 -0
- package/dist/hooks/useYAxis.d.ts +13 -0
- package/dist/hooks/useYAxis.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/lib/format.d.ts +5 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/measureFontTextWidth.d.ts +7 -0
- package/dist/lib/measureFontTextWidth.d.ts.map +1 -0
- package/dist/lib/monoFontFamily.d.ts +6 -0
- package/dist/lib/monoFontFamily.d.ts.map +1 -0
- package/dist/math/color.d.ts +5 -0
- package/dist/math/color.d.ts.map +1 -0
- package/dist/math/degenTick.d.ts +57 -0
- package/dist/math/degenTick.d.ts.map +1 -0
- package/dist/math/interpolate.d.ts +7 -0
- package/dist/math/interpolate.d.ts.map +1 -0
- package/dist/math/intervals.d.ts +3 -0
- package/dist/math/intervals.d.ts.map +1 -0
- package/dist/math/lerp.d.ts +7 -0
- package/dist/math/lerp.d.ts.map +1 -0
- package/dist/math/markers.d.ts +44 -0
- package/dist/math/markers.d.ts.map +1 -0
- package/dist/math/momentum.d.ts +8 -0
- package/dist/math/momentum.d.ts.map +1 -0
- package/dist/math/pickCandle.d.ts +8 -0
- package/dist/math/pickCandle.d.ts.map +1 -0
- package/dist/math/range.d.ts +10 -0
- package/dist/math/range.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +20 -0
- package/dist/math/referenceLines.d.ts.map +1 -0
- package/dist/math/spline.d.ts +45 -0
- package/dist/math/spline.d.ts.map +1 -0
- package/dist/math/squiggly.d.ts +36 -0
- package/dist/math/squiggly.d.ts.map +1 -0
- package/dist/theme.d.ts +30 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/types.d.ts +720 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +72 -0
- package/src/components/AnimatedLabel.tsx +41 -0
- package/src/components/BadgeOverlay.tsx +39 -0
- package/src/components/CrosshairLine.tsx +90 -0
- package/src/components/CrosshairOverlay.tsx +170 -0
- package/src/components/DegenParticlesOverlay.tsx +141 -0
- package/src/components/DotOverlay.tsx +69 -0
- package/src/components/LeftEdgeFade.tsx +36 -0
- package/src/components/LiveChart.tsx +821 -0
- package/src/components/LiveChartSeries.tsx +607 -0
- package/src/components/LiveChartTransition.tsx +135 -0
- package/src/components/LoadingOverlay.tsx +301 -0
- package/src/components/MarkerOverlay.tsx +331 -0
- package/src/components/MultiSeriesDots.tsx +117 -0
- package/src/components/MultiSeriesStroke.tsx +79 -0
- package/src/components/MultiSeriesTooltipStack.tsx +78 -0
- package/src/components/MultiSeriesValueLabels.tsx +103 -0
- package/src/components/MultiSeriesValueLines.tsx +115 -0
- package/src/components/ReferenceLineOverlay.tsx +317 -0
- package/src/components/SeriesToggleChips.tsx +201 -0
- package/src/components/TradeStreamOverlay.tsx +117 -0
- package/src/components/ValueLineOverlay.tsx +63 -0
- package/src/components/ValueTextOverlay.tsx +43 -0
- package/src/components/XAxisOverlay.tsx +109 -0
- package/src/components/YAxisOverlay.tsx +135 -0
- package/src/components/seriesMetaSig.ts +13 -0
- package/src/constants.ts +44 -0
- package/src/core/liveChartEngineTick.ts +185 -0
- package/src/core/liveChartSeriesEngineTick.ts +163 -0
- package/src/core/multiSeriesLayout.ts +80 -0
- package/src/core/resolveConfig.ts +538 -0
- package/src/core/useLiveChartEngine.ts +207 -0
- package/src/core/useLiveChartSeriesEngine.ts +215 -0
- package/src/draw/candle.ts +210 -0
- package/src/draw/grid.ts +174 -0
- package/src/draw/line.ts +257 -0
- package/src/draw/trade.ts +202 -0
- package/src/hooks/crosshairSeries.ts +78 -0
- package/src/hooks/crosshairShared.ts +296 -0
- package/src/hooks/index.ts +26 -0
- package/src/hooks/resolveChartLayout.ts +144 -0
- package/src/hooks/useBadge.ts +192 -0
- package/src/hooks/useCandlePaths.ts +165 -0
- package/src/hooks/useCanvasLayout.ts +22 -0
- package/src/hooks/useChartColors.ts +48 -0
- package/src/hooks/useChartPaths.ts +121 -0
- package/src/hooks/useChartReveal.ts +123 -0
- package/src/hooks/useChartSkiaFont.ts +34 -0
- package/src/hooks/useCrosshair.ts +250 -0
- package/src/hooks/useCrosshairSeries.ts +163 -0
- package/src/hooks/useDegen.ts +262 -0
- package/src/hooks/useLiveChartHasData.ts +31 -0
- package/src/hooks/useLiveDot.ts +31 -0
- package/src/hooks/useMarkers.ts +95 -0
- package/src/hooks/useModeBlend.ts +52 -0
- package/src/hooks/useMomentum.ts +37 -0
- package/src/hooks/useMultiSeriesDegen.ts +260 -0
- package/src/hooks/useMultiSeriesLinePaths.ts +76 -0
- package/src/hooks/useReferenceLine.ts +212 -0
- package/src/hooks/useReverseMorphEngineInputs.ts +154 -0
- package/src/hooks/useTradeStream.ts +64 -0
- package/src/hooks/useXAxis.ts +200 -0
- package/src/hooks/useYAxis.ts +48 -0
- package/src/index.ts +69 -0
- package/src/lib/format.ts +36 -0
- package/src/lib/measureFontTextWidth.ts +10 -0
- package/src/lib/monoFontFamily.ts +8 -0
- package/src/math/color.ts +31 -0
- package/src/math/degenTick.ts +151 -0
- package/src/math/interpolate.ts +32 -0
- package/src/math/intervals.ts +17 -0
- package/src/math/lerp.ts +17 -0
- package/src/math/markers.ts +175 -0
- package/src/math/momentum.ts +38 -0
- package/src/math/pickCandle.ts +42 -0
- package/src/math/range.ts +54 -0
- package/src/math/referenceLines.ts +56 -0
- package/src/math/spline.ts +183 -0
- package/src/math/squiggly.ts +108 -0
- package/src/theme.ts +231 -0
- package/src/types.ts +789 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
2
|
+
|
|
3
|
+
import type { SkFont } from "@shopify/react-native-skia";
|
|
4
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
5
|
+
import type { ChartPadding } from "../draw/line";
|
|
6
|
+
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
7
|
+
import {
|
|
8
|
+
classifyReferenceEdge,
|
|
9
|
+
referenceLineForm,
|
|
10
|
+
} from "../math/referenceLines";
|
|
11
|
+
import type { ReferenceLine } from "../types";
|
|
12
|
+
|
|
13
|
+
/** Screen-space geometry for one reference line / band, recomputed each frame. */
|
|
14
|
+
export interface ReferenceLineLayout {
|
|
15
|
+
visible: boolean;
|
|
16
|
+
/** Line y, or band top-edge y. */
|
|
17
|
+
y: number;
|
|
18
|
+
/** Band bottom-edge y (value/time band); equals `y` for a plain line. */
|
|
19
|
+
yBottom: number;
|
|
20
|
+
/** Left x extent. */
|
|
21
|
+
x1: number;
|
|
22
|
+
/** Right x extent. */
|
|
23
|
+
x2: number;
|
|
24
|
+
/** Resolved label text (with appended value when `showValue`). */
|
|
25
|
+
label: string;
|
|
26
|
+
labelX: number;
|
|
27
|
+
labelY: number;
|
|
28
|
+
/** True when a Form-A line is off-screen and its off-axis badge is showing. */
|
|
29
|
+
offAxis: boolean;
|
|
30
|
+
/** Off-axis chevron points up (target above range) vs down (below). */
|
|
31
|
+
chevronUp: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const INVISIBLE: ReferenceLineLayout = {
|
|
35
|
+
visible: false,
|
|
36
|
+
y: -1,
|
|
37
|
+
yBottom: -1,
|
|
38
|
+
x1: 0,
|
|
39
|
+
x2: 0,
|
|
40
|
+
label: "",
|
|
41
|
+
labelX: 0,
|
|
42
|
+
labelY: -1,
|
|
43
|
+
offAxis: false,
|
|
44
|
+
chevronUp: false,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Off-axis indicator inset from the nearest plot edge, in px. */
|
|
48
|
+
const OFF_AXIS_EDGE_INSET = 12;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Derives screen-space layout for a single reference line or band. Supports all
|
|
52
|
+
* three `ReferenceLine` forms (horizontal line, horizontal value band, vertical
|
|
53
|
+
* time band) plus the off-axis badge for an off-screen Form-A value.
|
|
54
|
+
*/
|
|
55
|
+
export function useReferenceLine(
|
|
56
|
+
engine: ChartEngineLayout,
|
|
57
|
+
padding: ChartPadding,
|
|
58
|
+
line: ReferenceLine | undefined,
|
|
59
|
+
formatValue: (v: number) => string,
|
|
60
|
+
font: SkFont,
|
|
61
|
+
): SharedValue<ReferenceLineLayout> {
|
|
62
|
+
const form = line ? referenceLineForm(line) : "none";
|
|
63
|
+
|
|
64
|
+
return useDerivedValue<ReferenceLineLayout>(() => {
|
|
65
|
+
if (!line || form === "none") return INVISIBLE;
|
|
66
|
+
|
|
67
|
+
const w = engine.canvasWidth.value;
|
|
68
|
+
const h = engine.canvasHeight.value;
|
|
69
|
+
if (w === 0 || h === 0) return INVISIBLE;
|
|
70
|
+
|
|
71
|
+
const chartTop = padding.top;
|
|
72
|
+
const chartBottom = h - padding.bottom;
|
|
73
|
+
const chartH = chartBottom - chartTop;
|
|
74
|
+
const x1 = padding.left;
|
|
75
|
+
const x2 = w - padding.right;
|
|
76
|
+
|
|
77
|
+
const fm = font.getMetrics();
|
|
78
|
+
const baselineOffset = (fm.ascent + fm.descent) / 2;
|
|
79
|
+
|
|
80
|
+
// ── Form C — vertical time band (independent of the value range) ─────────
|
|
81
|
+
if (form === "time-band") {
|
|
82
|
+
if (line.from === undefined || line.to === undefined) return INVISIBLE;
|
|
83
|
+
const now = engine.timestamp.value;
|
|
84
|
+
const win = engine.displayWindow.value;
|
|
85
|
+
const winStart = now - win;
|
|
86
|
+
const chartW = x2 - x1;
|
|
87
|
+
if (win <= 0 || chartW <= 0) return INVISIBLE;
|
|
88
|
+
|
|
89
|
+
let bx1 = x1 + ((line.from - winStart) / win) * chartW;
|
|
90
|
+
let bx2 = x1 + ((line.to - winStart) / win) * chartW;
|
|
91
|
+
if (bx2 < bx1) {
|
|
92
|
+
const t = bx1;
|
|
93
|
+
bx1 = bx2;
|
|
94
|
+
bx2 = t;
|
|
95
|
+
}
|
|
96
|
+
if (bx2 < x1 || bx1 > x2) return INVISIBLE;
|
|
97
|
+
if (bx1 < x1) bx1 = x1;
|
|
98
|
+
if (bx2 > x2) bx2 = x2;
|
|
99
|
+
|
|
100
|
+
const label = line.label ?? "";
|
|
101
|
+
const labelX =
|
|
102
|
+
line.labelPosition === "right"
|
|
103
|
+
? bx2 - 4 - measureFontTextWidth(font, label)
|
|
104
|
+
: bx1 + 4;
|
|
105
|
+
return {
|
|
106
|
+
visible: true,
|
|
107
|
+
y: chartTop,
|
|
108
|
+
yBottom: chartBottom,
|
|
109
|
+
x1: bx1,
|
|
110
|
+
x2: bx2,
|
|
111
|
+
label,
|
|
112
|
+
labelX,
|
|
113
|
+
labelY: chartTop - fm.ascent + 2,
|
|
114
|
+
offAxis: false,
|
|
115
|
+
chevronUp: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Forms A / B project values onto y, so they need a non-degenerate range.
|
|
120
|
+
const dMin = engine.displayMin.value;
|
|
121
|
+
const dMax = engine.displayMax.value;
|
|
122
|
+
const valRange = dMax - dMin;
|
|
123
|
+
if (valRange <= 0) return INVISIBLE;
|
|
124
|
+
const toY = (v: number) => chartTop + ((dMax - v) / valRange) * chartH;
|
|
125
|
+
|
|
126
|
+
// ── Form B — horizontal value band ───────────────────────────────────────
|
|
127
|
+
if (form === "value-band") {
|
|
128
|
+
if (line.valueFrom === undefined || line.valueTo === undefined) {
|
|
129
|
+
return INVISIBLE;
|
|
130
|
+
}
|
|
131
|
+
let yTop = toY(line.valueTo);
|
|
132
|
+
let yBot = toY(line.valueFrom);
|
|
133
|
+
if (yBot < yTop) {
|
|
134
|
+
const t = yTop;
|
|
135
|
+
yTop = yBot;
|
|
136
|
+
yBot = t;
|
|
137
|
+
}
|
|
138
|
+
if (yBot < chartTop || yTop > chartBottom) return INVISIBLE;
|
|
139
|
+
if (yTop < chartTop) yTop = chartTop;
|
|
140
|
+
if (yBot > chartBottom) yBot = chartBottom;
|
|
141
|
+
|
|
142
|
+
const label = line.label ?? "";
|
|
143
|
+
const labelX =
|
|
144
|
+
line.labelPosition === "right"
|
|
145
|
+
? x2 - 4 - measureFontTextWidth(font, label)
|
|
146
|
+
: x1 + 4;
|
|
147
|
+
return {
|
|
148
|
+
visible: true,
|
|
149
|
+
y: yTop,
|
|
150
|
+
yBottom: yBot,
|
|
151
|
+
x1,
|
|
152
|
+
x2,
|
|
153
|
+
label,
|
|
154
|
+
labelX,
|
|
155
|
+
labelY: yTop - fm.ascent + 2,
|
|
156
|
+
offAxis: false,
|
|
157
|
+
chevronUp: false,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Form A — horizontal line (with optional off-axis badge) ──────────────
|
|
162
|
+
if (line.value === undefined) return INVISIBLE;
|
|
163
|
+
const v = line.value;
|
|
164
|
+
const edge = classifyReferenceEdge(v, dMin, dMax);
|
|
165
|
+
|
|
166
|
+
if (edge !== "in") {
|
|
167
|
+
if (!line.offAxisBadge) return INVISIBLE; // legacy: cull off-screen line
|
|
168
|
+
const above = edge === "above";
|
|
169
|
+
const clampedY = above
|
|
170
|
+
? chartTop + OFF_AXIS_EDGE_INSET
|
|
171
|
+
: chartBottom - OFF_AXIS_EDGE_INSET;
|
|
172
|
+
const word = line.offAxisBadgeLabel ?? line.label;
|
|
173
|
+
const text = word ? `${word}: ${formatValue(v)}` : formatValue(v);
|
|
174
|
+
return {
|
|
175
|
+
visible: true,
|
|
176
|
+
y: clampedY,
|
|
177
|
+
yBottom: clampedY,
|
|
178
|
+
x1,
|
|
179
|
+
x2,
|
|
180
|
+
label: text,
|
|
181
|
+
labelX: x1 + 16,
|
|
182
|
+
labelY: clampedY - baselineOffset,
|
|
183
|
+
offAxis: true,
|
|
184
|
+
chevronUp: above,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const y = toY(v);
|
|
189
|
+
let label = line.label ?? formatValue(v);
|
|
190
|
+
if (line.showValue && line.label) label = `${line.label} ${formatValue(v)}`;
|
|
191
|
+
|
|
192
|
+
const pos = line.labelPosition ?? "right";
|
|
193
|
+
let labelX: number;
|
|
194
|
+
if (pos === "left") labelX = x1 + 4;
|
|
195
|
+
else if (pos === "center")
|
|
196
|
+
labelX = (x1 + x2) / 2 - measureFontTextWidth(font, label) / 2;
|
|
197
|
+
else labelX = x2 + 4; // "right" — legacy gutter position
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
visible: true,
|
|
201
|
+
y,
|
|
202
|
+
yBottom: y,
|
|
203
|
+
x1,
|
|
204
|
+
x2,
|
|
205
|
+
label,
|
|
206
|
+
labelX,
|
|
207
|
+
labelY: y - baselineOffset,
|
|
208
|
+
offAxis: false,
|
|
209
|
+
chevronUp: false,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useAnimatedReaction,
|
|
3
|
+
useSharedValue,
|
|
4
|
+
type SharedValue,
|
|
5
|
+
} from "react-native-reanimated";
|
|
6
|
+
import type { CandlePoint, LiveChartPoint, SeriesConfig } from "../types";
|
|
7
|
+
|
|
8
|
+
const STASH_MORPH_EPS = 0.01;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* While `morphT` animates down after data clears, keep feeding the engine the
|
|
12
|
+
* last stashed line/candle snapshot.
|
|
13
|
+
*/
|
|
14
|
+
export function useSingleChartReverseMorphInputs({
|
|
15
|
+
isCandle,
|
|
16
|
+
data,
|
|
17
|
+
candles,
|
|
18
|
+
liveCandle,
|
|
19
|
+
hasData,
|
|
20
|
+
morphT,
|
|
21
|
+
}: {
|
|
22
|
+
isCandle: boolean;
|
|
23
|
+
data: SharedValue<LiveChartPoint[]>;
|
|
24
|
+
candles: SharedValue<CandlePoint[]> | undefined;
|
|
25
|
+
liveCandle: SharedValue<CandlePoint | null> | undefined;
|
|
26
|
+
hasData: SharedValue<boolean>;
|
|
27
|
+
morphT: SharedValue<number>;
|
|
28
|
+
}): {
|
|
29
|
+
lineEngineData: SharedValue<LiveChartPoint[]>;
|
|
30
|
+
candlesEngine: SharedValue<CandlePoint[]>;
|
|
31
|
+
liveEngine: SharedValue<CandlePoint | null>;
|
|
32
|
+
} {
|
|
33
|
+
const lineStash = useSharedValue<LiveChartPoint[]>([]);
|
|
34
|
+
const candleStash = useSharedValue<CandlePoint[]>([]);
|
|
35
|
+
const lineEngineData = useSharedValue<LiveChartPoint[]>([]);
|
|
36
|
+
const candlesEngine = useSharedValue<CandlePoint[]>([]);
|
|
37
|
+
const liveEngine = useSharedValue<CandlePoint | null>(null);
|
|
38
|
+
|
|
39
|
+
useAnimatedReaction(
|
|
40
|
+
() => ({
|
|
41
|
+
candle: isCandle,
|
|
42
|
+
has: hasData.get(),
|
|
43
|
+
m: morphT.get(),
|
|
44
|
+
d: data.get(),
|
|
45
|
+
cLen: candles?.get().length ?? 0,
|
|
46
|
+
c: candles?.get(),
|
|
47
|
+
lc: liveCandle?.get() ?? null,
|
|
48
|
+
}),
|
|
49
|
+
(curr) => {
|
|
50
|
+
"worklet";
|
|
51
|
+
if (!curr.candle) {
|
|
52
|
+
if (curr.has && curr.d.length >= 2) {
|
|
53
|
+
lineStash.set(curr.d.slice());
|
|
54
|
+
lineEngineData.set(curr.d);
|
|
55
|
+
} else if (
|
|
56
|
+
!curr.has &&
|
|
57
|
+
curr.m > STASH_MORPH_EPS &&
|
|
58
|
+
lineStash.get().length >= 2
|
|
59
|
+
) {
|
|
60
|
+
lineEngineData.set(lineStash.get());
|
|
61
|
+
} else {
|
|
62
|
+
lineEngineData.set(curr.d);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cArr = curr.c;
|
|
68
|
+
if (curr.has && curr.cLen >= 2 && cArr) {
|
|
69
|
+
candleStash.set(cArr.slice());
|
|
70
|
+
candlesEngine.set(cArr);
|
|
71
|
+
liveEngine.set(curr.lc);
|
|
72
|
+
} else if (
|
|
73
|
+
!curr.has &&
|
|
74
|
+
curr.m > STASH_MORPH_EPS &&
|
|
75
|
+
candleStash.get().length >= 2
|
|
76
|
+
) {
|
|
77
|
+
candlesEngine.set(candleStash.get());
|
|
78
|
+
liveEngine.set(null);
|
|
79
|
+
} else {
|
|
80
|
+
candlesEngine.set(cArr ?? []);
|
|
81
|
+
liveEngine.set(curr.lc);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
[isCandle, data, candles, liveCandle, hasData, morphT],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return { lineEngineData, candlesEngine, liveEngine };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function snapshotSeriesWorklet(s: SeriesConfig[]): SeriesConfig[] {
|
|
91
|
+
"worklet";
|
|
92
|
+
const out: SeriesConfig[] = [];
|
|
93
|
+
for (let i = 0; i < s.length; i++) {
|
|
94
|
+
const x = s[i];
|
|
95
|
+
out.push({ ...x, data: x.data.slice() });
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function useMultiSeriesReverseMorphInputs({
|
|
101
|
+
series,
|
|
102
|
+
hasData,
|
|
103
|
+
morphT,
|
|
104
|
+
}: {
|
|
105
|
+
series: SharedValue<SeriesConfig[]>;
|
|
106
|
+
hasData: SharedValue<boolean>;
|
|
107
|
+
morphT: SharedValue<number>;
|
|
108
|
+
}): SharedValue<SeriesConfig[]> {
|
|
109
|
+
const seriesStash = useSharedValue<SeriesConfig[]>([]);
|
|
110
|
+
const effectiveSeries = useSharedValue<SeriesConfig[]>([]);
|
|
111
|
+
|
|
112
|
+
useAnimatedReaction(
|
|
113
|
+
() => ({
|
|
114
|
+
has: hasData.get(),
|
|
115
|
+
m: morphT.get(),
|
|
116
|
+
live: series.get(),
|
|
117
|
+
}),
|
|
118
|
+
(curr) => {
|
|
119
|
+
"worklet";
|
|
120
|
+
const live = curr.live;
|
|
121
|
+
|
|
122
|
+
let anyReady = false;
|
|
123
|
+
for (let i = 0; i < live.length; i++) {
|
|
124
|
+
if (live[i].data.length >= 2) {
|
|
125
|
+
anyReady = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (curr.has && anyReady) {
|
|
131
|
+
seriesStash.set(snapshotSeriesWorklet(live));
|
|
132
|
+
effectiveSeries.set(live);
|
|
133
|
+
} else {
|
|
134
|
+
let stashReady = false;
|
|
135
|
+
const stash = seriesStash.get();
|
|
136
|
+
for (let i = 0; i < stash.length; i++) {
|
|
137
|
+
if (stash[i].data.length >= 2) {
|
|
138
|
+
stashReady = true;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!curr.has && curr.m > STASH_MORPH_EPS && stashReady) {
|
|
144
|
+
effectiveSeries.set(seriesStash.get());
|
|
145
|
+
} else {
|
|
146
|
+
effectiveSeries.set(live);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
[series, hasData, morphT],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return effectiveSeries;
|
|
154
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useFrameCallback,
|
|
3
|
+
useSharedValue,
|
|
4
|
+
type SharedValue,
|
|
5
|
+
} from "react-native-reanimated";
|
|
6
|
+
import { MS_PER_FRAME_60FPS } from "../constants";
|
|
7
|
+
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
8
|
+
import type { ChartPadding } from "../draw/line";
|
|
9
|
+
import {
|
|
10
|
+
createTradeStreamState,
|
|
11
|
+
projectLabels,
|
|
12
|
+
tickTradeStream,
|
|
13
|
+
type TradeMarker,
|
|
14
|
+
type TradeStreamState,
|
|
15
|
+
} from "../draw/trade";
|
|
16
|
+
import type { TradeEvent } from "../types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Process live trade events into projected on-chart markers.
|
|
20
|
+
* Runs a frame callback that ticks the trade-stream state and produces
|
|
21
|
+
* `TradeMarker[]` positioned within the chart area.
|
|
22
|
+
*/
|
|
23
|
+
export function useTradeStream(
|
|
24
|
+
engine: SingleEngineState,
|
|
25
|
+
tradeStream: SharedValue<TradeEvent[]>,
|
|
26
|
+
padding: ChartPadding,
|
|
27
|
+
active: boolean,
|
|
28
|
+
): SharedValue<TradeMarker[]> {
|
|
29
|
+
const markers = useSharedValue<TradeMarker[]>([]);
|
|
30
|
+
const state = useSharedValue<TradeStreamState>(createTradeStreamState());
|
|
31
|
+
|
|
32
|
+
useFrameCallback(
|
|
33
|
+
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ (
|
|
34
|
+
frameInfo,
|
|
35
|
+
) => {
|
|
36
|
+
"worklet";
|
|
37
|
+
if (!active) {
|
|
38
|
+
if (markers.get().length > 0) markers.set([]);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dt = frameInfo.timeSincePreviousFrame ?? MS_PER_FRAME_60FPS;
|
|
43
|
+
const h = engine.canvasHeight.get();
|
|
44
|
+
const chartTop = padding.top;
|
|
45
|
+
const LABEL_CLEARANCE = 6;
|
|
46
|
+
const chartBottom = h - padding.bottom - LABEL_CLEARANCE;
|
|
47
|
+
|
|
48
|
+
if (chartBottom <= chartTop) return;
|
|
49
|
+
|
|
50
|
+
const s = state.get();
|
|
51
|
+
tickTradeStream(s, tradeStream.get(), dt, chartTop, chartBottom);
|
|
52
|
+
|
|
53
|
+
// Idle tape: nothing to draw and nothing already drawn. Skip the projection
|
|
54
|
+
// and SharedValue write so the TapeLabel derived values don't re-run and we
|
|
55
|
+
// don't allocate a fresh marker array every frame.
|
|
56
|
+
if (s.labels.length === 0 && markers.get().length === 0) return;
|
|
57
|
+
|
|
58
|
+
const chartH = chartBottom - chartTop;
|
|
59
|
+
markers.set(projectLabels(s, chartTop, chartH));
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return markers;
|
|
64
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useDerivedValue, useSharedValue } from "react-native-reanimated";
|
|
3
|
+
|
|
4
|
+
import type { SkFont } from "@shopify/react-native-skia";
|
|
5
|
+
import { MS_PER_FRAME_60FPS } from "../constants";
|
|
6
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
7
|
+
import type { ChartPadding } from "../draw/line";
|
|
8
|
+
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
9
|
+
import { niceTimeInterval } from "../math/intervals";
|
|
10
|
+
import { lerp } from "../math/lerp";
|
|
11
|
+
|
|
12
|
+
export interface XAxisEntry {
|
|
13
|
+
x: number;
|
|
14
|
+
label: string;
|
|
15
|
+
alpha: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const FADE = 0.08;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build a fresh label cache that re-formats every tick's `text` with
|
|
22
|
+
* `formatTime`, preserving each entry's `alpha`. The cache (see {@link useXAxis})
|
|
23
|
+
* formats each tick once for allocation reasons; this refreshes it when the
|
|
24
|
+
* `formatTime` prop changes. Returns a NEW object (and new entry objects) rather
|
|
25
|
+
* than mutating `cache` in place — the cache is also read inside the derived
|
|
26
|
+
* value worklet, and mutating an already-serialized object from the JS thread
|
|
27
|
+
* is unsafe under Reanimated's worklet model. Replacing the SharedValue with a
|
|
28
|
+
* fresh object propagates cleanly to the UI thread.
|
|
29
|
+
*/
|
|
30
|
+
export function reformatXAxisLabels(
|
|
31
|
+
cache: Record<number, { alpha: number; text: string }>,
|
|
32
|
+
formatTime: (t: number) => string,
|
|
33
|
+
): Record<number, { alpha: number; text: string }> {
|
|
34
|
+
const next: Record<number, { alpha: number; text: string }> = {};
|
|
35
|
+
for (const key in cache) {
|
|
36
|
+
next[key] = { alpha: cache[key].alpha, text: formatTime(Number(key) / 100) };
|
|
37
|
+
}
|
|
38
|
+
return next;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function xAxisKeyIsTarget(key: number, targetKeys: number[]): boolean {
|
|
42
|
+
"worklet";
|
|
43
|
+
for (let i = 0; i < targetKeys.length; i++) {
|
|
44
|
+
if (targetKeys[i] === key) return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Insertion sort — worklet-safe, no callback functions. */
|
|
50
|
+
export function insertionSortByX(arr: XAxisEntry[]) {
|
|
51
|
+
"worklet";
|
|
52
|
+
for (let i = 1; i < arr.length; i++) {
|
|
53
|
+
const item = arr[i];
|
|
54
|
+
let j = i - 1;
|
|
55
|
+
while (j >= 0 && arr[j].x > item.x) {
|
|
56
|
+
/* istanbul ignore next -- worklet instrumentation under-counts swaps; insertionSort.test.ts covers behaviour */
|
|
57
|
+
arr[j + 1] = arr[j];
|
|
58
|
+
/* istanbul ignore next */
|
|
59
|
+
j--;
|
|
60
|
+
}
|
|
61
|
+
arr[j + 1] = item;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useXAxis(
|
|
66
|
+
engine: ChartEngineLayout,
|
|
67
|
+
padding: ChartPadding,
|
|
68
|
+
formatTime: (t: number) => string,
|
|
69
|
+
font: SkFont,
|
|
70
|
+
) {
|
|
71
|
+
const labelAlphas = useSharedValue<
|
|
72
|
+
Record<number, { alpha: number; text: string }>
|
|
73
|
+
>({});
|
|
74
|
+
|
|
75
|
+
// A tick's key encodes its time, so its label is formatted once (below) and
|
|
76
|
+
// never re-formatted per frame — an allocation optimization. That assumes
|
|
77
|
+
// `formatTime` is stable; if the consumer swaps it at runtime, already-cached
|
|
78
|
+
// labels would otherwise keep their old text until they scroll off-window.
|
|
79
|
+
// Rebuild the cache with the new formatter (alphas preserved, so no fade
|
|
80
|
+
// restart) whenever the formatter identity changes.
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
labelAlphas.set(reformatXAxisLabels(labelAlphas.get(), formatTime));
|
|
83
|
+
}, [formatTime, labelAlphas]);
|
|
84
|
+
|
|
85
|
+
const xAxisEntries = useDerivedValue(() => {
|
|
86
|
+
const w = engine.canvasWidth.get();
|
|
87
|
+
const h = engine.canvasHeight.get();
|
|
88
|
+
if (w === 0 || h === 0) return [] as XAxisEntry[];
|
|
89
|
+
|
|
90
|
+
const now = engine.timestamp.get();
|
|
91
|
+
const windowSecs = engine.displayWindow.get();
|
|
92
|
+
const winStart = now - windowSecs;
|
|
93
|
+
|
|
94
|
+
const chartLeft = padding.left;
|
|
95
|
+
const chartRight = w - padding.right;
|
|
96
|
+
const chartW = chartRight - chartLeft;
|
|
97
|
+
/* istanbul ignore next -- defensive; Reanimated test mock rarely executes this guard with non-zero w/h */
|
|
98
|
+
if (chartW <= 0) return [] as XAxisEntry[];
|
|
99
|
+
|
|
100
|
+
const fadeZone = 50;
|
|
101
|
+
|
|
102
|
+
// Pick interval from window duration
|
|
103
|
+
const targetPxPerSec = chartW / windowSecs;
|
|
104
|
+
let interval = niceTimeInterval(windowSecs);
|
|
105
|
+
while (interval * targetPxPerSec < 60 && interval < windowSecs) {
|
|
106
|
+
interval *= 2;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Generate target label keys (plain array, no Set — worklet-safe)
|
|
110
|
+
const firstTime = Math.ceil((winStart - interval) / interval) * interval;
|
|
111
|
+
const targetKeys: number[] = [];
|
|
112
|
+
for (
|
|
113
|
+
let t = firstTime;
|
|
114
|
+
t <= now + interval && targetKeys.length < 30;
|
|
115
|
+
t += interval
|
|
116
|
+
) {
|
|
117
|
+
targetKeys.push(Math.round(t * 100));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const alphas = labelAlphas.get();
|
|
121
|
+
|
|
122
|
+
// Create labels for new keys only. A key encodes its time, so the formatted
|
|
123
|
+
// text never changes — re-formatting (and re-allocating the entry) every
|
|
124
|
+
// frame just churns strings/objects for the GC.
|
|
125
|
+
for (let i = 0; i < targetKeys.length; i++) {
|
|
126
|
+
const key = targetKeys[i];
|
|
127
|
+
if (!alphas[key]) {
|
|
128
|
+
alphas[key] = { alpha: 0, text: formatTime(key / 100) };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Update alphas
|
|
133
|
+
const allKeys = Object.keys(alphas);
|
|
134
|
+
for (let i = 0; i < allKeys.length; i++) {
|
|
135
|
+
const key = Number(allKeys[i]);
|
|
136
|
+
const label = alphas[key];
|
|
137
|
+
const x = chartLeft + ((key / 100 - winStart) / windowSecs) * chartW;
|
|
138
|
+
|
|
139
|
+
const fromLeft = x - chartLeft;
|
|
140
|
+
const fromRight = chartRight - x;
|
|
141
|
+
const fromEdge = Math.min(fromLeft, fromRight);
|
|
142
|
+
const edgeAlpha =
|
|
143
|
+
fromEdge >= fadeZone ? 1 : fromEdge <= 0 ? 0 : fromEdge / fadeZone;
|
|
144
|
+
|
|
145
|
+
const target = xAxisKeyIsTarget(key, targetKeys) ? edgeAlpha : 0;
|
|
146
|
+
let next = lerp(label.alpha, target, FADE, MS_PER_FRAME_60FPS);
|
|
147
|
+
if (Math.abs(next - target) < 0.02) next = target;
|
|
148
|
+
|
|
149
|
+
if (next < 0.01 && target === 0) {
|
|
150
|
+
delete alphas[key];
|
|
151
|
+
} else {
|
|
152
|
+
// Mutate in place — text is stable, so no need to reallocate the entry.
|
|
153
|
+
label.alpha = next;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
labelAlphas.set(alphas);
|
|
158
|
+
|
|
159
|
+
// Collect visible labels
|
|
160
|
+
const raw: XAxisEntry[] = [];
|
|
161
|
+
const visibleKeys = Object.keys(alphas);
|
|
162
|
+
for (let i = 0; i < visibleKeys.length; i++) {
|
|
163
|
+
const key = Number(visibleKeys[i]);
|
|
164
|
+
const label = alphas[key];
|
|
165
|
+
/* istanbul ignore next -- alpha band skip is timing-sensitive under RTL + Reanimated mocks */
|
|
166
|
+
if (label.alpha < 0.02) continue;
|
|
167
|
+
const x = chartLeft + ((key / 100 - winStart) / windowSecs) * chartW;
|
|
168
|
+
/* istanbul ignore next -- horizontal clip; x stays in-band for consistent time keys */
|
|
169
|
+
if (x < chartLeft - 20 || x > chartRight) continue;
|
|
170
|
+
raw.push({ x, label: label.text, alpha: label.alpha });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Worklet-safe sort (no callback)
|
|
174
|
+
insertionSortByX(raw);
|
|
175
|
+
|
|
176
|
+
// Overlap resolution: keep higher-alpha label when two collide
|
|
177
|
+
const drawn: XAxisEntry[] = [];
|
|
178
|
+
for (let i = 0; i < raw.length; i++) {
|
|
179
|
+
const entry = raw[i];
|
|
180
|
+
const textW = measureFontTextWidth(font, entry.label);
|
|
181
|
+
const left = entry.x - textW / 2;
|
|
182
|
+
if (drawn.length > 0) {
|
|
183
|
+
const prev = drawn[drawn.length - 1];
|
|
184
|
+
const prevW = measureFontTextWidth(font, prev.label);
|
|
185
|
+
const prevRight = prev.x + prevW / 2;
|
|
186
|
+
if (left < prevRight + 8) {
|
|
187
|
+
if (entry.alpha > prev.alpha) {
|
|
188
|
+
drawn[drawn.length - 1] = entry;
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
drawn.push(entry);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return drawn;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return { xAxisEntries, font };
|
|
200
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useDerivedValue, useSharedValue } from "react-native-reanimated";
|
|
2
|
+
|
|
3
|
+
import type { SkFont } from "@shopify/react-native-skia";
|
|
4
|
+
import { MS_PER_FRAME_60FPS } from "../constants";
|
|
5
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
6
|
+
import { computeGridEntries } from "../draw/grid";
|
|
7
|
+
import type { ChartPadding } from "../draw/line";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compute Y-axis grid entries (values + labels) with animated fade-in/out.
|
|
11
|
+
* Uses `computeGridEntries` to pick nice intervals and track label alpha for
|
|
12
|
+
* smooth transitions when the value range changes.
|
|
13
|
+
*/
|
|
14
|
+
export function useYAxis(
|
|
15
|
+
engine: ChartEngineLayout,
|
|
16
|
+
padding: ChartPadding,
|
|
17
|
+
formatValue: (v: number) => string,
|
|
18
|
+
font: SkFont,
|
|
19
|
+
minGap = 36,
|
|
20
|
+
) {
|
|
21
|
+
const prevInterval = useSharedValue(0);
|
|
22
|
+
const labelAlphas = useSharedValue<Record<number, number>>({});
|
|
23
|
+
|
|
24
|
+
const yAxisEntries = useDerivedValue(() => {
|
|
25
|
+
const dt = MS_PER_FRAME_60FPS;
|
|
26
|
+
|
|
27
|
+
const alphas = labelAlphas.get();
|
|
28
|
+
const result = computeGridEntries(
|
|
29
|
+
engine.displayMin.get(),
|
|
30
|
+
engine.displayMax.get(),
|
|
31
|
+
engine.canvasHeight.get(),
|
|
32
|
+
padding.top,
|
|
33
|
+
padding.bottom,
|
|
34
|
+
prevInterval.get(),
|
|
35
|
+
alphas,
|
|
36
|
+
formatValue,
|
|
37
|
+
dt,
|
|
38
|
+
minGap,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
prevInterval.set(result.interval);
|
|
42
|
+
labelAlphas.set(alphas);
|
|
43
|
+
|
|
44
|
+
return result.entries;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return { yAxisEntries, font };
|
|
48
|
+
}
|