react-native-livechart 3.3.0 → 3.4.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/README.md +4 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts +10 -3
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ScrubActionOverlay.d.ts +32 -0
- package/dist/components/ScrubActionOverlay.d.ts.map +1 -0
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +23 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +110 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +14 -2
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +2 -0
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useReferenceLine.d.ts +36 -3
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLinePress.d.ts +23 -0
- package/dist/hooks/useReferenceLinePress.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/referenceLines.d.ts +23 -0
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/types.d.ts +131 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/LiveChart.tsx +165 -21
- package/src/components/LiveChartSeries.tsx +39 -2
- package/src/components/ReferenceLineOverlay.tsx +121 -94
- package/src/components/ScrubActionOverlay.tsx +187 -0
- package/src/components/XAxisOverlay.tsx +2 -1
- package/src/constants.ts +5 -0
- package/src/core/resolveConfig.ts +40 -0
- package/src/hooks/crosshairShared.ts +293 -0
- package/src/hooks/useCrosshair.ts +340 -6
- package/src/hooks/useMarkers.ts +18 -2
- package/src/hooks/useReferenceLine.ts +259 -14
- package/src/hooks/useReferenceLinePress.ts +92 -0
- package/src/index.ts +3 -0
- package/src/math/referenceLines.ts +55 -0
- package/src/types.ts +134 -6
|
@@ -12,23 +12,21 @@ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
|
12
12
|
import type { ChartPadding } from "../draw/line";
|
|
13
13
|
import { usePathBuilder } from "../hooks/usePathBuilder";
|
|
14
14
|
import { useReferenceLine } from "../hooks/useReferenceLine";
|
|
15
|
-
import {
|
|
16
|
-
import { referenceLineForm } from "../math/referenceLines";
|
|
15
|
+
import { referenceLineForm, resolveReferenceBadge } from "../math/referenceLines";
|
|
17
16
|
import type { LiveChartPalette, ReferenceLine } from "../types";
|
|
18
17
|
|
|
19
18
|
/** Translucent fill alpha for value / time bands. */
|
|
20
19
|
const BAND_FILL_OPACITY = 0.16;
|
|
21
20
|
|
|
22
|
-
/**
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const OFF_AXIS_PILL_RADIUS = 5;
|
|
21
|
+
/** Vertical padding inside the badge pill, in px (kept in sync with the layout). */
|
|
22
|
+
const BADGE_PILL_PAD_Y = 3;
|
|
23
|
+
const BADGE_PILL_RADIUS = 5;
|
|
26
24
|
|
|
27
25
|
/**
|
|
28
26
|
* Renders one reference line or band into the chart canvas. Handles all three
|
|
29
27
|
* `ReferenceLine` forms (horizontal line, horizontal value band, vertical time
|
|
30
|
-
* band) plus the
|
|
31
|
-
* so callers can `.map()` over a variable-length
|
|
28
|
+
* band) plus the Form-A pill badge (in-range tag + off-screen chevron pin).
|
|
29
|
+
* Self-contained so callers can `.map()` over a variable-length array.
|
|
32
30
|
*/
|
|
33
31
|
export function ReferenceLineOverlay({
|
|
34
32
|
engine,
|
|
@@ -37,6 +35,7 @@ export function ReferenceLineOverlay({
|
|
|
37
35
|
palette,
|
|
38
36
|
formatValue,
|
|
39
37
|
font,
|
|
38
|
+
badgeLayer = false,
|
|
40
39
|
}: {
|
|
41
40
|
engine: ChartEngineLayout;
|
|
42
41
|
padding: ChartPadding;
|
|
@@ -44,6 +43,13 @@ export function ReferenceLineOverlay({
|
|
|
44
43
|
palette: LiveChartPalette;
|
|
45
44
|
formatValue: (v: number) => string;
|
|
46
45
|
font: SkFont;
|
|
46
|
+
/**
|
|
47
|
+
* Render only the badge + label (`true`) or only the lines / bands (`false`,
|
|
48
|
+
* default). The caller draws the base pass behind the chart content and the
|
|
49
|
+
* badge pass **above** the left-edge fade, so badges/labels stay crisp instead
|
|
50
|
+
* of being erased by the fade's `dstOut` blend.
|
|
51
|
+
*/
|
|
52
|
+
badgeLayer?: boolean;
|
|
47
53
|
}) {
|
|
48
54
|
const form = referenceLineForm(line);
|
|
49
55
|
const isBand = form === "value-band" || form === "time-band";
|
|
@@ -58,21 +64,24 @@ export function ReferenceLineOverlay({
|
|
|
58
64
|
const bandFillOpacity = line.fillOpacity ?? BAND_FILL_OPACITY;
|
|
59
65
|
const hasBandBorder = isBand && line.strokeWidth !== undefined;
|
|
60
66
|
|
|
61
|
-
//
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
67
|
+
// Resolved badge appearance (badge config → fallback flat fields → theme).
|
|
68
|
+
const badge = resolveReferenceBadge(line);
|
|
69
|
+
const badgeBackground = badge?.background ?? palette.tooltipBg;
|
|
70
|
+
const badgeBorderColor = badge?.borderColor ?? color;
|
|
71
|
+
const badgeRadius = badge?.radius ?? BADGE_PILL_RADIUS;
|
|
65
72
|
|
|
66
73
|
const lineBuilder = usePathBuilder();
|
|
67
74
|
const bandBuilder = usePathBuilder();
|
|
68
75
|
const borderBuilder = usePathBuilder();
|
|
69
|
-
const
|
|
76
|
+
const connBuilder = usePathBuilder();
|
|
70
77
|
const chevBuilder = usePathBuilder();
|
|
71
78
|
|
|
72
79
|
const linePath = useDerivedValue(() => {
|
|
73
80
|
const b = lineBuilder.value;
|
|
74
81
|
const l = layout.get();
|
|
75
|
-
|
|
82
|
+
// A plain full-width line. A badge instead draws a pill + a connector to the
|
|
83
|
+
// opposite edge, below.
|
|
84
|
+
if (l.visible && !l.badge && !isBand) {
|
|
76
85
|
b.moveTo(l.x1, l.y);
|
|
77
86
|
b.lineTo(l.x2, l.y);
|
|
78
87
|
}
|
|
@@ -113,19 +122,13 @@ export function ReferenceLineOverlay({
|
|
|
113
122
|
return b.detach();
|
|
114
123
|
});
|
|
115
124
|
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
// Badge connector — the dashed line from the pill out to the opposite edge.
|
|
126
|
+
const connPath = useDerivedValue(() => {
|
|
127
|
+
const b = connBuilder.value;
|
|
118
128
|
const l = layout.get();
|
|
119
|
-
if (l.visible && l.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const pillRight =
|
|
123
|
-
l.labelX + measureFontTextWidth(font, l.label) + OFF_AXIS_PILL_PAD_X;
|
|
124
|
-
const start = pillRight + 4;
|
|
125
|
-
if (start < l.x2) {
|
|
126
|
-
b.moveTo(start, l.y);
|
|
127
|
-
b.lineTo(l.x2, l.y);
|
|
128
|
-
}
|
|
129
|
+
if (l.visible && l.badge && l.connStart >= 0) {
|
|
130
|
+
b.moveTo(l.connStart, l.y);
|
|
131
|
+
b.lineTo(l.connEnd, l.y);
|
|
129
132
|
}
|
|
130
133
|
return b.detach();
|
|
131
134
|
});
|
|
@@ -133,8 +136,8 @@ export function ReferenceLineOverlay({
|
|
|
133
136
|
const chevronPath = useDerivedValue(() => {
|
|
134
137
|
const b = chevBuilder.value;
|
|
135
138
|
const l = layout.get();
|
|
136
|
-
if (l.visible && l.offAxis) {
|
|
137
|
-
const cx = l.
|
|
139
|
+
if (l.visible && l.offAxis && l.chevronCx >= 0) {
|
|
140
|
+
const cx = l.chevronCx;
|
|
138
141
|
const cy = l.y;
|
|
139
142
|
const s = 4;
|
|
140
143
|
if (l.chevronUp) {
|
|
@@ -150,55 +153,64 @@ export function ReferenceLineOverlay({
|
|
|
150
153
|
return b.detach();
|
|
151
154
|
});
|
|
152
155
|
|
|
153
|
-
const lineOpacity = useDerivedValue(() =>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
const lineOpacity = useDerivedValue(() => {
|
|
157
|
+
const l = layout.get();
|
|
158
|
+
return l.visible && !l.badge && !isBand ? 1 : 0;
|
|
159
|
+
});
|
|
156
160
|
const bandOpacity = useDerivedValue(() =>
|
|
157
161
|
layout.get().visible && isBand ? bandFillOpacity : 0,
|
|
158
162
|
);
|
|
159
163
|
const bandBorderOpacity = useDerivedValue(() =>
|
|
160
164
|
layout.get().visible && hasBandBorder ? 1 : 0,
|
|
161
165
|
);
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
const badgeOpacity = useDerivedValue(() => {
|
|
167
|
+
const l = layout.get();
|
|
168
|
+
return l.visible && l.badge ? 1 : 0;
|
|
169
|
+
});
|
|
170
|
+
// Text + icon ride in the badge pass too, so they stay crisp above the fade.
|
|
171
|
+
const labelOpacity = useDerivedValue(() => {
|
|
172
|
+
const l = layout.get();
|
|
173
|
+
return l.visible && l.label.length > 0 ? 1 : 0;
|
|
174
|
+
});
|
|
175
|
+
const iconOpacity = useDerivedValue(() => {
|
|
176
|
+
const l = layout.get();
|
|
177
|
+
return l.visible && l.icon.length > 0 ? 1 : 0;
|
|
178
|
+
});
|
|
166
179
|
|
|
167
180
|
const labelX = useDerivedValue(() => layout.get().labelX);
|
|
168
181
|
const labelY = useDerivedValue(() => layout.get().labelY);
|
|
169
182
|
const labelText = useDerivedValue(() => layout.get().label);
|
|
183
|
+
const iconX = useDerivedValue(() => layout.get().iconX);
|
|
184
|
+
const iconText = useDerivedValue(() => layout.get().icon);
|
|
170
185
|
|
|
171
186
|
// Font metrics depend only on the (stable) font, so read them once instead of
|
|
172
|
-
// on every frame inside the pill
|
|
173
|
-
// JSI). The off-axis pill's ascent offset and height are then plain constants.
|
|
187
|
+
// on every frame inside the pill worklet (`getMetrics` allocates + crosses JSI).
|
|
174
188
|
const { ascent: fontAscent, height: pillH } = (() => {
|
|
175
189
|
const fm = font.getMetrics();
|
|
176
190
|
return {
|
|
177
191
|
ascent: fm.ascent,
|
|
178
|
-
height: fm.descent - fm.ascent +
|
|
192
|
+
height: fm.descent - fm.ascent + BADGE_PILL_PAD_Y * 2,
|
|
179
193
|
};
|
|
180
194
|
})();
|
|
181
195
|
|
|
182
|
-
//
|
|
183
|
-
const pillX = useDerivedValue(() => layout.get().
|
|
196
|
+
// Pill rect — position/size from the layout; vertically centered on the line.
|
|
197
|
+
const pillX = useDerivedValue(() => layout.get().pillX);
|
|
198
|
+
const pillW = useDerivedValue(() => layout.get().pillW);
|
|
184
199
|
const pillY = useDerivedValue(
|
|
185
|
-
() => layout.get().labelY + fontAscent -
|
|
200
|
+
() => layout.get().labelY + fontAscent - BADGE_PILL_PAD_Y,
|
|
186
201
|
);
|
|
187
|
-
const pillW = useDerivedValue(() => {
|
|
188
|
-
const l = layout.get();
|
|
189
|
-
const textW = measureFontTextWidth(font, l.label);
|
|
190
|
-
return l.labelX + textW + OFF_AXIS_PILL_PAD_X - (l.x1 + 2);
|
|
191
|
-
});
|
|
192
202
|
|
|
193
203
|
return (
|
|
194
204
|
<Group>
|
|
195
|
-
{
|
|
205
|
+
{/* Base pass — bands + lines, drawn behind the chart content (and faded at
|
|
206
|
+
the left edge like the rest of the content). */}
|
|
207
|
+
{!badgeLayer && isBand && (
|
|
196
208
|
<Group opacity={bandOpacity}>
|
|
197
209
|
<Path path={bandPath} style="fill" color={color} />
|
|
198
210
|
</Group>
|
|
199
211
|
)}
|
|
200
212
|
|
|
201
|
-
{hasBandBorder && (
|
|
213
|
+
{!badgeLayer && hasBandBorder && (
|
|
202
214
|
<Group opacity={bandBorderOpacity}>
|
|
203
215
|
<Path
|
|
204
216
|
path={bandBorderPath}
|
|
@@ -211,7 +223,7 @@ export function ReferenceLineOverlay({
|
|
|
211
223
|
</Group>
|
|
212
224
|
)}
|
|
213
225
|
|
|
214
|
-
{!isBand && (
|
|
226
|
+
{!badgeLayer && !isBand && (
|
|
215
227
|
<Group opacity={lineOpacity}>
|
|
216
228
|
<Path
|
|
217
229
|
path={linePath}
|
|
@@ -224,52 +236,67 @@ export function ReferenceLineOverlay({
|
|
|
224
236
|
</Group>
|
|
225
237
|
)}
|
|
226
238
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
239
|
+
{/* Badge pass — connector + pill + chevron + icon, above the left-edge fade. */}
|
|
240
|
+
{badgeLayer && (
|
|
241
|
+
<Group opacity={badgeOpacity}>
|
|
242
|
+
<Path
|
|
243
|
+
path={connPath}
|
|
244
|
+
style="stroke"
|
|
245
|
+
strokeWidth={strokeWidth}
|
|
246
|
+
color={color}
|
|
247
|
+
>
|
|
248
|
+
<DashPathEffect intervals={intervals} />
|
|
249
|
+
</Path>
|
|
250
|
+
<RoundedRect
|
|
251
|
+
x={pillX}
|
|
252
|
+
y={pillY}
|
|
253
|
+
width={pillW}
|
|
254
|
+
height={pillH}
|
|
255
|
+
r={badgeRadius}
|
|
256
|
+
color={badgeBackground}
|
|
257
|
+
/>
|
|
258
|
+
<RoundedRect
|
|
259
|
+
x={pillX}
|
|
260
|
+
y={pillY}
|
|
261
|
+
width={pillW}
|
|
262
|
+
height={pillH}
|
|
263
|
+
r={badgeRadius}
|
|
264
|
+
color={badgeBorderColor}
|
|
265
|
+
style="stroke"
|
|
266
|
+
strokeWidth={1}
|
|
267
|
+
/>
|
|
268
|
+
<Path
|
|
269
|
+
path={chevronPath}
|
|
270
|
+
style="stroke"
|
|
271
|
+
strokeWidth={1.5}
|
|
272
|
+
color={color}
|
|
273
|
+
strokeCap="round"
|
|
274
|
+
strokeJoin="round"
|
|
275
|
+
/>
|
|
276
|
+
<Group opacity={iconOpacity}>
|
|
277
|
+
<SkiaText
|
|
278
|
+
x={iconX}
|
|
279
|
+
y={labelY}
|
|
280
|
+
text={iconText}
|
|
281
|
+
font={font}
|
|
282
|
+
color={labelColor}
|
|
283
|
+
/>
|
|
284
|
+
</Group>
|
|
285
|
+
</Group>
|
|
286
|
+
)}
|
|
263
287
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
288
|
+
{/* Labels ride in the badge pass too, so they stay crisp above the fade. */}
|
|
289
|
+
{badgeLayer && (
|
|
290
|
+
<Group opacity={labelOpacity}>
|
|
291
|
+
<SkiaText
|
|
292
|
+
x={labelX}
|
|
293
|
+
y={labelY}
|
|
294
|
+
text={labelText}
|
|
295
|
+
font={font}
|
|
296
|
+
color={labelColor}
|
|
297
|
+
/>
|
|
298
|
+
</Group>
|
|
299
|
+
)}
|
|
273
300
|
</Group>
|
|
274
301
|
);
|
|
275
302
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Circle,
|
|
3
|
+
DashPathEffect,
|
|
4
|
+
Group,
|
|
5
|
+
Line,
|
|
6
|
+
RoundedRect,
|
|
7
|
+
Text as SkiaText,
|
|
8
|
+
type SkFont,
|
|
9
|
+
} from "@shopify/react-native-skia";
|
|
10
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
11
|
+
|
|
12
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
13
|
+
import type { ChartPadding } from "../draw/line";
|
|
14
|
+
import type {
|
|
15
|
+
ActionBadgeLayout,
|
|
16
|
+
TimeBadgeLayout,
|
|
17
|
+
} from "../hooks/crosshairShared";
|
|
18
|
+
import type { LiveChartPalette } from "../types";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Scrub-action ("order ticket") overlay: the locked reticle (a horizontal price
|
|
22
|
+
* level line + a faint vertical line crossing at the reticle) and the right-gutter
|
|
23
|
+
* action badge — a circular icon button + a separate price pill. All keyed off the
|
|
24
|
+
* lock SharedValues, so it tracks the reticle on the UI thread without re-renders.
|
|
25
|
+
*
|
|
26
|
+
* Mounted by `LiveChart` **outside** the degen-shake group so the rendered badge
|
|
27
|
+
* stays aligned with the (untransformed) tap hit-test in {@link useCrosshair}.
|
|
28
|
+
* Visibility rides on a 0/1 opacity driven by `lockActive` (no React remount).
|
|
29
|
+
*/
|
|
30
|
+
export function ScrubActionOverlay({
|
|
31
|
+
lockActive,
|
|
32
|
+
lockX,
|
|
33
|
+
lockY,
|
|
34
|
+
actionBadge,
|
|
35
|
+
timeBadge,
|
|
36
|
+
engine,
|
|
37
|
+
padding,
|
|
38
|
+
palette,
|
|
39
|
+
font,
|
|
40
|
+
icon,
|
|
41
|
+
lineColor,
|
|
42
|
+
background,
|
|
43
|
+
iconColor,
|
|
44
|
+
}: {
|
|
45
|
+
lockActive: SharedValue<boolean>;
|
|
46
|
+
lockX: SharedValue<number>;
|
|
47
|
+
lockY: SharedValue<number>;
|
|
48
|
+
actionBadge: SharedValue<ActionBadgeLayout>;
|
|
49
|
+
timeBadge?: SharedValue<TimeBadgeLayout>;
|
|
50
|
+
engine: ChartEngineLayout;
|
|
51
|
+
padding: ChartPadding;
|
|
52
|
+
palette: LiveChartPalette;
|
|
53
|
+
font: SkFont;
|
|
54
|
+
icon: string;
|
|
55
|
+
lineColor?: string;
|
|
56
|
+
background?: string;
|
|
57
|
+
iconColor?: string;
|
|
58
|
+
}) {
|
|
59
|
+
const opacity = useDerivedValue(() => (lockActive.value ? 1 : 0), [lockActive]);
|
|
60
|
+
|
|
61
|
+
// Horizontal level line (the chosen price), full plot width.
|
|
62
|
+
const hLeft = useDerivedValue(
|
|
63
|
+
() => ({ x: padding.left, y: lockY.value }),
|
|
64
|
+
[padding.left, lockY],
|
|
65
|
+
);
|
|
66
|
+
const hRight = useDerivedValue(
|
|
67
|
+
() => ({ x: engine.canvasWidth.value - padding.right, y: lockY.value }),
|
|
68
|
+
[engine.canvasWidth, padding.right, lockY],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Faint vertical line at the reticle X, full plot height.
|
|
72
|
+
const vTop = useDerivedValue(
|
|
73
|
+
() => ({ x: lockX.value, y: padding.top }),
|
|
74
|
+
[lockX, padding.top],
|
|
75
|
+
);
|
|
76
|
+
const vBottom = useDerivedValue(
|
|
77
|
+
() => ({ x: lockX.value, y: engine.canvasHeight.value - padding.bottom }),
|
|
78
|
+
[lockX, engine.canvasHeight, padding.bottom],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Action badge — a circular icon button + a separate price pill (2px gap).
|
|
82
|
+
// Center the icon by its *visual* bounds (not advance box) so a symbol like
|
|
83
|
+
// "+" sits dead-center in the circle. The glyph bounds are constant, so measure
|
|
84
|
+
// once and offset the per-frame circle center.
|
|
85
|
+
const iconBounds = icon ? font.measureText(icon) : null;
|
|
86
|
+
const iconOffX = iconBounds ? -iconBounds.x - iconBounds.width / 2 : 0;
|
|
87
|
+
const iconOffY = iconBounds ? -iconBounds.y - iconBounds.height / 2 : 0;
|
|
88
|
+
|
|
89
|
+
const iconOpacity = useDerivedValue(() =>
|
|
90
|
+
actionBadge.value.hasIcon ? 1 : 0,
|
|
91
|
+
);
|
|
92
|
+
const iconCx = useDerivedValue(() => actionBadge.value.iconCx);
|
|
93
|
+
const iconCy = useDerivedValue(() => actionBadge.value.iconCy);
|
|
94
|
+
const iconR = useDerivedValue(() => actionBadge.value.iconR);
|
|
95
|
+
const iconGlyphX = useDerivedValue(() => actionBadge.value.iconCx + iconOffX);
|
|
96
|
+
const iconGlyphY = useDerivedValue(() => actionBadge.value.iconCy + iconOffY);
|
|
97
|
+
|
|
98
|
+
const priceOpacity = useDerivedValue(() =>
|
|
99
|
+
actionBadge.value.hasPrice ? 1 : 0,
|
|
100
|
+
);
|
|
101
|
+
const priceX = useDerivedValue(() => actionBadge.value.priceX);
|
|
102
|
+
const priceY = useDerivedValue(() => actionBadge.value.priceY);
|
|
103
|
+
const priceW = useDerivedValue(() => actionBadge.value.priceW);
|
|
104
|
+
const priceH = useDerivedValue(() => actionBadge.value.priceH);
|
|
105
|
+
const priceR = useDerivedValue(() => actionBadge.value.priceH / 2);
|
|
106
|
+
const priceTextX = useDerivedValue(() => actionBadge.value.priceTextX);
|
|
107
|
+
const priceText = useDerivedValue(() => actionBadge.value.priceText);
|
|
108
|
+
|
|
109
|
+
const textY = useDerivedValue(() => actionBadge.value.textY);
|
|
110
|
+
|
|
111
|
+
// X-axis time pill (opt-in). `timeBadge` may be omitted; default to hidden.
|
|
112
|
+
const tb = timeBadge;
|
|
113
|
+
const timeOpacity = useDerivedValue(() => (tb?.value.visible ? 1 : 0), [tb]);
|
|
114
|
+
const timeX = useDerivedValue(() => tb?.value.x ?? -400, [tb]);
|
|
115
|
+
const timeY = useDerivedValue(() => tb?.value.y ?? 0, [tb]);
|
|
116
|
+
const timeW = useDerivedValue(() => tb?.value.w ?? 0, [tb]);
|
|
117
|
+
const timeH = useDerivedValue(() => tb?.value.h ?? 0, [tb]);
|
|
118
|
+
const timeR = useDerivedValue(() => (tb?.value.h ?? 0) / 2, [tb]);
|
|
119
|
+
const timeTextX = useDerivedValue(() => tb?.value.textX ?? -400, [tb]);
|
|
120
|
+
const timeTextY = useDerivedValue(() => tb?.value.textY ?? 0, [tb]);
|
|
121
|
+
const timeText = useDerivedValue(() => tb?.value.timeText ?? "", [tb]);
|
|
122
|
+
|
|
123
|
+
const levelColor = lineColor ?? palette.crosshairLine;
|
|
124
|
+
const pillColor = background ?? palette.badgeBg;
|
|
125
|
+
const labelColor = iconColor ?? palette.badgeText;
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<Group opacity={opacity}>
|
|
129
|
+
<Line p1={hLeft} p2={hRight} color={levelColor} strokeWidth={1}>
|
|
130
|
+
<DashPathEffect intervals={[4, 4]} />
|
|
131
|
+
</Line>
|
|
132
|
+
<Line p1={vTop} p2={vBottom} color={levelColor} strokeWidth={1}>
|
|
133
|
+
<DashPathEffect intervals={[2, 4]} />
|
|
134
|
+
</Line>
|
|
135
|
+
|
|
136
|
+
{/* Circular icon button (the action). */}
|
|
137
|
+
<Group opacity={iconOpacity}>
|
|
138
|
+
<Circle cx={iconCx} cy={iconCy} r={iconR} color={pillColor} />
|
|
139
|
+
<SkiaText
|
|
140
|
+
x={iconGlyphX}
|
|
141
|
+
y={iconGlyphY}
|
|
142
|
+
text={icon}
|
|
143
|
+
font={font}
|
|
144
|
+
color={labelColor}
|
|
145
|
+
/>
|
|
146
|
+
</Group>
|
|
147
|
+
|
|
148
|
+
{/* Price pill (capsule). */}
|
|
149
|
+
<Group opacity={priceOpacity}>
|
|
150
|
+
<RoundedRect
|
|
151
|
+
x={priceX}
|
|
152
|
+
y={priceY}
|
|
153
|
+
width={priceW}
|
|
154
|
+
height={priceH}
|
|
155
|
+
r={priceR}
|
|
156
|
+
color={pillColor}
|
|
157
|
+
/>
|
|
158
|
+
<SkiaText
|
|
159
|
+
x={priceTextX}
|
|
160
|
+
y={textY}
|
|
161
|
+
text={priceText}
|
|
162
|
+
font={font}
|
|
163
|
+
color={labelColor}
|
|
164
|
+
/>
|
|
165
|
+
</Group>
|
|
166
|
+
|
|
167
|
+
{/* X-axis time pill (capsule) where the vertical line meets the axis. */}
|
|
168
|
+
<Group opacity={timeOpacity}>
|
|
169
|
+
<RoundedRect
|
|
170
|
+
x={timeX}
|
|
171
|
+
y={timeY}
|
|
172
|
+
width={timeW}
|
|
173
|
+
height={timeH}
|
|
174
|
+
r={timeR}
|
|
175
|
+
color={pillColor}
|
|
176
|
+
/>
|
|
177
|
+
<SkiaText
|
|
178
|
+
x={timeTextX}
|
|
179
|
+
y={timeTextY}
|
|
180
|
+
text={timeText}
|
|
181
|
+
font={font}
|
|
182
|
+
color={labelColor}
|
|
183
|
+
/>
|
|
184
|
+
</Group>
|
|
185
|
+
</Group>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Group, Path, type SkFont } from "@shopify/react-native-skia";
|
|
2
2
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
3
|
+
import { X_AXIS_LABEL_OFFSET_Y } from "../constants";
|
|
3
4
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
4
5
|
import type { ChartPadding } from "../draw/line";
|
|
5
6
|
import type { XAxisEntry } from "../hooks/useXAxis";
|
|
@@ -10,7 +11,7 @@ import { AnimatedLabel } from "./AnimatedLabel";
|
|
|
10
11
|
|
|
11
12
|
const MAX_X_LABELS = 10;
|
|
12
13
|
const TICK_HEIGHT = 5;
|
|
13
|
-
const LABEL_OFFSET_Y =
|
|
14
|
+
const LABEL_OFFSET_Y = X_AXIS_LABEL_OFFSET_Y;
|
|
14
15
|
|
|
15
16
|
export function XAxisOverlay({
|
|
16
17
|
entries,
|
package/src/constants.ts
CHANGED
|
@@ -62,6 +62,11 @@ export const BADGE_MARGIN_RIGHT = BADGE_METRICS_DEFAULTS.marginEdge;
|
|
|
62
62
|
/** Gap between the live dot and the badge tail tip. */
|
|
63
63
|
export const BADGE_DOT_GAP = BADGE_METRICS_DEFAULTS.dotGap;
|
|
64
64
|
|
|
65
|
+
/** Baseline offset (px) below the plot bottom where x-axis time labels sit.
|
|
66
|
+
* Shared so overlays that align to the time-axis row (e.g. the scrub-action
|
|
67
|
+
* time badge) match the {@link XAxisOverlay} labels. */
|
|
68
|
+
export const X_AXIS_LABEL_OFFSET_Y = 19;
|
|
69
|
+
|
|
65
70
|
/** Maximum simultaneous series rendered (paths/dots slots). */
|
|
66
71
|
export const MAX_MULTI_SERIES = 12;
|
|
67
72
|
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
MultiSeriesDotConfig,
|
|
18
18
|
PulseConfig,
|
|
19
19
|
ReferenceLine,
|
|
20
|
+
ScrubActionConfig,
|
|
20
21
|
ScrubConfig,
|
|
21
22
|
SelectionDotConfig,
|
|
22
23
|
SelectionDotProps,
|
|
@@ -90,6 +91,24 @@ export interface ResolvedScrubConfig {
|
|
|
90
91
|
panGestureDelay: number;
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
export interface ResolvedScrubActionConfig {
|
|
95
|
+
/** Glyph drawn in the action badge. */
|
|
96
|
+
icon: string;
|
|
97
|
+
/** undefined → palette.badgeBg */
|
|
98
|
+
background: string | undefined;
|
|
99
|
+
/** undefined → palette.badgeText */
|
|
100
|
+
iconColor: string | undefined;
|
|
101
|
+
/** undefined → palette.crosshairLine */
|
|
102
|
+
lineColor: string | undefined;
|
|
103
|
+
/** Show the price readout pill; false → icon-only badge. */
|
|
104
|
+
text: boolean;
|
|
105
|
+
/** Show the date/time pill where the vertical line meets the x-axis. */
|
|
106
|
+
timeBadge: boolean;
|
|
107
|
+
/** undefined → no rounding */
|
|
108
|
+
snap: number | undefined;
|
|
109
|
+
dismissOnTapOutside: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
export interface ResolvedGradientConfig {
|
|
94
113
|
/** undefined → use palette.fillTop (theme-aware) at render time */
|
|
95
114
|
topOpacity: number | undefined;
|
|
@@ -289,6 +308,27 @@ export function resolveScrub(
|
|
|
289
308
|
return resolveToggle(prop, SCRUB_DEFAULTS, false);
|
|
290
309
|
}
|
|
291
310
|
|
|
311
|
+
const SCRUB_ACTION_DEFAULTS: ResolvedScrubActionConfig = {
|
|
312
|
+
icon: "+",
|
|
313
|
+
background: undefined,
|
|
314
|
+
iconColor: undefined,
|
|
315
|
+
lineColor: undefined,
|
|
316
|
+
text: true,
|
|
317
|
+
timeBadge: false,
|
|
318
|
+
snap: undefined,
|
|
319
|
+
dismissOnTapOutside: false,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Resolves `scrubAction` prop to a fully-typed config or null (disabled).
|
|
324
|
+
* Opt-in: `false`/`undefined` → null; `true` → defaults; object → merged.
|
|
325
|
+
*/
|
|
326
|
+
export function resolveScrubAction(
|
|
327
|
+
prop: boolean | ScrubActionConfig | undefined,
|
|
328
|
+
): ResolvedScrubActionConfig | null {
|
|
329
|
+
return resolveToggle(prop, SCRUB_ACTION_DEFAULTS, false);
|
|
330
|
+
}
|
|
331
|
+
|
|
292
332
|
const GRADIENT_DEFAULTS: ResolvedGradientConfig = {
|
|
293
333
|
topOpacity: undefined,
|
|
294
334
|
bottomOpacity: undefined,
|