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.
Files changed (67) hide show
  1. package/README.md +15 -4
  2. package/dist/components/CrosshairLine.d.ts.map +1 -1
  3. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  4. package/dist/components/LiveChart.d.ts.map +1 -1
  5. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  6. package/dist/components/LoadingOverlay.d.ts +6 -2
  7. package/dist/components/LoadingOverlay.d.ts.map +1 -1
  8. package/dist/components/MarkerOverlay.d.ts +8 -4
  9. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  10. package/dist/components/YAxisOverlay.d.ts +4 -2
  11. package/dist/components/YAxisOverlay.d.ts.map +1 -1
  12. package/dist/constants.d.ts +16 -13
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/core/liveChartEngineTick.d.ts +2 -0
  15. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  16. package/dist/core/liveChartSeriesEngineTick.d.ts +2 -0
  17. package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
  18. package/dist/core/resolveConfig.d.ts +18 -4
  19. package/dist/core/resolveConfig.d.ts.map +1 -1
  20. package/dist/core/useLiveChartEngine.d.ts +3 -0
  21. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  22. package/dist/core/useLiveChartSeriesEngine.d.ts +3 -0
  23. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  24. package/dist/draw/candle.d.ts +2 -2
  25. package/dist/draw/candle.d.ts.map +1 -1
  26. package/dist/draw/grid.d.ts +2 -1
  27. package/dist/draw/grid.d.ts.map +1 -1
  28. package/dist/draw/line.d.ts +8 -8
  29. package/dist/draw/line.d.ts.map +1 -1
  30. package/dist/draw/markerAtlas.d.ts +44 -0
  31. package/dist/draw/markerAtlas.d.ts.map +1 -0
  32. package/dist/hooks/resolveChartLayout.d.ts +3 -1
  33. package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
  34. package/dist/hooks/useBadge.d.ts +2 -2
  35. package/dist/hooks/useBadge.d.ts.map +1 -1
  36. package/dist/hooks/useCandlePaths.d.ts +2 -2
  37. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  38. package/dist/hooks/useYAxis.d.ts +2 -1
  39. package/dist/hooks/useYAxis.d.ts.map +1 -1
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/types.d.ts +100 -5
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/components/CrosshairLine.tsx +23 -9
  46. package/src/components/CrosshairOverlay.tsx +23 -9
  47. package/src/components/LiveChart.tsx +14 -0
  48. package/src/components/LiveChartSeries.tsx +10 -1
  49. package/src/components/LoadingOverlay.tsx +20 -11
  50. package/src/components/MarkerOverlay.tsx +119 -164
  51. package/src/components/YAxisOverlay.tsx +8 -4
  52. package/src/constants.ts +54 -14
  53. package/src/core/liveChartEngineTick.ts +7 -2
  54. package/src/core/liveChartSeriesEngineTick.ts +8 -2
  55. package/src/core/resolveConfig.ts +88 -32
  56. package/src/core/useLiveChartEngine.ts +6 -0
  57. package/src/core/useLiveChartSeriesEngine.ts +6 -0
  58. package/src/draw/candle.ts +11 -6
  59. package/src/draw/grid.ts +5 -5
  60. package/src/draw/line.ts +106 -25
  61. package/src/draw/markerAtlas.ts +304 -0
  62. package/src/hooks/resolveChartLayout.ts +11 -2
  63. package/src/hooks/useBadge.ts +38 -24
  64. package/src/hooks/useCandlePaths.ts +4 -2
  65. package/src/hooks/useYAxis.ts +4 -1
  66. package/src/index.ts +7 -0
  67. 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(config.yAxis, badgeUsesRightGutter, showTail);
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 };
@@ -6,11 +6,8 @@ import {
6
6
  type SharedValue,
7
7
  } from "react-native-reanimated";
8
8
  import {
9
- BADGE_DOT_GAP,
10
- BADGE_MARGIN_RIGHT,
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 { BadgeVariant, LiveChartPalette, Momentum } from "../types";
26
-
27
- const TAIL_SPREAD = 2.5;
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() + BADGE_PILL_PAD_Y * 2;
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 * BADGE_PILL_PAD_X + textW;
105
- const bodyRight = dotXPos - BADGE_DOT_GAP;
106
- const bodyLeft = Math.max(BADGE_MARGIN_RIGHT, bodyRight - pillW);
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 + BADGE_DOT_GAP + tl;
118
- const bodyRight = w - BADGE_MARGIN_RIGHT;
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(w, padding.right, BADGE_DOT_GAP + tl, textW);
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 + BADGE_DOT_GAP;
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 + BADGE_TAIL_LEN + 2,
145
+ badgeX + badgeMetrics.tailLength + 2,
138
146
  badgeY + pillH,
139
147
  badgeX + 3,
140
- badgeY + r + TAIL_SPREAD,
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 - TAIL_SPREAD,
147
- badgeX + BADGE_TAIL_LEN + 2,
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(lerp(colorR.get(), targetRgb[0], 0.08, MS_PER_FRAME_60FPS));
174
- colorG.set(lerp(colorG.get(), targetRgb[1], 0.08, MS_PER_FRAME_60FPS));
175
- colorB.set(lerp(colorB.get(), targetRgb[2], 0.08, MS_PER_FRAME_60FPS));
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
 
@@ -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,