react-native-livechart 1.1.0 → 2.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/README.md +15 -4
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/LoadingOverlay.d.ts +6 -2
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts +8 -4
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +4 -2
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +16 -13
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +2 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts +2 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +18 -4
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +3 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +3 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/candle.d.ts +2 -2
- package/dist/draw/candle.d.ts.map +1 -1
- package/dist/draw/grid.d.ts +2 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/line.d.ts +8 -8
- package/dist/draw/line.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +44 -0
- package/dist/draw/markerAtlas.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +3 -1
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
- package/dist/hooks/useBadge.d.ts +2 -2
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +2 -2
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useYAxis.d.ts +2 -1
- package/dist/hooks/useYAxis.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +100 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairLine.tsx +23 -9
- package/src/components/CrosshairOverlay.tsx +23 -9
- package/src/components/LiveChart.tsx +14 -0
- package/src/components/LiveChartSeries.tsx +10 -1
- package/src/components/LoadingOverlay.tsx +20 -11
- package/src/components/MarkerOverlay.tsx +119 -164
- package/src/components/YAxisOverlay.tsx +8 -4
- package/src/constants.ts +54 -14
- package/src/core/liveChartEngineTick.ts +7 -2
- package/src/core/liveChartSeriesEngineTick.ts +8 -2
- package/src/core/resolveConfig.ts +88 -32
- package/src/core/useLiveChartEngine.ts +6 -0
- package/src/core/useLiveChartSeriesEngine.ts +6 -0
- package/src/draw/candle.ts +11 -6
- package/src/draw/grid.ts +5 -5
- package/src/draw/line.ts +106 -25
- package/src/draw/markerAtlas.ts +304 -0
- package/src/hooks/resolveChartLayout.ts +11 -2
- package/src/hooks/useBadge.ts +38 -24
- package/src/hooks/useCandlePaths.ts +4 -2
- package/src/hooks/useYAxis.ts +4 -1
- package/src/index.ts +7 -0
- package/src/types.ts +109 -5
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Skia,
|
|
3
|
+
PaintStyle,
|
|
4
|
+
StrokeCap,
|
|
5
|
+
StrokeJoin,
|
|
6
|
+
drawAsImageFromPicture,
|
|
7
|
+
type SkCanvas,
|
|
8
|
+
type SkFont,
|
|
9
|
+
type SkImage,
|
|
10
|
+
type SkPaint,
|
|
11
|
+
type SkRect,
|
|
12
|
+
} from "@shopify/react-native-skia";
|
|
13
|
+
import type { LiveChartPalette, Marker, MarkerKind } from "../types";
|
|
14
|
+
|
|
15
|
+
/** Default icon box (px) when `marker.size` is unset. */
|
|
16
|
+
export const DEFAULT_ICON_SIZE = 16;
|
|
17
|
+
/** Circular badge padding around the icon glyph + background ring width. */
|
|
18
|
+
const PILL_PAD = 2;
|
|
19
|
+
const PILL_BORDER = 2;
|
|
20
|
+
const PILL_TEXT_COLOR = "#ffffff";
|
|
21
|
+
/** Anti-alias breathing room baked around every cell so sprites don't clip. */
|
|
22
|
+
const CELL_MARGIN = 2;
|
|
23
|
+
|
|
24
|
+
/** Default glyph color per kind when `marker.color` is unset. */
|
|
25
|
+
export function defaultMarkerColor(
|
|
26
|
+
kind: MarkerKind,
|
|
27
|
+
palette: LiveChartPalette,
|
|
28
|
+
): string {
|
|
29
|
+
switch (kind) {
|
|
30
|
+
case "trade":
|
|
31
|
+
return palette.line;
|
|
32
|
+
case "boost":
|
|
33
|
+
return palette.refLine;
|
|
34
|
+
case "graduation":
|
|
35
|
+
return palette.dotUp;
|
|
36
|
+
case "winner":
|
|
37
|
+
return palette.dotUp;
|
|
38
|
+
case "clawback":
|
|
39
|
+
return palette.refLabel;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Axis-anchored kinds whose geometry depends on the chart's baseline (a
|
|
45
|
+
* variable-length stem, or a box pinned to the axis) and therefore cannot be a
|
|
46
|
+
* fixed-size sprite. These fall back to a self-projecting glyph component.
|
|
47
|
+
*
|
|
48
|
+
* Only applies when the marker has no `icon`/`image` override — those take
|
|
49
|
+
* precedence and render as ordinary centered stamps via the atlas.
|
|
50
|
+
*/
|
|
51
|
+
export function isConnectorMarker(m: Marker): boolean {
|
|
52
|
+
"worklet";
|
|
53
|
+
return (
|
|
54
|
+
!m.image &&
|
|
55
|
+
!m.icon &&
|
|
56
|
+
(m.kind === "graduation" || m.kind === "clawback")
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stable key for a marker's *visual appearance* (excludes position/anchor).
|
|
62
|
+
* Two markers with the same signature share one atlas cell. Called both on the
|
|
63
|
+
* JS thread (to build the atlas) and inside the per-frame worklet (to look a
|
|
64
|
+
* marker's cell up), so it must stay worklet-safe and primitive-only.
|
|
65
|
+
*/
|
|
66
|
+
export function markerAppearanceSig(m: Marker): string {
|
|
67
|
+
"worklet";
|
|
68
|
+
if (m.image) return `img\x1f${m.id}\x1f${m.size ?? DEFAULT_ICON_SIZE}`;
|
|
69
|
+
const size = m.size ?? "";
|
|
70
|
+
if (m.icon)
|
|
71
|
+
return `ic\x1f${m.icon}\x1f${m.pill ? 1 : 0}\x1f${m.color ?? ""}\x1f${size}`;
|
|
72
|
+
return `k\x1f${m.kind}\x1f${m.color ?? ""}\x1f${size}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One packed glyph in the atlas image: its source rect + box size. */
|
|
76
|
+
export interface AtlasCell {
|
|
77
|
+
rect: SkRect;
|
|
78
|
+
w: number;
|
|
79
|
+
h: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface MarkerAtlas {
|
|
83
|
+
/** Packed glyph texture, or null when there are no atlas-rendered markers. */
|
|
84
|
+
image: SkImage | null;
|
|
85
|
+
/** appearance-signature → cell. */
|
|
86
|
+
cells: Record<string, AtlasCell>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const EMPTY_ATLAS: MarkerAtlas = { image: null, cells: {} };
|
|
90
|
+
|
|
91
|
+
function fillPaint(color: string): SkPaint {
|
|
92
|
+
const p = Skia.Paint();
|
|
93
|
+
p.setAntiAlias(true);
|
|
94
|
+
p.setColor(Skia.Color(color));
|
|
95
|
+
p.setStyle(PaintStyle.Fill);
|
|
96
|
+
return p;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function strokePaint(color: string, width: number, round = false): SkPaint {
|
|
100
|
+
const p = Skia.Paint();
|
|
101
|
+
p.setAntiAlias(true);
|
|
102
|
+
p.setColor(Skia.Color(color));
|
|
103
|
+
p.setStyle(PaintStyle.Stroke);
|
|
104
|
+
p.setStrokeWidth(width);
|
|
105
|
+
if (round) {
|
|
106
|
+
p.setStrokeCap(StrokeCap.Round);
|
|
107
|
+
p.setStrokeJoin(StrokeJoin.Round);
|
|
108
|
+
}
|
|
109
|
+
return p;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface CellSpec {
|
|
113
|
+
w: number;
|
|
114
|
+
h: number;
|
|
115
|
+
/** Draw the glyph centered at (cx, cy) in the recording canvas. */
|
|
116
|
+
draw: (canvas: SkCanvas, cx: number, cy: number) => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Geometry + draw routine for one marker appearance. Mirrors the per-glyph
|
|
121
|
+
* rendering the old `MarkerGlyph` did inline, so the atlas is pixel-equivalent.
|
|
122
|
+
*/
|
|
123
|
+
function cellSpec(m: Marker, palette: LiveChartPalette, font: SkFont): CellSpec {
|
|
124
|
+
const color = m.color ?? defaultMarkerColor(m.kind, palette);
|
|
125
|
+
const m2 = CELL_MARGIN * 2;
|
|
126
|
+
|
|
127
|
+
if (m.image) {
|
|
128
|
+
const img = m.image;
|
|
129
|
+
const size = m.size ?? DEFAULT_ICON_SIZE;
|
|
130
|
+
const imgPaint = Skia.Paint();
|
|
131
|
+
imgPaint.setAntiAlias(true);
|
|
132
|
+
return {
|
|
133
|
+
w: size + m2,
|
|
134
|
+
h: size + m2,
|
|
135
|
+
draw: (canvas, cx, cy) => {
|
|
136
|
+
const iw = img.width();
|
|
137
|
+
const ih = img.height();
|
|
138
|
+
// `contain` fit: scale to fit the box without distorting aspect.
|
|
139
|
+
const scale = iw > 0 && ih > 0 ? Math.min(size / iw, size / ih) : 1;
|
|
140
|
+
const dw = iw * scale;
|
|
141
|
+
const dh = ih * scale;
|
|
142
|
+
canvas.drawImageRect(
|
|
143
|
+
img,
|
|
144
|
+
Skia.XYWHRect(0, 0, iw, ih),
|
|
145
|
+
Skia.XYWHRect(cx - dw / 2, cy - dh / 2, dw, dh),
|
|
146
|
+
imgPaint,
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (m.icon) {
|
|
153
|
+
const icon = m.icon;
|
|
154
|
+
const b = font.measureText(icon);
|
|
155
|
+
// Center the glyph on its measured bounds (tighter than ascent/descent).
|
|
156
|
+
const iconDX = b.x + b.width / 2;
|
|
157
|
+
const iconDY = b.y + b.height / 2;
|
|
158
|
+
|
|
159
|
+
if (m.pill) {
|
|
160
|
+
const bgColor = `rgb(${palette.bgRgb[0]}, ${palette.bgRgb[1]}, ${palette.bgRgb[2]})`;
|
|
161
|
+
const pillR = Math.max(b.width, b.height) / 2 + PILL_PAD;
|
|
162
|
+
const ringR = pillR + PILL_BORDER;
|
|
163
|
+
const box = Math.ceil(2 * ringR) + m2;
|
|
164
|
+
return {
|
|
165
|
+
w: box,
|
|
166
|
+
h: box,
|
|
167
|
+
draw: (canvas, cx, cy) => {
|
|
168
|
+
canvas.drawCircle(cx, cy, ringR, fillPaint(bgColor));
|
|
169
|
+
canvas.drawCircle(cx, cy, pillR, fillPaint(color));
|
|
170
|
+
canvas.drawText(
|
|
171
|
+
icon,
|
|
172
|
+
cx - iconDX,
|
|
173
|
+
cy - iconDY,
|
|
174
|
+
fillPaint(PILL_TEXT_COLOR),
|
|
175
|
+
font,
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
w: Math.ceil(Math.max(b.width, 1)) + m2,
|
|
183
|
+
h: Math.ceil(Math.max(b.height, 1)) + m2,
|
|
184
|
+
draw: (canvas, cx, cy) => {
|
|
185
|
+
canvas.drawText(icon, cx - iconDX, cy - iconDY, fillPaint(color), font);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (m.kind === "trade") {
|
|
191
|
+
const box = 2 * (5 + 1) + m2; // ring r=5 stroked w=2
|
|
192
|
+
return {
|
|
193
|
+
w: box,
|
|
194
|
+
h: box,
|
|
195
|
+
draw: (canvas, cx, cy) => {
|
|
196
|
+
canvas.drawCircle(cx, cy, 5, strokePaint(color, 2));
|
|
197
|
+
canvas.drawCircle(cx, cy, 2, fillPaint(color));
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (m.kind === "winner") {
|
|
203
|
+
const outer = 7;
|
|
204
|
+
const inner = 3;
|
|
205
|
+
const box = 2 * outer + m2;
|
|
206
|
+
return {
|
|
207
|
+
w: box,
|
|
208
|
+
h: box,
|
|
209
|
+
draw: (canvas, cx, cy) => {
|
|
210
|
+
const p = Skia.Path.Make();
|
|
211
|
+
for (let k = 0; k < 10; k++) {
|
|
212
|
+
const ang = -Math.PI / 2 + (k * Math.PI) / 5;
|
|
213
|
+
const rad = k % 2 === 0 ? outer : inner;
|
|
214
|
+
const px = cx + rad * Math.cos(ang);
|
|
215
|
+
const py = cy + rad * Math.sin(ang);
|
|
216
|
+
if (k === 0) p.moveTo(px, py);
|
|
217
|
+
else p.lineTo(px, py);
|
|
218
|
+
}
|
|
219
|
+
p.close();
|
|
220
|
+
canvas.drawPath(p, fillPaint(color));
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (m.kind === "boost") {
|
|
226
|
+
const L = 6;
|
|
227
|
+
const box = 2 * L + m2;
|
|
228
|
+
return {
|
|
229
|
+
w: box,
|
|
230
|
+
h: box,
|
|
231
|
+
draw: (canvas, cx, cy) => {
|
|
232
|
+
const p = Skia.Path.Make();
|
|
233
|
+
for (let k = 0; k < 4; k++) {
|
|
234
|
+
const ang = (k * Math.PI) / 4;
|
|
235
|
+
const dx = L * Math.cos(ang);
|
|
236
|
+
const dy = L * Math.sin(ang);
|
|
237
|
+
p.moveTo(cx - dx, cy - dy);
|
|
238
|
+
p.lineTo(cx + dx, cy + dy);
|
|
239
|
+
}
|
|
240
|
+
canvas.drawPath(p, strokePaint(color, 1.5, true));
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Connector kinds are handled outside the atlas; this is a defensive dot.
|
|
246
|
+
const box = 6 + m2;
|
|
247
|
+
return {
|
|
248
|
+
w: box,
|
|
249
|
+
h: box,
|
|
250
|
+
draw: (canvas, cx, cy) => canvas.drawCircle(cx, cy, 3, fillPaint(color)),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Rasterize every distinct marker appearance into a single packed atlas image,
|
|
256
|
+
* once per appearance-set change (NOT per frame). The per-frame worklet then
|
|
257
|
+
* blits these cells via one `drawAtlas` call.
|
|
258
|
+
*
|
|
259
|
+
* Connector kinds (see `isConnectorMarker`) are skipped — they render via a
|
|
260
|
+
* self-projecting fallback component.
|
|
261
|
+
*/
|
|
262
|
+
export function buildMarkerAtlas(
|
|
263
|
+
markers: Marker[],
|
|
264
|
+
palette: LiveChartPalette,
|
|
265
|
+
font: SkFont,
|
|
266
|
+
): MarkerAtlas {
|
|
267
|
+
const seen = new Set<string>();
|
|
268
|
+
const specs: { sig: string; spec: CellSpec }[] = [];
|
|
269
|
+
for (let i = 0; i < markers.length; i++) {
|
|
270
|
+
const m = markers[i];
|
|
271
|
+
if (isConnectorMarker(m)) continue;
|
|
272
|
+
const sig = markerAppearanceSig(m);
|
|
273
|
+
if (seen.has(sig)) continue;
|
|
274
|
+
seen.add(sig);
|
|
275
|
+
specs.push({ sig, spec: cellSpec(m, palette, font) });
|
|
276
|
+
}
|
|
277
|
+
if (specs.length === 0) return EMPTY_ATLAS;
|
|
278
|
+
|
|
279
|
+
let totalW = 0;
|
|
280
|
+
let maxH = 0;
|
|
281
|
+
for (let i = 0; i < specs.length; i++) {
|
|
282
|
+
totalW += specs[i].spec.w;
|
|
283
|
+
if (specs[i].spec.h > maxH) maxH = specs[i].spec.h;
|
|
284
|
+
}
|
|
285
|
+
const W = Math.max(1, Math.ceil(totalW));
|
|
286
|
+
const H = Math.max(1, Math.ceil(maxH));
|
|
287
|
+
|
|
288
|
+
const recorder = Skia.PictureRecorder();
|
|
289
|
+
const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, W, H));
|
|
290
|
+
const cells: Record<string, AtlasCell> = {};
|
|
291
|
+
let x = 0;
|
|
292
|
+
for (let i = 0; i < specs.length; i++) {
|
|
293
|
+
const { sig, spec } = specs[i];
|
|
294
|
+
canvas.save();
|
|
295
|
+
canvas.translate(x, 0);
|
|
296
|
+
spec.draw(canvas, spec.w / 2, H / 2);
|
|
297
|
+
canvas.restore();
|
|
298
|
+
cells[sig] = { rect: Skia.XYWHRect(x, 0, spec.w, H), w: spec.w, h: H };
|
|
299
|
+
x += spec.w;
|
|
300
|
+
}
|
|
301
|
+
const picture = recorder.finishRecordingAsPicture();
|
|
302
|
+
const image = drawAsImageFromPicture(picture, { width: W, height: H });
|
|
303
|
+
return { image, cells };
|
|
304
|
+
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
resolvePadding,
|
|
10
10
|
type ChartPadding,
|
|
11
11
|
} from "../draw/line";
|
|
12
|
-
import type { ChartInsets, LiveChartPalette } from "../types";
|
|
12
|
+
import type { BadgeMetrics, ChartInsets, LiveChartPalette } from "../types";
|
|
13
13
|
|
|
14
14
|
export interface ChartLayoutConfig {
|
|
15
15
|
palette: LiveChartPalette;
|
|
@@ -17,6 +17,8 @@ export interface ChartLayoutConfig {
|
|
|
17
17
|
insetsOverride?: ChartInsets;
|
|
18
18
|
yAxis: boolean;
|
|
19
19
|
badge: boolean;
|
|
20
|
+
/** Badge pill geometry tokens. Omit for built-in defaults. */
|
|
21
|
+
badgeMetrics?: BadgeMetrics;
|
|
20
22
|
/**
|
|
21
23
|
* When true (default if omitted and `badge` is true), reserve the wide right gutter for the badge.
|
|
22
24
|
* Set false when the badge is anchored left of the live dot (`position: "left"`).
|
|
@@ -79,6 +81,7 @@ export function resolveChartLayout(
|
|
|
79
81
|
config.font.getSize(),
|
|
80
82
|
measuredYAxisLabelWidth,
|
|
81
83
|
showTail,
|
|
84
|
+
config.badgeMetrics,
|
|
82
85
|
)
|
|
83
86
|
: config.multiSeriesValueLabel && config.yAxis
|
|
84
87
|
? Math.max(
|
|
@@ -91,7 +94,12 @@ export function resolveChartLayout(
|
|
|
91
94
|
? Math.max(measuredYAxisLabelWidth + 16 + dotR * 2, 44)
|
|
92
95
|
: resolveAutoRight(false, false);
|
|
93
96
|
} else {
|
|
94
|
-
rightPad = resolveAutoRight(
|
|
97
|
+
rightPad = resolveAutoRight(
|
|
98
|
+
config.yAxis,
|
|
99
|
+
badgeUsesRightGutter,
|
|
100
|
+
showTail,
|
|
101
|
+
config.badgeMetrics,
|
|
102
|
+
);
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
if (config.pulse && config.yAxis && config.insetsOverride?.right == null) {
|
|
@@ -121,6 +129,7 @@ export function resolveChartLayout(
|
|
|
121
129
|
false,
|
|
122
130
|
xAxis,
|
|
123
131
|
showTail,
|
|
132
|
+
config.badgeMetrics,
|
|
124
133
|
);
|
|
125
134
|
|
|
126
135
|
let padding: ChartPadding = { ...base, right: rightPad, left: leftPad };
|
package/src/hooks/useBadge.ts
CHANGED
|
@@ -6,11 +6,8 @@ import {
|
|
|
6
6
|
type SharedValue,
|
|
7
7
|
} from "react-native-reanimated";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
BADGE_PILL_PAD_X,
|
|
12
|
-
BADGE_PILL_PAD_Y,
|
|
13
|
-
BADGE_TAIL_LEN,
|
|
9
|
+
BADGE_METRICS_DEFAULTS,
|
|
10
|
+
MOTION_METRICS_DEFAULTS,
|
|
14
11
|
MS_PER_FRAME_60FPS,
|
|
15
12
|
} from "../constants";
|
|
16
13
|
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
@@ -22,9 +19,12 @@ import {
|
|
|
22
19
|
} from "../draw/line";
|
|
23
20
|
import { hexToRgb, lerpColor } from "../math/color";
|
|
24
21
|
import { lerp } from "../math/lerp";
|
|
25
|
-
import type {
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
import type {
|
|
23
|
+
BadgeMetrics,
|
|
24
|
+
BadgeVariant,
|
|
25
|
+
LiveChartPalette,
|
|
26
|
+
Momentum,
|
|
27
|
+
} from "../types";
|
|
28
28
|
|
|
29
29
|
export function useBadge(
|
|
30
30
|
engine: ChartEngineWithLiveValue,
|
|
@@ -37,6 +37,8 @@ export function useBadge(
|
|
|
37
37
|
momentum?: SharedValue<Momentum>,
|
|
38
38
|
position: "right" | "left" = "right",
|
|
39
39
|
background?: string,
|
|
40
|
+
badgeMetrics: BadgeMetrics = BADGE_METRICS_DEFAULTS,
|
|
41
|
+
badgeColorSpeed: number = MOTION_METRICS_DEFAULTS.badgeColorSpeed,
|
|
40
42
|
) {
|
|
41
43
|
const colorR = useSharedValue(0);
|
|
42
44
|
const colorG = useSharedValue(0);
|
|
@@ -93,7 +95,7 @@ export function useBadge(
|
|
|
93
95
|
const text = formatValue(engine.displayValue.get());
|
|
94
96
|
const textW = measureFontTextWidth(font, text);
|
|
95
97
|
|
|
96
|
-
const pillH = font.getSize() +
|
|
98
|
+
const pillH = font.getSize() + badgeMetrics.padY * 2;
|
|
97
99
|
const r = pillH / 2;
|
|
98
100
|
const badgeY = dotY - pillH / 2;
|
|
99
101
|
let textX: number;
|
|
@@ -101,9 +103,9 @@ export function useBadge(
|
|
|
101
103
|
if (position === "left") {
|
|
102
104
|
// Pill to the left of the live dot; no tail (`showTail` applies to right gutter only).
|
|
103
105
|
const dotXPos = w - padding.right;
|
|
104
|
-
const pillW = 2 *
|
|
105
|
-
const bodyRight = dotXPos -
|
|
106
|
-
const bodyLeft = Math.max(
|
|
106
|
+
const pillW = 2 * badgeMetrics.padX + textW;
|
|
107
|
+
const bodyRight = dotXPos - badgeMetrics.dotGap;
|
|
108
|
+
const bodyLeft = Math.max(badgeMetrics.marginEdge, bodyRight - pillW);
|
|
107
109
|
const pillBodyW = bodyRight - bodyLeft;
|
|
108
110
|
textX = (bodyLeft + bodyRight - textW) / 2;
|
|
109
111
|
path.addRRect({
|
|
@@ -113,15 +115,21 @@ export function useBadge(
|
|
|
113
115
|
});
|
|
114
116
|
} else {
|
|
115
117
|
// Right-gutter badge (default): asymmetric layout with optional tail.
|
|
116
|
-
const tl = badgeTailAndCap(font.getSize(), showTail);
|
|
117
|
-
const bodyLeft = w - padding.right +
|
|
118
|
-
const bodyRight = w -
|
|
118
|
+
const tl = badgeTailAndCap(font.getSize(), showTail, badgeMetrics);
|
|
119
|
+
const bodyLeft = w - padding.right + badgeMetrics.dotGap + tl;
|
|
120
|
+
const bodyRight = w - badgeMetrics.marginEdge;
|
|
119
121
|
const pillW = bodyRight - bodyLeft;
|
|
120
122
|
// Text centered in pill body — same formula used by GridOverlay.
|
|
121
|
-
textX = pillTextLeftX(
|
|
123
|
+
textX = pillTextLeftX(
|
|
124
|
+
w,
|
|
125
|
+
padding.right,
|
|
126
|
+
badgeMetrics.dotGap + tl,
|
|
127
|
+
textW,
|
|
128
|
+
badgeMetrics,
|
|
129
|
+
);
|
|
122
130
|
|
|
123
131
|
if (showTail) {
|
|
124
|
-
const badgeX = w - padding.right +
|
|
132
|
+
const badgeX = w - padding.right + badgeMetrics.dotGap;
|
|
125
133
|
const cx = tl + pillW - r;
|
|
126
134
|
|
|
127
135
|
path.moveTo(badgeX + tl, badgeY);
|
|
@@ -134,17 +142,17 @@ export function useBadge(
|
|
|
134
142
|
);
|
|
135
143
|
path.lineTo(badgeX + tl, badgeY + pillH);
|
|
136
144
|
path.cubicTo(
|
|
137
|
-
badgeX +
|
|
145
|
+
badgeX + badgeMetrics.tailLength + 2,
|
|
138
146
|
badgeY + pillH,
|
|
139
147
|
badgeX + 3,
|
|
140
|
-
badgeY + r +
|
|
148
|
+
badgeY + r + badgeMetrics.tailSpread,
|
|
141
149
|
badgeX,
|
|
142
150
|
badgeY + r,
|
|
143
151
|
);
|
|
144
152
|
path.cubicTo(
|
|
145
153
|
badgeX + 3,
|
|
146
|
-
badgeY + r -
|
|
147
|
-
badgeX +
|
|
154
|
+
badgeY + r - badgeMetrics.tailSpread,
|
|
155
|
+
badgeX + badgeMetrics.tailLength + 2,
|
|
148
156
|
badgeY,
|
|
149
157
|
badgeX + tl,
|
|
150
158
|
badgeY,
|
|
@@ -170,9 +178,15 @@ export function useBadge(
|
|
|
170
178
|
} else if (momentum) {
|
|
171
179
|
const m = momentum.get();
|
|
172
180
|
const targetRgb = m === "up" ? upRgb : m === "down" ? downRgb : accentRgb;
|
|
173
|
-
colorR.set(
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
colorR.set(
|
|
182
|
+
lerp(colorR.get(), targetRgb[0], badgeColorSpeed, MS_PER_FRAME_60FPS),
|
|
183
|
+
);
|
|
184
|
+
colorG.set(
|
|
185
|
+
lerp(colorG.get(), targetRgb[1], badgeColorSpeed, MS_PER_FRAME_60FPS),
|
|
186
|
+
);
|
|
187
|
+
colorB.set(
|
|
188
|
+
lerp(colorB.get(), targetRgb[2], badgeColorSpeed, MS_PER_FRAME_60FPS),
|
|
189
|
+
);
|
|
176
190
|
bgColor = lerpColor(
|
|
177
191
|
[colorR.get(), colorG.get(), colorB.get()],
|
|
178
192
|
[colorR.get(), colorG.get(), colorB.get()],
|
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
useSharedValue,
|
|
7
7
|
type SharedValue,
|
|
8
8
|
} from "react-native-reanimated";
|
|
9
|
-
import { MS_PER_FRAME_60FPS } from "../constants";
|
|
9
|
+
import { CANDLE_METRICS_DEFAULTS, MS_PER_FRAME_60FPS } from "../constants";
|
|
10
10
|
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
11
11
|
import { buildCandleGeometry } from "../draw/candle";
|
|
12
12
|
import type { ChartPadding } from "../draw/line";
|
|
13
13
|
import { lerp } from "../math/lerp";
|
|
14
|
-
import type { CandlePoint } from "../types";
|
|
14
|
+
import type { CandleMetrics, CandlePoint } from "../types";
|
|
15
15
|
|
|
16
16
|
const CANDLE_WIDTH_LERP_SPEED = 0.08;
|
|
17
17
|
|
|
@@ -29,6 +29,7 @@ export function useCandlePaths(
|
|
|
29
29
|
liveCandle: SharedValue<CandlePoint | null> | undefined,
|
|
30
30
|
candleWidthSecs: number,
|
|
31
31
|
active: boolean,
|
|
32
|
+
candleMetrics: CandleMetrics = CANDLE_METRICS_DEFAULTS,
|
|
32
33
|
) {
|
|
33
34
|
const targetCandleWidth = useDerivedValue(() => candleWidthSecs);
|
|
34
35
|
const displayCandleWidth = useSharedValue(candleWidthSecs);
|
|
@@ -92,6 +93,7 @@ export function useCandlePaths(
|
|
|
92
93
|
engine.displayMin.value,
|
|
93
94
|
engine.displayMax.value,
|
|
94
95
|
displayCandleWidth.get(),
|
|
96
|
+
candleMetrics,
|
|
95
97
|
);
|
|
96
98
|
});
|
|
97
99
|
|
package/src/hooks/useYAxis.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useDerivedValue, useSharedValue } from "react-native-reanimated";
|
|
2
2
|
|
|
3
3
|
import type { SkFont } from "@shopify/react-native-skia";
|
|
4
|
-
import { MS_PER_FRAME_60FPS } from "../constants";
|
|
4
|
+
import { GRID_METRICS_DEFAULTS, MS_PER_FRAME_60FPS } from "../constants";
|
|
5
5
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
6
6
|
import { computeGridEntries } from "../draw/grid";
|
|
7
7
|
import type { ChartPadding } from "../draw/line";
|
|
8
|
+
import type { GridMetrics } from "../types";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Compute Y-axis grid entries (values + labels) with animated fade-in/out.
|
|
@@ -17,6 +18,7 @@ export function useYAxis(
|
|
|
17
18
|
formatValue: (v: number) => string,
|
|
18
19
|
font: SkFont,
|
|
19
20
|
minGap = 36,
|
|
21
|
+
gridMetrics: GridMetrics = GRID_METRICS_DEFAULTS,
|
|
20
22
|
) {
|
|
21
23
|
const prevInterval = useSharedValue(0);
|
|
22
24
|
const labelAlphas = useSharedValue<Record<number, number>>({});
|
|
@@ -36,6 +38,7 @@ export function useYAxis(
|
|
|
36
38
|
formatValue,
|
|
37
39
|
dt,
|
|
38
40
|
minGap,
|
|
41
|
+
gridMetrics,
|
|
39
42
|
);
|
|
40
43
|
|
|
41
44
|
prevInterval.set(result.interval);
|
package/src/index.ts
CHANGED
|
@@ -29,20 +29,26 @@ export { useTradeStream } from "./hooks/useTradeStream";
|
|
|
29
29
|
|
|
30
30
|
export type {
|
|
31
31
|
BadgeConfig,
|
|
32
|
+
BadgeMetrics,
|
|
32
33
|
BadgeVariant,
|
|
34
|
+
CandleMetrics,
|
|
33
35
|
CandlePoint,
|
|
34
36
|
ChartInsets,
|
|
35
37
|
DegenOptions,
|
|
36
38
|
DegenShakePayload,
|
|
39
|
+
EmptyStateMetrics,
|
|
37
40
|
FontConfig,
|
|
38
41
|
FontWeight,
|
|
39
42
|
GradientConfig,
|
|
43
|
+
GridMetrics,
|
|
40
44
|
GridStyleConfig,
|
|
41
45
|
LeftEdgeFadeConfig,
|
|
42
46
|
LegendConfig,
|
|
43
47
|
LegendStyle,
|
|
44
48
|
LineConfig,
|
|
45
49
|
LiveChartCoreProps,
|
|
50
|
+
LiveChartMetrics,
|
|
51
|
+
LiveChartMetricsOverride,
|
|
46
52
|
LiveChartPalette,
|
|
47
53
|
LiveChartPoint,
|
|
48
54
|
LiveChartProps,
|
|
@@ -52,6 +58,7 @@ export type {
|
|
|
52
58
|
MarkerKind,
|
|
53
59
|
Momentum,
|
|
54
60
|
MomentumConfig,
|
|
61
|
+
MotionMetrics,
|
|
55
62
|
MultiSeriesDotConfig,
|
|
56
63
|
PulseConfig,
|
|
57
64
|
ReferenceLine,
|