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
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
523
|
-
|
|
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(
|
|
546
|
-
valueLine: resolveValueLine(
|
|
547
|
-
valueLabel:
|
|
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,
|
package/src/draw/candle.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
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(
|
|
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(
|
|
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 ?
|
|
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) *
|
|
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
|
-
|
|
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(
|
|
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 +
|
|
42
|
-
return (showTail ?
|
|
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 -
|
|
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
|
-
|
|
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(
|
|
124
|
+
export function minPaddingLeftForBadge(
|
|
125
|
+
textWidth: number,
|
|
126
|
+
badge: BadgeMetrics = BADGE_METRICS_DEFAULTS,
|
|
127
|
+
): number {
|
|
124
128
|
return Math.ceil(
|
|
125
|
-
|
|
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(
|
|
131
|
-
|
|
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(
|
|
176
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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 + (
|
|
246
|
-
padding.top + (
|
|
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
|