react-native-livechart 1.1.0 → 2.0.1

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 +17 -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
@@ -9,6 +9,8 @@ import type {
9
9
  LeftEdgeFadeConfig,
10
10
  LegendConfig,
11
11
  LegendStyle,
12
+ LiveChartMetrics,
13
+ LiveChartMetricsOverride,
12
14
  DotConfig,
13
15
  DotRingConfig,
14
16
  MultiSeriesDotConfig,
@@ -22,7 +24,14 @@ import type {
22
24
  } from "../types";
23
25
 
24
26
  import type { SharedValue } from "react-native-reanimated";
25
- import { FADE_EDGE_WIDTH } from "../constants";
27
+ import {
28
+ BADGE_METRICS_DEFAULTS,
29
+ CANDLE_METRICS_DEFAULTS,
30
+ EMPTY_STATE_METRICS_DEFAULTS,
31
+ FADE_EDGE_WIDTH,
32
+ GRID_METRICS_DEFAULTS,
33
+ MOTION_METRICS_DEFAULTS,
34
+ } from "../constants";
26
35
 
27
36
  // ─── Resolved types (all fields required, no optionals) ──────────────────────
28
37
 
@@ -139,6 +148,28 @@ export interface ResolvedLeftEdgeFadeConfig {
139
148
 
140
149
  // ─── Resolver functions ───────────────────────────────────────────────────────
141
150
 
151
+ /**
152
+ * Uniform `boolean | Config` feature-flag resolver, shared by every toggle below.
153
+ * - `false` → `null` (explicitly disabled)
154
+ * - `undefined` → `defaultOn ? defaults : null` (the feature's default state)
155
+ * - `true` → `defaults`
156
+ * - object → defaults shallow-merged with the caller's overrides
157
+ *
158
+ * `defaultOn` makes each toggle's default explicit at the resolver. Toggles whose
159
+ * default-on is owned by the component prop (e.g. `badge = true`) pass
160
+ * `defaultOn: false` here, so a bare resolver call without that default stays off.
161
+ */
162
+ function resolveToggle<C extends object, R extends object>(
163
+ prop: boolean | C | undefined,
164
+ defaults: R,
165
+ defaultOn: boolean,
166
+ ): R | null {
167
+ if (prop === false) return null;
168
+ if (prop == null) return defaultOn ? defaults : null;
169
+ if (prop === true) return defaults;
170
+ return { ...defaults, ...prop } as R;
171
+ }
172
+
142
173
  const VALUE_LINE_DEFAULTS: ResolvedValueLineConfig = {
143
174
  strokeWidth: 1,
144
175
  intervals: [4, 4],
@@ -152,9 +183,7 @@ const VALUE_LINE_DEFAULTS: ResolvedValueLineConfig = {
152
183
  export function resolveValueLine(
153
184
  prop: boolean | ValueLineConfig | undefined,
154
185
  ): ResolvedValueLineConfig | null {
155
- if (!prop) return null;
156
- if (prop === true) return VALUE_LINE_DEFAULTS;
157
- return { ...VALUE_LINE_DEFAULTS, ...prop };
186
+ return resolveToggle(prop, VALUE_LINE_DEFAULTS, false);
158
187
  }
159
188
 
160
189
  const BADGE_DEFAULTS: ResolvedBadgeConfig = {
@@ -171,9 +200,7 @@ const BADGE_DEFAULTS: ResolvedBadgeConfig = {
171
200
  export function resolveBadge(
172
201
  prop: boolean | BadgeConfig | undefined,
173
202
  ): ResolvedBadgeConfig | null {
174
- if (!prop) return null;
175
- if (prop === true) return BADGE_DEFAULTS;
176
- return { ...BADGE_DEFAULTS, ...prop };
203
+ return resolveToggle(prop, BADGE_DEFAULTS, false);
177
204
  }
178
205
 
179
206
  const Y_AXIS_DEFAULTS: ResolvedYAxisConfig = {
@@ -187,9 +214,7 @@ const Y_AXIS_DEFAULTS: ResolvedYAxisConfig = {
187
214
  export function resolveYAxis(
188
215
  prop: boolean | YAxisConfig | undefined,
189
216
  ): ResolvedYAxisConfig | null {
190
- if (!prop) return null;
191
- if (prop === true) return Y_AXIS_DEFAULTS;
192
- return { ...Y_AXIS_DEFAULTS, ...prop };
217
+ return resolveToggle(prop, Y_AXIS_DEFAULTS, false);
193
218
  }
194
219
 
195
220
  const X_AXIS_DEFAULTS: ResolvedXAxisConfig = {
@@ -198,14 +223,12 @@ const X_AXIS_DEFAULTS: ResolvedXAxisConfig = {
198
223
 
199
224
  /**
200
225
  * Resolves `xAxis` prop to a fully-typed config or null (disabled).
201
- * Defaults to enabled (`true`) so `undefined` also returns the defaults.
226
+ * Defaults to enabled (`true`) so a bare `undefined` also returns the defaults.
202
227
  */
203
228
  export function resolveXAxis(
204
229
  prop: boolean | XAxisConfig | undefined,
205
230
  ): ResolvedXAxisConfig | null {
206
- if (prop === false) return null;
207
- if (prop === undefined || prop === true) return X_AXIS_DEFAULTS;
208
- return { ...X_AXIS_DEFAULTS, ...prop };
231
+ return resolveToggle(prop, X_AXIS_DEFAULTS, true);
209
232
  }
210
233
 
211
234
  const SCRUB_DEFAULTS: ResolvedScrubConfig = {
@@ -226,9 +249,7 @@ const SCRUB_DEFAULTS: ResolvedScrubConfig = {
226
249
  export function resolveScrub(
227
250
  prop: boolean | ScrubConfig | undefined,
228
251
  ): ResolvedScrubConfig | null {
229
- if (!prop) return null;
230
- if (prop === true) return SCRUB_DEFAULTS;
231
- return { ...SCRUB_DEFAULTS, ...prop };
252
+ return resolveToggle(prop, SCRUB_DEFAULTS, false);
232
253
  }
233
254
 
234
255
  const GRADIENT_DEFAULTS: ResolvedGradientConfig = {
@@ -243,9 +264,7 @@ const GRADIENT_DEFAULTS: ResolvedGradientConfig = {
243
264
  export function resolveGradient(
244
265
  prop: boolean | GradientConfig | undefined,
245
266
  ): ResolvedGradientConfig | null {
246
- if (!prop) return null;
247
- if (prop === true) return GRADIENT_DEFAULTS;
248
- return { ...GRADIENT_DEFAULTS, ...prop };
267
+ return resolveToggle(prop, GRADIENT_DEFAULTS, false);
249
268
  }
250
269
 
251
270
  /** Fallback when no theme background is passed (e.g. unit tests). */
@@ -295,9 +314,7 @@ const PULSE_DEFAULTS: ResolvedPulseConfig = {
295
314
  export function resolvePulse(
296
315
  prop: boolean | PulseConfig | undefined,
297
316
  ): ResolvedPulseConfig | null {
298
- if (!prop) return null;
299
- if (prop === true) return PULSE_DEFAULTS;
300
- return { ...PULSE_DEFAULTS, ...prop };
317
+ return resolveToggle(prop, PULSE_DEFAULTS, false);
301
318
  }
302
319
 
303
320
  const REFERENCE_LINE_VISUAL_DEFAULTS = {
@@ -499,9 +516,7 @@ const RING_DEFAULTS: ResolvedDotRingConfig = {
499
516
  export function resolveDotRing(
500
517
  prop: boolean | DotRingConfig | undefined,
501
518
  ): ResolvedDotRingConfig | null {
502
- if (prop === false) return null;
503
- if (prop === undefined || prop === true) return RING_DEFAULTS;
504
- return { ...RING_DEFAULTS, ...prop };
519
+ return resolveToggle(prop, RING_DEFAULTS, true);
505
520
  }
506
521
 
507
522
  /** Shared, fully-resolved dot styling (single- and multi-series). */
@@ -519,8 +534,18 @@ const DOT_DEFAULTS: ResolvedDotConfig = {
519
534
  color: undefined,
520
535
  };
521
536
 
522
- export function resolveDot(prop: DotConfig | undefined): ResolvedDotConfig {
523
- if (!prop) return DOT_DEFAULTS;
537
+ /**
538
+ * Resolves the `dot` prop. Always returns a config (the live dot's geometry is
539
+ * read unconditionally); visibility rides on `show`.
540
+ * - `false` → defaults with `show: false` (the canonical "hide the dot")
541
+ * - `undefined`/`true` → shown defaults
542
+ * - object → merged with defaults (honoring an explicit `show`)
543
+ */
544
+ export function resolveDot(
545
+ prop: boolean | DotConfig | undefined,
546
+ ): ResolvedDotConfig {
547
+ if (prop === false) return { ...DOT_DEFAULTS, show: false };
548
+ if (prop == null || prop === true) return DOT_DEFAULTS;
524
549
  return {
525
550
  radius: prop.radius ?? DOT_DEFAULTS.radius,
526
551
  ring: resolveDotRing(prop.ring),
@@ -538,13 +563,14 @@ export interface ResolvedMultiSeriesDotConfig extends ResolvedDotConfig {
538
563
  }
539
564
 
540
565
  export function resolveMultiSeriesDot(
541
- prop: MultiSeriesDotConfig | undefined,
566
+ prop: boolean | MultiSeriesDotConfig | undefined,
542
567
  ): ResolvedMultiSeriesDotConfig {
568
+ const cfg = typeof prop === "object" && prop !== null ? prop : undefined;
543
569
  return {
544
570
  ...resolveDot(prop),
545
- pulse: resolvePulse(prop?.pulse ?? true),
546
- valueLine: resolveValueLine(prop?.valueLine),
547
- valueLabel: prop?.valueLabel ?? true,
571
+ pulse: resolvePulse(cfg?.pulse ?? true),
572
+ valueLine: resolveValueLine(cfg?.valueLine),
573
+ valueLabel: cfg?.valueLabel ?? true,
548
574
  };
549
575
  }
550
576
 
@@ -579,3 +605,33 @@ export function resolveLegend(
579
605
  style: prop.style,
580
606
  };
581
607
  }
608
+
609
+ // ─── Metrics (sizing & motion tokens) ─────────────────────────────────────────
610
+
611
+ /** Canonical resolved metrics — every namespace/field present. */
612
+ const METRICS_DEFAULTS: LiveChartMetrics = {
613
+ badge: BADGE_METRICS_DEFAULTS,
614
+ candle: CANDLE_METRICS_DEFAULTS,
615
+ grid: GRID_METRICS_DEFAULTS,
616
+ motion: MOTION_METRICS_DEFAULTS,
617
+ emptyState: EMPTY_STATE_METRICS_DEFAULTS,
618
+ };
619
+
620
+ /**
621
+ * Resolve the `metrics` prop into a fully-typed config. Per-namespace shallow
622
+ * merge over the defaults — only the keys the caller sets are replaced, the same
623
+ * model as `applyPaletteOverride`. Returns the shared defaults object when no
624
+ * override is supplied (treated read-only by consumers).
625
+ */
626
+ export function resolveMetrics(
627
+ prop: LiveChartMetricsOverride | undefined,
628
+ ): LiveChartMetrics {
629
+ if (!prop) return METRICS_DEFAULTS;
630
+ return {
631
+ badge: { ...METRICS_DEFAULTS.badge, ...prop.badge },
632
+ candle: { ...METRICS_DEFAULTS.candle, ...prop.candle },
633
+ grid: { ...METRICS_DEFAULTS.grid, ...prop.grid },
634
+ motion: { ...METRICS_DEFAULTS.motion, ...prop.motion },
635
+ emptyState: { ...METRICS_DEFAULTS.emptyState, ...prop.emptyState },
636
+ };
637
+ }
@@ -21,6 +21,8 @@ export interface EngineConfig {
21
21
  value: SharedValue<number>;
22
22
  timeWindow: number;
23
23
  smoothing: number;
24
+ /** Extra catch-up speed added to `smoothing` when the live value lags. */
25
+ adaptiveSpeedBoost?: number;
24
26
  exaggerate?: boolean;
25
27
  referenceValue?: number;
26
28
  referenceValues?: number[];
@@ -78,6 +80,7 @@ export interface EngineFrameRefs {
78
80
  timestamp: SharedValue<number>;
79
81
  timeWindow: SharedValue<number>;
80
82
  smoothing: SharedValue<number>;
83
+ adaptiveSpeedBoostSV?: SharedValue<number | undefined>;
81
84
  exaggerateSV: SharedValue<boolean>;
82
85
  referenceValue: SharedValue<number | undefined>;
83
86
  referenceValues?: SharedValue<number[] | undefined>;
@@ -114,6 +117,7 @@ export function applyLiveChartEngineFrame(
114
117
  canvasHeight: sv.canvasHeight.value,
115
118
  timeWindow: sv.timeWindow.value,
116
119
  smoothing: sv.smoothing.value,
120
+ adaptiveSpeedBoost: sv.adaptiveSpeedBoostSV?.value,
117
121
  exaggerate: sv.exaggerateSV.value,
118
122
  referenceValue: sv.referenceValue.value,
119
123
  referenceValues: sv.referenceValues?.value,
@@ -140,6 +144,7 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
140
144
  // Low-frequency config → UI thread via useDerivedValue
141
145
  const timeWindow = useDerivedValue(() => config.timeWindow);
142
146
  const smoothing = useDerivedValue(() => config.smoothing);
147
+ const adaptiveSpeedBoostSV = useDerivedValue(() => config.adaptiveSpeedBoost);
143
148
  const exaggerateSV = useDerivedValue(() => config.exaggerate ?? false);
144
149
  const referenceValue = useDerivedValue(() => config.referenceValue);
145
150
  const referenceValues = useDerivedValue(() => config.referenceValues);
@@ -179,6 +184,7 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
179
184
  canvasHeight,
180
185
  timeWindow,
181
186
  smoothing,
187
+ adaptiveSpeedBoostSV,
182
188
  exaggerateSV,
183
189
  referenceValue,
184
190
  referenceValues,
@@ -14,6 +14,8 @@ export interface MultiSeriesEngineConfig {
14
14
  series: SharedValue<SeriesConfig[]>;
15
15
  timeWindow: number;
16
16
  smoothing: number;
17
+ /** Extra catch-up speed added to `smoothing` when a series tip lags. */
18
+ adaptiveSpeedBoost?: number;
17
19
  exaggerate?: boolean;
18
20
  referenceValue?: number;
19
21
  referenceValues?: number[];
@@ -36,6 +38,7 @@ export interface MultiEngineFrameRefs {
36
38
  canvasHeight: SharedValue<number>;
37
39
  timeWindow: SharedValue<number>;
38
40
  smoothing: SharedValue<number>;
41
+ adaptiveSpeedBoostSV?: SharedValue<number | undefined>;
39
42
  exaggerateSV: SharedValue<boolean>;
40
43
  referenceValue: SharedValue<number | undefined>;
41
44
  referenceValues?: SharedValue<number[] | undefined>;
@@ -107,6 +110,7 @@ export function applyLiveChartSeriesEngineFrame(
107
110
  canvasHeight: sv.canvasHeight.value,
108
111
  timeWindow: sv.timeWindow.value,
109
112
  smoothing: sv.smoothing.value,
113
+ adaptiveSpeedBoost: sv.adaptiveSpeedBoostSV?.value,
110
114
  exaggerate: sv.exaggerateSV.value,
111
115
  referenceValue: sv.referenceValue.value,
112
116
  referenceValues: sv.referenceValues?.value,
@@ -135,6 +139,7 @@ export function useLiveChartSeriesEngine(
135
139
  ): MultiEngineState {
136
140
  const timeWindow = useDerivedValue(() => config.timeWindow);
137
141
  const smoothing = useDerivedValue(() => config.smoothing);
142
+ const adaptiveSpeedBoostSV = useDerivedValue(() => config.adaptiveSpeedBoost);
138
143
  const exaggerateSV = useDerivedValue(() => config.exaggerate ?? false);
139
144
  const referenceValue = useDerivedValue(() => config.referenceValue);
140
145
  const referenceValues = useDerivedValue(() => config.referenceValues);
@@ -185,6 +190,7 @@ export function useLiveChartSeriesEngine(
185
190
  canvasHeight,
186
191
  timeWindow,
187
192
  smoothing,
193
+ adaptiveSpeedBoostSV,
188
194
  exaggerateSV,
189
195
  referenceValue,
190
196
  referenceValues,
@@ -1,4 +1,5 @@
1
- import type { CandlePoint } from "../types";
1
+ import { CANDLE_METRICS_DEFAULTS } from "../constants";
2
+ import type { CandleMetrics, CandlePoint } from "../types";
2
3
  import type { ChartPadding } from "./line";
3
4
 
4
5
  export interface CandleRect {
@@ -21,9 +22,6 @@ export interface CandleGeometry {
21
22
  wicks: CandleWick[];
22
23
  }
23
24
 
24
- const MIN_BODY_PX = 1;
25
- const MAX_BODY_PX = 40;
26
-
27
25
  function candleTimeToX(
28
26
  t: number,
29
27
  padLeft: number,
@@ -62,6 +60,7 @@ function appendCandleShapes(
62
60
  valRange: number,
63
61
  candleWidthSecs: number,
64
62
  bodyW: number,
63
+ minBodyPx: number,
65
64
  bodies: CandleRect[],
66
65
  wicks: CandleWick[],
67
66
  ): void {
@@ -94,7 +93,7 @@ function appendCandleShapes(
94
93
  valRange,
95
94
  chartH,
96
95
  );
97
- const bodyH = Math.max(MIN_BODY_PX, bodyBot - bodyTop);
96
+ const bodyH = Math.max(minBodyPx, bodyBot - bodyTop);
98
97
 
99
98
  let bx = xCenter - bodyW / 2;
100
99
  let bw = bodyW;
@@ -132,6 +131,7 @@ export function buildCandleGeometry(
132
131
  displayMin: number,
133
132
  displayMax: number,
134
133
  candleWidthSecs: number,
134
+ metrics: CandleMetrics = CANDLE_METRICS_DEFAULTS,
135
135
  ): CandleGeometry {
136
136
  "worklet";
137
137
  const chartW = canvasW - padding.left - padding.right;
@@ -142,7 +142,10 @@ export function buildCandleGeometry(
142
142
  return { bodies: [], wicks: [] };
143
143
 
144
144
  const slotPx = (candleWidthSecs / windowSecs) * chartW;
145
- const bodyW = Math.max(1, Math.min(slotPx * 0.8, slotPx - 2, MAX_BODY_PX));
145
+ const bodyW = Math.max(
146
+ 1,
147
+ Math.min(slotPx * metrics.bodyWidthRatio, slotPx - 2, metrics.maxBodyPx),
148
+ );
146
149
 
147
150
  const bodies: CandleRect[] = [];
148
151
  const wicks: CandleWick[] = [];
@@ -180,6 +183,7 @@ export function buildCandleGeometry(
180
183
  valRange,
181
184
  candleWidthSecs,
182
185
  bodyW,
186
+ metrics.minBodyPx,
183
187
  bodies,
184
188
  wicks,
185
189
  );
@@ -201,6 +205,7 @@ export function buildCandleGeometry(
201
205
  valRange,
202
206
  candleWidthSecs,
203
207
  bodyW,
208
+ metrics.minBodyPx,
204
209
  bodies,
205
210
  wicks,
206
211
  );
package/src/draw/grid.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { GRID_METRICS_DEFAULTS } from "../constants";
1
2
  import { lerp } from "../math/lerp";
3
+ import type { GridMetrics } from "../types";
2
4
 
3
5
  export interface YAxisEntry {
4
6
  y: number;
@@ -64,9 +66,6 @@ function divisible(val: number, interval: number): boolean {
64
66
  return Math.abs(ratio - Math.round(ratio)) < 0.01;
65
67
  }
66
68
 
67
- const FADE_IN = 0.18;
68
- const FADE_OUT = 0.12;
69
-
70
69
  /** Map data value → canvas Y (nested arrows inside worklets are not UI-thread-safe). */
71
70
  function gridValueToY(
72
71
  val: number,
@@ -94,6 +93,7 @@ export function computeGridEntries(
94
93
  formatValue: (v: number) => string,
95
94
  dt: number,
96
95
  minGap = 36,
96
+ grid: GridMetrics = GRID_METRICS_DEFAULTS,
97
97
  ): { entries: YAxisEntry[]; interval: number } {
98
98
  "worklet";
99
99
  const chartH = canvasHeight - padTop - padBottom;
@@ -135,7 +135,7 @@ export function computeGridEntries(
135
135
  const key = Number(keys[i]);
136
136
  const alpha = labelAlphas[key];
137
137
  const target = targets[key] ?? 0;
138
- const speed = target >= alpha ? FADE_IN : FADE_OUT;
138
+ const speed = target >= alpha ? grid.fadeInSpeed : grid.fadeOutSpeed;
139
139
  let next = lerp(alpha, target, speed, dt);
140
140
  if (Math.abs(next - target) < 0.02) next = target;
141
141
  if (next < 0.01 && target === 0) {
@@ -152,7 +152,7 @@ export function computeGridEntries(
152
152
  /* istanbul ignore else -- key already tracked after phase 2 when targets repeat across frames */
153
153
  if (labelAlphas[key] === undefined) {
154
154
  /* istanbul ignore next -- targets keys always map to a number; ?? keeps types safe */
155
- labelAlphas[key] = (targets[key] ?? 0) * FADE_IN;
155
+ labelAlphas[key] = (targets[key] ?? 0) * grid.fadeInSpeed;
156
156
  }
157
157
  }
158
158
 
package/src/draw/line.ts CHANGED
@@ -1,11 +1,5 @@
1
- import {
2
- BADGE_DOT_GAP,
3
- BADGE_MARGIN_RIGHT,
4
- BADGE_PILL_PAD_X,
5
- BADGE_PILL_PAD_Y,
6
- BADGE_TAIL_LEN,
7
- } from "../constants";
8
- import type { ChartInsets, LiveChartPoint } from "../types";
1
+ import { BADGE_METRICS_DEFAULTS } from "../constants";
2
+ import type { BadgeMetrics, ChartInsets, LiveChartPoint } from "../types";
9
3
  export {
10
4
  BADGE_DOT_GAP,
11
5
  BADGE_MARGIN_RIGHT,
@@ -36,10 +30,14 @@ export const DEFAULT_PADDING: ChartPadding = {
36
30
  * When `showTail` is false the tail spike is omitted and only the round cap
37
31
  * radius is returned, letting callers shrink the right gutter.
38
32
  */
39
- export function badgeTailAndCap(fontSize: number, showTail = true): number {
33
+ export function badgeTailAndCap(
34
+ fontSize: number,
35
+ showTail = true,
36
+ badge: BadgeMetrics = BADGE_METRICS_DEFAULTS,
37
+ ): number {
40
38
  "worklet";
41
- const pillH = fontSize + BADGE_PILL_PAD_Y * 2;
42
- return (showTail ? BADGE_TAIL_LEN : 0) + pillH / 2;
39
+ const pillH = fontSize + badge.padY * 2;
40
+ return (showTail ? badge.tailLength : 0) + pillH / 2;
43
41
  }
44
42
 
45
43
  /**
@@ -56,10 +54,11 @@ export function pillTextLeftX(
56
54
  paddingRight: number,
57
55
  tl: number,
58
56
  textWidth: number,
57
+ badge: BadgeMetrics = BADGE_METRICS_DEFAULTS,
59
58
  ): number {
60
59
  "worklet";
61
60
  const bodyLeft = canvasWidth - paddingRight + tl;
62
- const bodyRight = canvasWidth - BADGE_MARGIN_RIGHT;
61
+ const bodyRight = canvasWidth - badge.marginEdge;
63
62
  return (bodyLeft + bodyRight - textWidth) / 2;
64
63
  }
65
64
 
@@ -98,10 +97,11 @@ export function minPaddingRightForBadgeYAxisAlign(
98
97
  fontSize: number,
99
98
  textWidth: number,
100
99
  showTail = true,
100
+ badge: BadgeMetrics = BADGE_METRICS_DEFAULTS,
101
101
  ): number {
102
- const tl = badgeTailAndCap(fontSize, showTail);
102
+ const tl = badgeTailAndCap(fontSize, showTail, badge);
103
103
  return Math.ceil(
104
- BADGE_DOT_GAP + tl + 2 * BADGE_PILL_PAD_X + textWidth + BADGE_MARGIN_RIGHT,
104
+ badge.dotGap + tl + 2 * badge.padX + textWidth + badge.marginEdge,
105
105
  );
106
106
  }
107
107
 
@@ -110,8 +110,9 @@ export function resolveAutoRight(
110
110
  yAxis: boolean,
111
111
  badge: boolean,
112
112
  showTail = true,
113
+ badgeMetrics: BadgeMetrics = BADGE_METRICS_DEFAULTS,
113
114
  ): number {
114
- if (badge) return minPaddingRightForBadgeYAxisAlign(12, 49, showTail);
115
+ if (badge) return minPaddingRightForBadgeYAxisAlign(12, 49, showTail, badgeMetrics);
115
116
  if (yAxis) return 44;
116
117
  return DEFAULT_PADDING.right;
117
118
  }
@@ -120,15 +121,21 @@ export function resolveAutoRight(
120
121
  * Minimum `padding.left` for a badge pill drawn in the left chart margin (label width + horizontal padding + dot gap).
121
122
  * `resolveChartLayout` does not call this; it remains available for custom layouts or `resolvePadding(..., badgeOnLeft: true)`.
122
123
  */
123
- export function minPaddingLeftForBadge(textWidth: number): number {
124
+ export function minPaddingLeftForBadge(
125
+ textWidth: number,
126
+ badge: BadgeMetrics = BADGE_METRICS_DEFAULTS,
127
+ ): number {
124
128
  return Math.ceil(
125
- BADGE_MARGIN_RIGHT + 2 * BADGE_PILL_PAD_X + textWidth + BADGE_DOT_GAP,
129
+ badge.marginEdge + 2 * badge.padX + textWidth + badge.dotGap,
126
130
  );
127
131
  }
128
132
 
129
133
  /** Default left inset, or a wider inset when `badgeOnLeft` is true (see `minPaddingLeftForBadge`). */
130
- export function resolveAutoLeft(badgeOnLeft: boolean): number {
131
- if (badgeOnLeft) return minPaddingLeftForBadge(49);
134
+ export function resolveAutoLeft(
135
+ badgeOnLeft: boolean,
136
+ badgeMetrics: BadgeMetrics = BADGE_METRICS_DEFAULTS,
137
+ ): number {
138
+ if (badgeOnLeft) return minPaddingLeftForBadge(49, badgeMetrics);
132
139
  return DEFAULT_PADDING.left;
133
140
  }
134
141
 
@@ -171,9 +178,15 @@ export function resolvePadding(
171
178
  badgeOnLeft = false,
172
179
  xAxis = true,
173
180
  showTail = true,
181
+ badgeMetrics: BadgeMetrics = BADGE_METRICS_DEFAULTS,
174
182
  ): ChartPadding {
175
- const autoRight = resolveAutoRight(yAxis, badge && !badgeOnLeft, showTail);
176
- const autoLeft = resolveAutoLeft(badgeOnLeft);
183
+ const autoRight = resolveAutoRight(
184
+ yAxis,
185
+ badge && !badgeOnLeft,
186
+ showTail,
187
+ badgeMetrics,
188
+ );
189
+ const autoLeft = resolveAutoLeft(badgeOnLeft, badgeMetrics);
177
190
  const autoBottom = xAxis ? DEFAULT_PADDING.bottom : 8;
178
191
  if (!override) {
179
192
  return {
@@ -239,12 +252,80 @@ export function buildLinePoints(
239
252
  }
240
253
  const startIdx = Math.max(0, lo - 1);
241
254
 
242
- for (let i = startIdx; i < data.length; i++) {
243
- if (data[i].time > now) break;
255
+ // End of the visible range: first index strictly after `now` (upper bound).
256
+ let elo = startIdx;
257
+ let ehi = data.length;
258
+ while (elo < ehi) {
259
+ const emid = (elo + ehi) >> 1;
260
+ if (data[emid].time <= now) elo = emid + 1;
261
+ else ehi = emid;
262
+ }
263
+ const endIdx = elo;
264
+
265
+ const xScale = chartW / windowSecs;
266
+ const yScale = chartH / valRange;
267
+
268
+ // Decimate to ~2 points per horizontal pixel once the window is denser than
269
+ // that. Drawing more points than the canvas is wide is wasted per-frame work
270
+ // (array build + Skia stroke) that scales with sample count rather than
271
+ // pixels — the dominant cost on dense / wide-window charts, and the reason
272
+ // scrubbing a saturated window drops frames. Below the threshold we keep the
273
+ // exact per-sample path (and existing behaviour) untouched.
274
+ const maxPlainPoints = Math.ceil(chartW * 2);
275
+
276
+ if (endIdx - startIdx <= maxPlainPoints) {
277
+ for (let i = startIdx; i < endIdx; i++) {
278
+ pts.push(
279
+ padding.left + (data[i].time - winStart) * xScale,
280
+ padding.top + (displayMax - data[i].value) * yScale,
281
+ );
282
+ }
283
+ } else {
284
+ // Min/max-per-pixel-column decimation: within each pixel column keep the
285
+ // lowest- and highest-value samples (emitted in their original time order)
286
+ // so the line's envelope and any volatility spikes survive at full vertical
287
+ // fidelity while the point count stays bounded by the canvas width.
288
+ let curCol = -2147483648;
289
+ let minIdx = startIdx;
290
+ let maxIdx = startIdx;
291
+ for (let i = startIdx; i < endIdx; i++) {
292
+ const col = ((data[i].time - winStart) * xScale) | 0;
293
+ if (col !== curCol) {
294
+ if (curCol !== -2147483648) {
295
+ const a = minIdx <= maxIdx ? minIdx : maxIdx;
296
+ const b = minIdx <= maxIdx ? maxIdx : minIdx;
297
+ pts.push(
298
+ padding.left + (data[a].time - winStart) * xScale,
299
+ padding.top + (displayMax - data[a].value) * yScale,
300
+ );
301
+ if (b !== a) {
302
+ pts.push(
303
+ padding.left + (data[b].time - winStart) * xScale,
304
+ padding.top + (displayMax - data[b].value) * yScale,
305
+ );
306
+ }
307
+ }
308
+ curCol = col;
309
+ minIdx = i;
310
+ maxIdx = i;
311
+ } else {
312
+ if (data[i].value < data[minIdx].value) minIdx = i;
313
+ if (data[i].value > data[maxIdx].value) maxIdx = i;
314
+ }
315
+ }
316
+ // Flush the final column.
317
+ const a = minIdx <= maxIdx ? minIdx : maxIdx;
318
+ const b = minIdx <= maxIdx ? maxIdx : minIdx;
244
319
  pts.push(
245
- padding.left + ((data[i].time - winStart) / windowSecs) * chartW,
246
- padding.top + ((displayMax - data[i].value) / valRange) * chartH,
320
+ padding.left + (data[a].time - winStart) * xScale,
321
+ padding.top + (displayMax - data[a].value) * yScale,
247
322
  );
323
+ if (b !== a) {
324
+ pts.push(
325
+ padding.left + (data[b].time - winStart) * xScale,
326
+ padding.top + (displayMax - data[b].value) * yScale,
327
+ );
328
+ }
248
329
  }
249
330
 
250
331
  // Live tip at current time with smoothed value