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,331 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Circle,
|
|
3
|
+
Group,
|
|
4
|
+
Image as SkiaImage,
|
|
5
|
+
Path,
|
|
6
|
+
Skia,
|
|
7
|
+
Text as SkiaText,
|
|
8
|
+
type SkFont,
|
|
9
|
+
type SkPath,
|
|
10
|
+
} from "@shopify/react-native-skia";
|
|
11
|
+
import { useRef, useState } from "react";
|
|
12
|
+
import {
|
|
13
|
+
useAnimatedReaction,
|
|
14
|
+
useDerivedValue,
|
|
15
|
+
type SharedValue,
|
|
16
|
+
} from "react-native-reanimated";
|
|
17
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
18
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
19
|
+
import type { ChartPadding } from "../draw/line";
|
|
20
|
+
import { markersSignature, projectPoint } from "../math/markers";
|
|
21
|
+
import type {
|
|
22
|
+
LiveChartPalette,
|
|
23
|
+
LiveChartPoint,
|
|
24
|
+
Marker,
|
|
25
|
+
MarkerKind,
|
|
26
|
+
SeriesConfig,
|
|
27
|
+
} from "../types";
|
|
28
|
+
|
|
29
|
+
/** Default glyph color per kind when `marker.color` is unset. */
|
|
30
|
+
function defaultMarkerColor(kind: MarkerKind, palette: LiveChartPalette): string {
|
|
31
|
+
switch (kind) {
|
|
32
|
+
case "trade":
|
|
33
|
+
return palette.line;
|
|
34
|
+
case "boost":
|
|
35
|
+
return palette.refLine;
|
|
36
|
+
case "graduation":
|
|
37
|
+
return palette.dotUp;
|
|
38
|
+
case "winner":
|
|
39
|
+
return palette.dotUp;
|
|
40
|
+
case "clawback":
|
|
41
|
+
return palette.refLabel;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const OFF = -9999;
|
|
46
|
+
const DEFAULT_ICON_SIZE = 16;
|
|
47
|
+
/** Circular badge: padding around the icon glyph, background-colored ring width,
|
|
48
|
+
* and the icon color drawn on the fill. */
|
|
49
|
+
const PILL_PAD = 2;
|
|
50
|
+
const PILL_BORDER = 2;
|
|
51
|
+
const PILL_TEXT_COLOR = "#ffffff";
|
|
52
|
+
|
|
53
|
+
function MarkerGlyph({
|
|
54
|
+
marker,
|
|
55
|
+
color,
|
|
56
|
+
bgColor,
|
|
57
|
+
engine,
|
|
58
|
+
padding,
|
|
59
|
+
seriesSV,
|
|
60
|
+
lineDataSV,
|
|
61
|
+
font,
|
|
62
|
+
axisY,
|
|
63
|
+
}: {
|
|
64
|
+
marker: Marker;
|
|
65
|
+
color: string;
|
|
66
|
+
/** Chart background color, drawn as a ring behind a `pill` badge. */
|
|
67
|
+
bgColor?: string;
|
|
68
|
+
engine: ChartEngineLayout;
|
|
69
|
+
padding: ChartPadding;
|
|
70
|
+
seriesSV?: SharedValue<SeriesConfig[]>;
|
|
71
|
+
lineDataSV?: SharedValue<LiveChartPoint[]>;
|
|
72
|
+
font: SkFont;
|
|
73
|
+
axisY: SharedValue<number>;
|
|
74
|
+
}) {
|
|
75
|
+
// Capture only primitive anchor fields — never the full marker (which may
|
|
76
|
+
// carry non-serializable `data` / `image`) — in the worklet closure.
|
|
77
|
+
const { kind, icon, image, pill } = marker;
|
|
78
|
+
const time = marker.time;
|
|
79
|
+
const value = marker.value;
|
|
80
|
+
const seriesId = marker.seriesId;
|
|
81
|
+
const size = marker.size ?? DEFAULT_ICON_SIZE;
|
|
82
|
+
|
|
83
|
+
// Each glyph projects its OWN marker — no shared index buffer, so reordering
|
|
84
|
+
// the marker array can't make a glyph flash another marker's position.
|
|
85
|
+
const layout = useDerivedValue(() =>
|
|
86
|
+
projectPoint(time, value, seriesId, {
|
|
87
|
+
canvasWidth: engine.canvasWidth.get(),
|
|
88
|
+
canvasHeight: engine.canvasHeight.get(),
|
|
89
|
+
padTop: padding.top,
|
|
90
|
+
padBottom: padding.bottom,
|
|
91
|
+
padLeft: padding.left,
|
|
92
|
+
padRight: padding.right,
|
|
93
|
+
timestamp: engine.timestamp.get(),
|
|
94
|
+
displayWindow: engine.displayWindow.get(),
|
|
95
|
+
displayMin: engine.displayMin.get(),
|
|
96
|
+
displayMax: engine.displayMax.get(),
|
|
97
|
+
series: seriesSV?.get(),
|
|
98
|
+
lineData: lineDataSV?.get(),
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const cx = useDerivedValue(() =>
|
|
103
|
+
layout.get().visible ? layout.get().x : OFF,
|
|
104
|
+
);
|
|
105
|
+
const cy = useDerivedValue(() =>
|
|
106
|
+
layout.get().visible ? layout.get().y : OFF,
|
|
107
|
+
);
|
|
108
|
+
const opacity = useDerivedValue(() => (layout.get().visible ? 1 : 0));
|
|
109
|
+
|
|
110
|
+
const cacheRef = useRef<{ a: SkPath; b: SkPath; tick: boolean } | null>(null);
|
|
111
|
+
if (cacheRef.current === null) {
|
|
112
|
+
cacheRef.current = { a: Skia.Path.Make(), b: Skia.Path.Make(), tick: false };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const glyphPath = useDerivedValue(() => {
|
|
116
|
+
const cache = cacheRef.current!;
|
|
117
|
+
cache.tick = !cache.tick;
|
|
118
|
+
const p: SkPath = cache.tick ? cache.a : cache.b;
|
|
119
|
+
p.reset();
|
|
120
|
+
const l = layout.get();
|
|
121
|
+
if (!l.visible) return p;
|
|
122
|
+
const x = l.x;
|
|
123
|
+
const y = l.y;
|
|
124
|
+
if (kind === "winner") {
|
|
125
|
+
const outer = 7;
|
|
126
|
+
const inner = 3;
|
|
127
|
+
for (let k = 0; k < 10; k++) {
|
|
128
|
+
const ang = -Math.PI / 2 + (k * Math.PI) / 5;
|
|
129
|
+
const rad = k % 2 === 0 ? outer : inner;
|
|
130
|
+
const px = x + rad * Math.cos(ang);
|
|
131
|
+
const py = y + rad * Math.sin(ang);
|
|
132
|
+
if (k === 0) p.moveTo(px, py);
|
|
133
|
+
else p.lineTo(px, py);
|
|
134
|
+
}
|
|
135
|
+
p.close();
|
|
136
|
+
} else if (kind === "boost") {
|
|
137
|
+
const L = 6;
|
|
138
|
+
for (let k = 0; k < 4; k++) {
|
|
139
|
+
const ang = (k * Math.PI) / 4;
|
|
140
|
+
const dx = L * Math.cos(ang);
|
|
141
|
+
const dy = L * Math.sin(ang);
|
|
142
|
+
p.moveTo(x - dx, y - dy);
|
|
143
|
+
p.lineTo(x + dx, y + dy);
|
|
144
|
+
}
|
|
145
|
+
} else if (kind === "graduation") {
|
|
146
|
+
p.moveTo(x, axisY.get());
|
|
147
|
+
p.lineTo(x, y);
|
|
148
|
+
p.moveTo(x, y - 7);
|
|
149
|
+
p.lineTo(x + 8, y - 4);
|
|
150
|
+
p.lineTo(x, y - 1);
|
|
151
|
+
p.close();
|
|
152
|
+
} else if (kind === "clawback") {
|
|
153
|
+
const s = 5;
|
|
154
|
+
const ay = axisY.get() - s;
|
|
155
|
+
p.moveTo(x - s, ay);
|
|
156
|
+
p.lineTo(x + s, ay);
|
|
157
|
+
p.lineTo(x + s, ay + s * 2);
|
|
158
|
+
p.lineTo(x - s, ay + s * 2);
|
|
159
|
+
p.close();
|
|
160
|
+
}
|
|
161
|
+
return p;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Image icon centered on the marker.
|
|
165
|
+
const imgX = useDerivedValue(() =>
|
|
166
|
+
layout.get().visible ? layout.get().x - size / 2 : OFF,
|
|
167
|
+
);
|
|
168
|
+
const imgY = useDerivedValue(() =>
|
|
169
|
+
layout.get().visible ? layout.get().y - size / 2 : OFF,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Text / emoji icon centering — width + baseline shift depend only on the
|
|
173
|
+
// (stable) font and icon, so measure once instead of every frame. Skia
|
|
174
|
+
// `measureText`/`getMetrics` both allocate, so hoisting them out of the
|
|
175
|
+
// per-frame worklet removes one measure + one metrics call per glyph/frame.
|
|
176
|
+
// Center the icon glyph on the marker using its measured bounds — tighter and
|
|
177
|
+
// more accurate than font ascent/descent, so `+` / `−` etc. sit centered both
|
|
178
|
+
// horizontally and vertically in the badge. Stable per render, not per frame.
|
|
179
|
+
const iconBounds = font.measureText(icon ?? "");
|
|
180
|
+
const iconDX = iconBounds.x + iconBounds.width / 2;
|
|
181
|
+
const iconDY = iconBounds.y + iconBounds.height / 2;
|
|
182
|
+
|
|
183
|
+
const iconX = useDerivedValue(() =>
|
|
184
|
+
layout.get().visible ? layout.get().x - iconDX : OFF,
|
|
185
|
+
);
|
|
186
|
+
const iconY = useDerivedValue(() =>
|
|
187
|
+
layout.get().visible ? layout.get().y - iconDY : OFF,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Circular badge radius — fits the glyph's larger dimension + padding.
|
|
191
|
+
const pillR = Math.max(iconBounds.width, iconBounds.height) / 2 + PILL_PAD;
|
|
192
|
+
|
|
193
|
+
if (image) {
|
|
194
|
+
return (
|
|
195
|
+
<Group opacity={opacity}>
|
|
196
|
+
<SkiaImage
|
|
197
|
+
image={image}
|
|
198
|
+
x={imgX}
|
|
199
|
+
y={imgY}
|
|
200
|
+
width={size}
|
|
201
|
+
height={size}
|
|
202
|
+
fit="contain"
|
|
203
|
+
/>
|
|
204
|
+
</Group>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (icon) {
|
|
209
|
+
if (pill) {
|
|
210
|
+
// Filled circular badge in the marker color with the icon in white — e.g.
|
|
211
|
+
// a green `+` buy / red `−` sell tag. A background-colored ring underneath
|
|
212
|
+
// separates it from the line passing behind.
|
|
213
|
+
return (
|
|
214
|
+
<Group opacity={opacity}>
|
|
215
|
+
{bgColor !== undefined && (
|
|
216
|
+
<Circle cx={cx} cy={cy} r={pillR + PILL_BORDER} color={bgColor} />
|
|
217
|
+
)}
|
|
218
|
+
<Circle cx={cx} cy={cy} r={pillR} color={color} />
|
|
219
|
+
<SkiaText
|
|
220
|
+
x={iconX}
|
|
221
|
+
y={iconY}
|
|
222
|
+
text={icon}
|
|
223
|
+
font={font}
|
|
224
|
+
color={PILL_TEXT_COLOR}
|
|
225
|
+
/>
|
|
226
|
+
</Group>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return (
|
|
230
|
+
<Group opacity={opacity}>
|
|
231
|
+
<SkiaText x={iconX} y={iconY} text={icon} font={font} color={color} />
|
|
232
|
+
</Group>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (kind === "trade") {
|
|
237
|
+
return (
|
|
238
|
+
<Group opacity={opacity}>
|
|
239
|
+
<Circle cx={cx} cy={cy} r={5} color={color} style="stroke" strokeWidth={2} />
|
|
240
|
+
<Circle cx={cx} cy={cy} r={2} color={color} />
|
|
241
|
+
</Group>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const fill = kind === "winner" || kind === "clawback";
|
|
246
|
+
return (
|
|
247
|
+
<Group opacity={opacity}>
|
|
248
|
+
<Path
|
|
249
|
+
path={glyphPath}
|
|
250
|
+
color={color}
|
|
251
|
+
style={fill ? "fill" : "stroke"}
|
|
252
|
+
strokeWidth={1.5}
|
|
253
|
+
strokeCap="round"
|
|
254
|
+
strokeJoin="round"
|
|
255
|
+
/>
|
|
256
|
+
</Group>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Renders the `markers` SharedValue as canvas glyphs. Marker metadata is
|
|
262
|
+
* mirrored into React state when it changes; each glyph projects its own
|
|
263
|
+
* position every frame (keyed by id), so adding/removing markers never makes a
|
|
264
|
+
* surviving glyph flash another marker's position.
|
|
265
|
+
*
|
|
266
|
+
* Glyph precedence per marker: `image` → `icon` (text) → built-in `kind` shape.
|
|
267
|
+
*/
|
|
268
|
+
export function MarkerOverlay({
|
|
269
|
+
markers,
|
|
270
|
+
engine,
|
|
271
|
+
padding,
|
|
272
|
+
palette,
|
|
273
|
+
font,
|
|
274
|
+
series,
|
|
275
|
+
lineData,
|
|
276
|
+
}: {
|
|
277
|
+
markers: SharedValue<Marker[]>;
|
|
278
|
+
engine: ChartEngineLayout;
|
|
279
|
+
padding: ChartPadding;
|
|
280
|
+
palette: LiveChartPalette;
|
|
281
|
+
font: SkFont;
|
|
282
|
+
/** Multi-series data, used to anchor markers by `seriesId`. */
|
|
283
|
+
series?: SharedValue<SeriesConfig[]>;
|
|
284
|
+
/** Single-series line data; anchors markers that omit `value`. */
|
|
285
|
+
lineData?: SharedValue<LiveChartPoint[]>;
|
|
286
|
+
}) {
|
|
287
|
+
// Seed from the current markers at mount; the reaction below keeps it in sync.
|
|
288
|
+
const [snapshot, setSnapshot] = useState<Marker[]>(() => markers.get().slice());
|
|
289
|
+
|
|
290
|
+
// Read the `markers` prop from closure rather than a SharedValue passed
|
|
291
|
+
// through `scheduleOnRN`: the handle serialized across the worklet→JS
|
|
292
|
+
// boundary exposes the native `.value` accessor but NOT the `.get()` method,
|
|
293
|
+
// so calling `.get()` on it throws ("sv.get is not a function").
|
|
294
|
+
const pull = () => {
|
|
295
|
+
setSnapshot(markers.get().slice());
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
useAnimatedReaction(
|
|
299
|
+
() => markersSignature(markers.get()),
|
|
300
|
+
/* istanbul ignore next -- scheduleOnRN from UI-thread reaction */
|
|
301
|
+
(sig, prev) => {
|
|
302
|
+
if (sig !== prev) scheduleOnRN(pull);
|
|
303
|
+
},
|
|
304
|
+
[markers, pull],
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const axisY = useDerivedValue(
|
|
308
|
+
() => engine.canvasHeight.get() - padding.bottom,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const bgColor = `rgb(${palette.bgRgb[0]}, ${palette.bgRgb[1]}, ${palette.bgRgb[2]})`;
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<Group>
|
|
315
|
+
{snapshot.map((m) => (
|
|
316
|
+
<MarkerGlyph
|
|
317
|
+
key={m.id}
|
|
318
|
+
marker={m}
|
|
319
|
+
color={m.color ?? defaultMarkerColor(m.kind, palette)}
|
|
320
|
+
bgColor={bgColor}
|
|
321
|
+
engine={engine}
|
|
322
|
+
padding={padding}
|
|
323
|
+
seriesSV={series}
|
|
324
|
+
lineDataSV={lineData}
|
|
325
|
+
font={font}
|
|
326
|
+
axisY={axisY}
|
|
327
|
+
/>
|
|
328
|
+
))}
|
|
329
|
+
</Group>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Circle, Group } from "@shopify/react-native-skia";
|
|
2
|
+
|
|
3
|
+
import { useDerivedValue } from "react-native-reanimated";
|
|
4
|
+
import { MAX_MULTI_SERIES } from "../constants";
|
|
5
|
+
import type { ChartPadding } from "../draw/line";
|
|
6
|
+
import type { ResolvedPulseConfig } from "../core/resolveConfig";
|
|
7
|
+
import type { MultiEngineState } from "../core/useLiveChartEngine";
|
|
8
|
+
|
|
9
|
+
const MIN_PULSE_RADIUS = 6;
|
|
10
|
+
const PULSE_STAGGER_MS = 200;
|
|
11
|
+
|
|
12
|
+
function SeriesDotAtIndex({
|
|
13
|
+
index,
|
|
14
|
+
engine,
|
|
15
|
+
padding,
|
|
16
|
+
color,
|
|
17
|
+
radius,
|
|
18
|
+
pulse,
|
|
19
|
+
}: {
|
|
20
|
+
index: number;
|
|
21
|
+
engine: MultiEngineState;
|
|
22
|
+
padding: ChartPadding;
|
|
23
|
+
color: string;
|
|
24
|
+
radius: number;
|
|
25
|
+
pulse: ResolvedPulseConfig | null;
|
|
26
|
+
}) {
|
|
27
|
+
const dotX = useDerivedValue(() => {
|
|
28
|
+
const w = engine.canvasWidth.value;
|
|
29
|
+
if (w === 0) return -100;
|
|
30
|
+
return w - padding.right;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const dotY = useDerivedValue(() => {
|
|
34
|
+
const h = engine.canvasHeight.value;
|
|
35
|
+
if (h === 0) return -100;
|
|
36
|
+
const s = engine.series.value;
|
|
37
|
+
const displays = engine.displaySeriesValues.value;
|
|
38
|
+
if (index >= s.length) return -100;
|
|
39
|
+
const chartH = h - padding.top - padding.bottom;
|
|
40
|
+
const dMin = engine.displayMin.value;
|
|
41
|
+
const dMax = engine.displayMax.value;
|
|
42
|
+
const valRange = dMax - dMin;
|
|
43
|
+
const v = displays[index] ?? s[index].value;
|
|
44
|
+
if (valRange === 0) return padding.top + chartH / 2;
|
|
45
|
+
return padding.top + ((dMax - v) / valRange) * chartH;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const opacity = useDerivedValue(() => {
|
|
49
|
+
const s = engine.series.value;
|
|
50
|
+
const op = engine.seriesOpacities.value;
|
|
51
|
+
if (index >= s.length || index >= op.length) return 0;
|
|
52
|
+
return op[index] ?? 0;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const pulseRadius = useDerivedValue(() => {
|
|
56
|
+
if (!pulse) return 0;
|
|
57
|
+
const nowMs = engine.timestamp.value * 1000 + index * PULSE_STAGGER_MS;
|
|
58
|
+
const t = (nowMs % pulse.interval) / pulse.duration;
|
|
59
|
+
if (t >= 1) return 0;
|
|
60
|
+
return MIN_PULSE_RADIUS + t * (pulse.maxRadius - MIN_PULSE_RADIUS);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const pulseOpacity = useDerivedValue(() => {
|
|
64
|
+
if (!pulse) return 0;
|
|
65
|
+
const nowMs = engine.timestamp.value * 1000 + index * PULSE_STAGGER_MS;
|
|
66
|
+
const t = (nowMs % pulse.interval) / pulse.duration;
|
|
67
|
+
if (t >= 1) return 0;
|
|
68
|
+
return pulse.opacity * (1 - t);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Group opacity={opacity}>
|
|
73
|
+
{pulse && (
|
|
74
|
+
<Circle
|
|
75
|
+
cx={dotX}
|
|
76
|
+
cy={dotY}
|
|
77
|
+
r={pulseRadius}
|
|
78
|
+
color={color}
|
|
79
|
+
style="stroke"
|
|
80
|
+
strokeWidth={pulse.strokeWidth}
|
|
81
|
+
opacity={pulseOpacity}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
<Circle cx={dotX} cy={dotY} r={radius} color={color} />
|
|
85
|
+
</Group>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function MultiSeriesDots({
|
|
90
|
+
engine,
|
|
91
|
+
padding,
|
|
92
|
+
colors,
|
|
93
|
+
radius,
|
|
94
|
+
pulse,
|
|
95
|
+
}: {
|
|
96
|
+
engine: MultiEngineState;
|
|
97
|
+
padding: ChartPadding;
|
|
98
|
+
colors: string[];
|
|
99
|
+
radius: number;
|
|
100
|
+
pulse: ResolvedPulseConfig | null;
|
|
101
|
+
}) {
|
|
102
|
+
return (
|
|
103
|
+
<Group>
|
|
104
|
+
{Array.from({ length: MAX_MULTI_SERIES }, (_, i) => (
|
|
105
|
+
<SeriesDotAtIndex
|
|
106
|
+
key={i}
|
|
107
|
+
index={i}
|
|
108
|
+
engine={engine}
|
|
109
|
+
padding={padding}
|
|
110
|
+
color={colors[i] ?? "#ffffff"}
|
|
111
|
+
radius={radius}
|
|
112
|
+
pulse={pulse}
|
|
113
|
+
/>
|
|
114
|
+
))}
|
|
115
|
+
</Group>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Blur, Group, Path, Skia } from "@shopify/react-native-skia";
|
|
2
|
+
import { DashPathEffect } from "@shopify/react-native-skia";
|
|
3
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
4
|
+
import type { SeriesLineStyle } from "../core/multiSeriesLayout";
|
|
5
|
+
import { SERIES_COLORS } from "../theme";
|
|
6
|
+
import type { SeriesConfig } from "../types";
|
|
7
|
+
|
|
8
|
+
export function MultiSeriesStroke({
|
|
9
|
+
index,
|
|
10
|
+
paths,
|
|
11
|
+
opacities,
|
|
12
|
+
series,
|
|
13
|
+
strokeWidth,
|
|
14
|
+
lineStyle,
|
|
15
|
+
}: {
|
|
16
|
+
index: number;
|
|
17
|
+
paths: SharedValue<ReturnType<typeof Skia.Path.Make>[]>;
|
|
18
|
+
opacities: SharedValue<number[]>;
|
|
19
|
+
series: SharedValue<SeriesConfig[]>;
|
|
20
|
+
strokeWidth: number;
|
|
21
|
+
/** Per-series static stroke style (dash / width / glow). */
|
|
22
|
+
lineStyle?: SeriesLineStyle;
|
|
23
|
+
}) {
|
|
24
|
+
const path = useDerivedValue(
|
|
25
|
+
/* istanbul ignore next -- Skia path derived on UI thread */
|
|
26
|
+
() => paths.value[index] ?? Skia.Path.Make(),
|
|
27
|
+
);
|
|
28
|
+
const opacity = useDerivedValue(
|
|
29
|
+
/* istanbul ignore next -- Reanimated derived on UI thread */
|
|
30
|
+
() => {
|
|
31
|
+
const o = opacities.value;
|
|
32
|
+
if (index >= o.length) return 0;
|
|
33
|
+
return o[index] ?? 0;
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
const color = useDerivedValue(
|
|
37
|
+
/* istanbul ignore next -- Reanimated derived on UI thread */
|
|
38
|
+
() => {
|
|
39
|
+
const s = series.value;
|
|
40
|
+
if (index >= s.length) return "#ffffff";
|
|
41
|
+
return (s[index].color ??
|
|
42
|
+
SERIES_COLORS[index % SERIES_COLORS.length]) as string;
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const sw = lineStyle?.strokeWidth ?? strokeWidth;
|
|
47
|
+
const dashed = lineStyle?.dashed ?? false;
|
|
48
|
+
const intervals = lineStyle?.intervals ?? [6, 4];
|
|
49
|
+
const glow = lineStyle?.glow ?? false;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Group opacity={opacity}>
|
|
53
|
+
{glow && (
|
|
54
|
+
<Group opacity={0.35}>
|
|
55
|
+
<Path
|
|
56
|
+
path={path}
|
|
57
|
+
style="stroke"
|
|
58
|
+
strokeWidth={sw * 2.5}
|
|
59
|
+
color={color}
|
|
60
|
+
strokeCap="round"
|
|
61
|
+
strokeJoin="round"
|
|
62
|
+
>
|
|
63
|
+
<Blur blur={4} />
|
|
64
|
+
</Path>
|
|
65
|
+
</Group>
|
|
66
|
+
)}
|
|
67
|
+
<Path
|
|
68
|
+
path={path}
|
|
69
|
+
style="stroke"
|
|
70
|
+
strokeWidth={sw}
|
|
71
|
+
color={color}
|
|
72
|
+
strokeCap="round"
|
|
73
|
+
strokeJoin="round"
|
|
74
|
+
>
|
|
75
|
+
{dashed && <DashPathEffect intervals={intervals} />}
|
|
76
|
+
</Path>
|
|
77
|
+
</Group>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Group,
|
|
3
|
+
Text as SkiaText,
|
|
4
|
+
type SkFont,
|
|
5
|
+
} from "@shopify/react-native-skia";
|
|
6
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
7
|
+
import { MAX_MULTI_SERIES } from "../constants";
|
|
8
|
+
import type { TooltipLayout } from "../hooks/crosshairShared";
|
|
9
|
+
import type { LiveChartPalette } from "../types";
|
|
10
|
+
|
|
11
|
+
const TOOLTIP_STACK_SLOTS = MAX_MULTI_SERIES + 1;
|
|
12
|
+
|
|
13
|
+
function TooltipStackLine({
|
|
14
|
+
index,
|
|
15
|
+
tooltipLayout,
|
|
16
|
+
font,
|
|
17
|
+
palette,
|
|
18
|
+
}: {
|
|
19
|
+
index: number;
|
|
20
|
+
tooltipLayout: SharedValue<TooltipLayout>;
|
|
21
|
+
font: SkFont;
|
|
22
|
+
palette: LiveChartPalette;
|
|
23
|
+
}) {
|
|
24
|
+
const opacity = useDerivedValue(() => {
|
|
25
|
+
const sl = tooltipLayout.value.stackedLines;
|
|
26
|
+
if (!sl || index >= sl.length) return 0;
|
|
27
|
+
return 1;
|
|
28
|
+
});
|
|
29
|
+
const text = useDerivedValue(() => {
|
|
30
|
+
const sl = tooltipLayout.value.stackedLines;
|
|
31
|
+
if (!sl || index >= sl.length) return "";
|
|
32
|
+
return sl[index].text;
|
|
33
|
+
});
|
|
34
|
+
const x = useDerivedValue(() => {
|
|
35
|
+
const sl = tooltipLayout.value.stackedLines;
|
|
36
|
+
if (!sl || index >= sl.length) return -400;
|
|
37
|
+
return sl[index].textX;
|
|
38
|
+
});
|
|
39
|
+
const y = useDerivedValue(() => {
|
|
40
|
+
const sl = tooltipLayout.value.stackedLines;
|
|
41
|
+
if (!sl || index >= sl.length) return 0;
|
|
42
|
+
return sl[index].baselineY;
|
|
43
|
+
});
|
|
44
|
+
const color = useDerivedValue(() => {
|
|
45
|
+
const sl = tooltipLayout.value.stackedLines;
|
|
46
|
+
if (!sl || index >= sl.length) return palette.tooltipText;
|
|
47
|
+
return sl[index].dim ? palette.gridLabel : palette.tooltipText;
|
|
48
|
+
});
|
|
49
|
+
return (
|
|
50
|
+
<Group opacity={opacity}>
|
|
51
|
+
<SkiaText x={x} y={y} text={text} font={font} color={color} />
|
|
52
|
+
</Group>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function MultiSeriesTooltipStack({
|
|
57
|
+
tooltipLayout,
|
|
58
|
+
font,
|
|
59
|
+
palette,
|
|
60
|
+
}: {
|
|
61
|
+
tooltipLayout: SharedValue<TooltipLayout>;
|
|
62
|
+
font: SkFont;
|
|
63
|
+
palette: LiveChartPalette;
|
|
64
|
+
}) {
|
|
65
|
+
return (
|
|
66
|
+
<Group>
|
|
67
|
+
{Array.from({ length: TOOLTIP_STACK_SLOTS }, (_, i) => (
|
|
68
|
+
<TooltipStackLine
|
|
69
|
+
key={i}
|
|
70
|
+
index={i}
|
|
71
|
+
tooltipLayout={tooltipLayout}
|
|
72
|
+
font={font}
|
|
73
|
+
palette={palette}
|
|
74
|
+
/>
|
|
75
|
+
))}
|
|
76
|
+
</Group>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Group,
|
|
3
|
+
Text as SkiaText,
|
|
4
|
+
type SkFont,
|
|
5
|
+
} from "@shopify/react-native-skia";
|
|
6
|
+
import { useDerivedValue } from "react-native-reanimated";
|
|
7
|
+
|
|
8
|
+
import { MAX_MULTI_SERIES } from "../constants";
|
|
9
|
+
import type { ChartPadding } from "../draw/line";
|
|
10
|
+
import type { MultiEngineState } from "../core/useLiveChartEngine";
|
|
11
|
+
|
|
12
|
+
const LABEL_GAP = 8;
|
|
13
|
+
|
|
14
|
+
function SeriesValueLabelAtIndex({
|
|
15
|
+
index,
|
|
16
|
+
engine,
|
|
17
|
+
padding,
|
|
18
|
+
color,
|
|
19
|
+
font,
|
|
20
|
+
dotRadius,
|
|
21
|
+
}: {
|
|
22
|
+
index: number;
|
|
23
|
+
engine: MultiEngineState;
|
|
24
|
+
padding: ChartPadding;
|
|
25
|
+
color: string;
|
|
26
|
+
font: SkFont;
|
|
27
|
+
dotRadius: number;
|
|
28
|
+
}) {
|
|
29
|
+
const labelX = useDerivedValue(() => {
|
|
30
|
+
const w = engine.canvasWidth.value;
|
|
31
|
+
if (w === 0) return -200;
|
|
32
|
+
return w - padding.right + dotRadius + LABEL_GAP;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const labelY = useDerivedValue(() => {
|
|
36
|
+
const h = engine.canvasHeight.value;
|
|
37
|
+
if (h === 0) return -200;
|
|
38
|
+
const s = engine.series.value;
|
|
39
|
+
const displays = engine.displaySeriesValues.value;
|
|
40
|
+
if (index >= s.length) return -200;
|
|
41
|
+
const chartH = h - padding.top - padding.bottom;
|
|
42
|
+
const dMin = engine.displayMin.value;
|
|
43
|
+
const dMax = engine.displayMax.value;
|
|
44
|
+
const valRange = dMax - dMin;
|
|
45
|
+
const v = displays[index] ?? s[index].value;
|
|
46
|
+
const y =
|
|
47
|
+
valRange === 0
|
|
48
|
+
? padding.top + chartH / 2
|
|
49
|
+
: padding.top + ((dMax - v) / valRange) * chartH;
|
|
50
|
+
|
|
51
|
+
const fm = font.getMetrics();
|
|
52
|
+
return y - (fm.ascent + fm.descent) / 2;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const text = useDerivedValue(() => {
|
|
56
|
+
const s = engine.series.value;
|
|
57
|
+
if (index >= s.length) return "";
|
|
58
|
+
return s[index].label ?? s[index].id;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const opacity = useDerivedValue(() => {
|
|
62
|
+
const s = engine.series.value;
|
|
63
|
+
const op = engine.seriesOpacities.value;
|
|
64
|
+
if (index >= s.length || index >= op.length) return 0;
|
|
65
|
+
return op[index] ?? 0;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Group opacity={opacity}>
|
|
70
|
+
<SkiaText x={labelX} y={labelY} text={text} font={font} color={color} />
|
|
71
|
+
</Group>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function MultiSeriesValueLabels({
|
|
76
|
+
engine,
|
|
77
|
+
padding,
|
|
78
|
+
colors,
|
|
79
|
+
font,
|
|
80
|
+
dotRadius,
|
|
81
|
+
}: {
|
|
82
|
+
engine: MultiEngineState;
|
|
83
|
+
padding: ChartPadding;
|
|
84
|
+
colors: string[];
|
|
85
|
+
font: SkFont;
|
|
86
|
+
dotRadius: number;
|
|
87
|
+
}) {
|
|
88
|
+
return (
|
|
89
|
+
<Group>
|
|
90
|
+
{Array.from({ length: MAX_MULTI_SERIES }, (_, i) => (
|
|
91
|
+
<SeriesValueLabelAtIndex
|
|
92
|
+
key={i}
|
|
93
|
+
index={i}
|
|
94
|
+
engine={engine}
|
|
95
|
+
padding={padding}
|
|
96
|
+
color={colors[i] ?? "#ffffff"}
|
|
97
|
+
font={font}
|
|
98
|
+
dotRadius={dotRadius}
|
|
99
|
+
/>
|
|
100
|
+
))}
|
|
101
|
+
</Group>
|
|
102
|
+
);
|
|
103
|
+
}
|