react-native-livechart 3.12.0 → 4.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/dist/components/BadgeOverlay.d.ts +9 -1
- package/dist/components/BadgeOverlay.d.ts.map +1 -1
- package/dist/components/ChartOverlayLayer.d.ts +19 -0
- package/dist/components/ChartOverlayLayer.d.ts.map +1 -0
- package/dist/components/CustomMarkerOverlay.d.ts +16 -6
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +36 -0
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +5 -2
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +7 -1
- package/dist/components/DotOverlay.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/MarkerOverlay.d.ts +6 -3
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +4 -1
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/ReferenceLineGroupOverlay.d.ts +27 -0
- package/dist/components/ReferenceLineGroupOverlay.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +25 -2
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/XAxisOverlay.d.ts +7 -1
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +12 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +29 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts +18 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +56 -2
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +58 -7
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +8 -2
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +20 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/draw/volume.d.ts +25 -0
- package/dist/draw/volume.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +12 -6
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/overlayScale.d.ts +22 -0
- package/dist/hooks/overlayScale.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +9 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
- package/dist/hooks/useBadge.d.ts +22 -1
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +6 -0
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartOverlayContext.d.ts +45 -0
- package/dist/hooks/useChartOverlayContext.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +8 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -2
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useLiveDot.d.ts +6 -1
- package/dist/hooks/useLiveDot.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +9 -4
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +82 -0
- package/dist/hooks/usePanScroll.d.ts.map +1 -0
- package/dist/hooks/usePinchZoom.d.ts +74 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -0
- package/dist/hooks/useReferenceDrag.d.ts +24 -0
- package/dist/hooks/useReferenceDrag.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +23 -4
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLinePress.d.ts +5 -1
- package/dist/hooks/useReferenceLinePress.d.ts.map +1 -1
- package/dist/hooks/useVisibleRange.d.ts +39 -0
- package/dist/hooks/useVisibleRange.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/markerCluster.d.ts +47 -0
- package/dist/math/markerCluster.d.ts.map +1 -0
- package/dist/math/markers.d.ts +10 -0
- package/dist/math/markers.d.ts.map +1 -1
- package/dist/math/referenceDrag.d.ts +24 -0
- package/dist/math/referenceDrag.d.ts.map +1 -0
- package/dist/math/referenceGroup.d.ts +31 -0
- package/dist/math/referenceGroup.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +33 -7
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/types.d.ts +505 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BadgeOverlay.tsx +26 -1
- package/src/components/ChartOverlayLayer.tsx +32 -0
- package/src/components/CustomMarkerOverlay.tsx +150 -57
- package/src/components/CustomReferenceLineOverlay.tsx +250 -0
- package/src/components/CustomTooltipOverlay.tsx +27 -7
- package/src/components/DotOverlay.tsx +9 -0
- package/src/components/LiveChart.tsx +549 -32
- package/src/components/LiveChartSeries.tsx +110 -5
- package/src/components/MarkerOverlay.tsx +92 -6
- package/src/components/MultiSeriesDots.tsx +12 -3
- package/src/components/ReferenceLineGroupOverlay.tsx +207 -0
- package/src/components/ReferenceLineOverlay.tsx +105 -32
- package/src/components/XAxisOverlay.tsx +9 -2
- package/src/components/YAxisOverlay.tsx +48 -26
- package/src/constants.ts +10 -0
- package/src/core/liveChartEngineTick.ts +81 -8
- package/src/core/liveChartSeriesEngineTick.ts +50 -9
- package/src/core/resolveConfig.ts +122 -1
- package/src/core/useLiveChartEngine.ts +103 -10
- package/src/core/useLiveChartSeriesEngine.ts +26 -3
- package/src/draw/grid.ts +13 -1
- package/src/draw/markerAtlas.ts +117 -3
- package/src/draw/volume.ts +190 -0
- package/src/hooks/crosshairShared.ts +37 -9
- package/src/hooks/overlayScale.ts +59 -0
- package/src/hooks/resolveChartLayout.ts +33 -0
- package/src/hooks/useBadge.ts +89 -21
- package/src/hooks/useCandlePaths.ts +104 -7
- package/src/hooks/useChartOverlayContext.ts +93 -0
- package/src/hooks/useChartPaths.ts +14 -1
- package/src/hooks/useCrosshair.ts +29 -2
- package/src/hooks/useLiveDot.ts +16 -5
- package/src/hooks/useMarkers.ts +39 -11
- package/src/hooks/usePanScroll.ts +253 -0
- package/src/hooks/usePinchZoom.ts +189 -0
- package/src/hooks/useReferenceDrag.ts +254 -0
- package/src/hooks/useReferenceLine.ts +56 -9
- package/src/hooks/useReferenceLinePress.ts +16 -2
- package/src/hooks/useVisibleRange.ts +119 -0
- package/src/index.ts +17 -1
- package/src/math/markerCluster.ts +214 -0
- package/src/math/markers.ts +35 -3
- package/src/math/referenceDrag.ts +66 -0
- package/src/math/referenceGroup.ts +65 -0
- package/src/math/referenceLines.ts +76 -19
- package/src/types.ts +525 -21
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
LineStyleConfig,
|
|
15
15
|
LiveChartMetrics,
|
|
16
16
|
LiveChartMetricsOverride,
|
|
17
|
+
MarkerClusterConfig,
|
|
17
18
|
DotConfig,
|
|
18
19
|
DotRingConfig,
|
|
19
20
|
MultiSeriesDotConfig,
|
|
@@ -28,12 +29,15 @@ import type {
|
|
|
28
29
|
ThresholdLineConfig,
|
|
29
30
|
TradeEvent,
|
|
30
31
|
ValueLineConfig,
|
|
32
|
+
VolumeConfig,
|
|
31
33
|
XAxisConfig,
|
|
32
34
|
YAxisConfig,
|
|
35
|
+
ZoomConfig,
|
|
33
36
|
} from "../types";
|
|
34
37
|
|
|
35
38
|
import type { ComponentType, ReactElement } from "react";
|
|
36
39
|
import type { SharedValue } from "react-native-reanimated";
|
|
40
|
+
import type { ResolvedMarkerCluster } from "../math/markerCluster";
|
|
37
41
|
import {
|
|
38
42
|
BADGE_METRICS_DEFAULTS,
|
|
39
43
|
CANDLE_METRICS_DEFAULTS,
|
|
@@ -57,10 +61,39 @@ export interface ResolvedBadgeConfig {
|
|
|
57
61
|
tail: boolean;
|
|
58
62
|
position: "right" | "left";
|
|
59
63
|
background: string | undefined;
|
|
64
|
+
/** undefined → capsule (pillHeight / 2) at render time */
|
|
65
|
+
radius: number | undefined;
|
|
66
|
+
/** undefined → no border */
|
|
67
|
+
borderColor: string | undefined;
|
|
68
|
+
borderWidth: number;
|
|
69
|
+
/** undefined → variant/theme text color at render time */
|
|
70
|
+
textColor: string | undefined;
|
|
71
|
+
fontSize: number | undefined;
|
|
72
|
+
fontFamily: string | undefined;
|
|
73
|
+
fontWeight: FontWeight | undefined;
|
|
74
|
+
offsetX: number;
|
|
75
|
+
offsetY: number;
|
|
76
|
+
/** Track the visible window's right-edge price while scrolled back. */
|
|
77
|
+
followViewEdge: boolean;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
export interface ResolvedYAxisConfig {
|
|
63
81
|
minGap: number;
|
|
82
|
+
/** Float the axis over a full-width plot (no reserved right gutter). */
|
|
83
|
+
float: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ResolvedVolumeConfig {
|
|
87
|
+
/** undefined → use palette.candleUp at render time. */
|
|
88
|
+
upColor: string | undefined;
|
|
89
|
+
/** undefined → use palette.candleDown at render time. */
|
|
90
|
+
downColor: string | undefined;
|
|
91
|
+
/** Reserved band height (px) — the tallest a bar can be. */
|
|
92
|
+
maxHeight: number;
|
|
93
|
+
/** Corner radius (px) of bar tops. */
|
|
94
|
+
radius: number;
|
|
95
|
+
/** Opacity (0..1) applied to the whole band. */
|
|
96
|
+
opacity: number;
|
|
64
97
|
}
|
|
65
98
|
|
|
66
99
|
/** Resolved straight-line styling (connector, etc.). `color: undefined` → caller default. */
|
|
@@ -119,7 +152,7 @@ export interface ResolvedScrubConfig {
|
|
|
119
152
|
/** Tooltip pill corner radius in px. */
|
|
120
153
|
tooltipBorderRadius: number;
|
|
121
154
|
/** Where the tooltip pill sits relative to the scrub line. */
|
|
122
|
-
tooltipPlacement: "side" | "top" | "bottom";
|
|
155
|
+
tooltipPlacement: "side" | "top" | "bottom" | "point";
|
|
123
156
|
/** Gap (px) between the tooltip and the plot edge it's pinned to. */
|
|
124
157
|
tooltipMargin: number;
|
|
125
158
|
/** Show the value row in the default tooltip body. */
|
|
@@ -336,6 +369,16 @@ const BADGE_DEFAULTS: ResolvedBadgeConfig = {
|
|
|
336
369
|
tail: true,
|
|
337
370
|
position: "right",
|
|
338
371
|
background: undefined,
|
|
372
|
+
radius: undefined,
|
|
373
|
+
borderColor: undefined,
|
|
374
|
+
borderWidth: 1,
|
|
375
|
+
textColor: undefined,
|
|
376
|
+
fontSize: undefined,
|
|
377
|
+
fontFamily: undefined,
|
|
378
|
+
fontWeight: undefined,
|
|
379
|
+
offsetX: 0,
|
|
380
|
+
offsetY: 0,
|
|
381
|
+
followViewEdge: false,
|
|
339
382
|
};
|
|
340
383
|
|
|
341
384
|
/**
|
|
@@ -350,6 +393,7 @@ export function resolveBadge(
|
|
|
350
393
|
|
|
351
394
|
const Y_AXIS_DEFAULTS: ResolvedYAxisConfig = {
|
|
352
395
|
minGap: 36,
|
|
396
|
+
float: false,
|
|
353
397
|
};
|
|
354
398
|
|
|
355
399
|
/**
|
|
@@ -362,6 +406,47 @@ export function resolveYAxis(
|
|
|
362
406
|
return resolveToggle(prop, Y_AXIS_DEFAULTS, false);
|
|
363
407
|
}
|
|
364
408
|
|
|
409
|
+
const VOLUME_DEFAULTS: ResolvedVolumeConfig = {
|
|
410
|
+
upColor: undefined,
|
|
411
|
+
downColor: undefined,
|
|
412
|
+
maxHeight: 48,
|
|
413
|
+
radius: 2,
|
|
414
|
+
opacity: 0.6,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Resolves the `volume` prop to a fully-typed config or null (disabled).
|
|
419
|
+
* `true` → defaults, object → merged with defaults, falsy → null. Colors left
|
|
420
|
+
* `undefined` fall back to the candle palette at render time.
|
|
421
|
+
*/
|
|
422
|
+
export function resolveVolume(
|
|
423
|
+
prop: boolean | VolumeConfig | undefined,
|
|
424
|
+
): ResolvedVolumeConfig | null {
|
|
425
|
+
return resolveToggle(prop, VOLUME_DEFAULTS, false);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface ResolvedZoomConfig {
|
|
429
|
+
/** Tightest window (max zoom-in), seconds. `undefined` → `timeWindow / 8`. */
|
|
430
|
+
minTimeWindow: number | undefined;
|
|
431
|
+
/** Widest window (max zoom-out), seconds. `undefined` → full data span. */
|
|
432
|
+
maxTimeWindow: number | undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const ZOOM_DEFAULTS: ResolvedZoomConfig = {
|
|
436
|
+
minTimeWindow: undefined,
|
|
437
|
+
maxTimeWindow: undefined,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Resolves the `zoom` prop to a config or null (disabled). `true` → defaults
|
|
442
|
+
* (bounds derived at gesture time), object → merged, falsy → null.
|
|
443
|
+
*/
|
|
444
|
+
export function resolveZoom(
|
|
445
|
+
prop: boolean | ZoomConfig | undefined,
|
|
446
|
+
): ResolvedZoomConfig | null {
|
|
447
|
+
return resolveToggle(prop, ZOOM_DEFAULTS, false);
|
|
448
|
+
}
|
|
449
|
+
|
|
365
450
|
const AXIS_LABEL_DEFAULTS: ResolvedAxisLabelConfig = {
|
|
366
451
|
format: undefined,
|
|
367
452
|
color: undefined,
|
|
@@ -496,6 +581,42 @@ export function resolveScrubAction(
|
|
|
496
581
|
return resolveToggle(prop, SCRUB_ACTION_DEFAULTS, false);
|
|
497
582
|
}
|
|
498
583
|
|
|
584
|
+
/** Spacing + collapse-threshold defaults for `markerCluster: "stacked"`. */
|
|
585
|
+
const MARKER_CLUSTER_GAP = 2;
|
|
586
|
+
const MARKER_CLUSTER_MAX_BEFORE_GROUP = 5;
|
|
587
|
+
const MARKER_CLUSTER_OVERLAP = 0.75;
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Resolves the `markerCluster` prop (a `"anchored"`/`"stacked"` shorthand or a
|
|
591
|
+
* {@link MarkerClusterConfig} object) to a full config. Always returns a config
|
|
592
|
+
* (never null) — `"anchored"` (the default) is a valid mode the cluster pass
|
|
593
|
+
* treats as "side offsets only, no bucketing". The object form implies
|
|
594
|
+
* `"stacked"` unless `mode` is set.
|
|
595
|
+
*/
|
|
596
|
+
export function resolveMarkerCluster(
|
|
597
|
+
prop: "anchored" | "stacked" | MarkerClusterConfig | undefined,
|
|
598
|
+
): ResolvedMarkerCluster {
|
|
599
|
+
if (typeof prop === "object") {
|
|
600
|
+
return {
|
|
601
|
+
mode: prop.mode ?? "stacked",
|
|
602
|
+
overlap: clamp01(prop.overlap ?? MARKER_CLUSTER_OVERLAP),
|
|
603
|
+
gap: MARKER_CLUSTER_GAP,
|
|
604
|
+
maxBeforeGroup: prop.maxBeforeGroup ?? MARKER_CLUSTER_MAX_BEFORE_GROUP,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
mode: prop === "stacked" ? "stacked" : "anchored",
|
|
609
|
+
overlap: MARKER_CLUSTER_OVERLAP,
|
|
610
|
+
gap: MARKER_CLUSTER_GAP,
|
|
611
|
+
maxBeforeGroup: MARKER_CLUSTER_MAX_BEFORE_GROUP,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Clamp to [0, 0.95] so a glyph never fully covers its neighbor (overlap 1). */
|
|
616
|
+
function clamp01(v: number): number {
|
|
617
|
+
return v < 0 ? 0 : v > 0.95 ? 0.95 : v;
|
|
618
|
+
}
|
|
619
|
+
|
|
499
620
|
const GRADIENT_DEFAULTS: ResolvedGradientConfig = {
|
|
500
621
|
topOpacity: undefined,
|
|
501
622
|
bottomOpacity: undefined,
|
|
@@ -27,6 +27,13 @@ export interface EngineConfig {
|
|
|
27
27
|
exaggerate?: boolean;
|
|
28
28
|
referenceValue?: number;
|
|
29
29
|
referenceValues?: number[];
|
|
30
|
+
/**
|
|
31
|
+
* Live, per-frame Y values to fold into the axis-range fit on top of the static
|
|
32
|
+
* {@link referenceValues} — used so a *dragging* reference line expands the range
|
|
33
|
+
* and the axis follows the finger smoothly (the committed values already sit in
|
|
34
|
+
* `referenceValues`). Read on the UI thread each frame; omit for non-draggable charts.
|
|
35
|
+
*/
|
|
36
|
+
liveReferenceValues?: SharedValue<number[]>;
|
|
30
37
|
nonNegative?: boolean;
|
|
31
38
|
maxValue?: number;
|
|
32
39
|
nowOverride?: number;
|
|
@@ -50,12 +57,14 @@ export interface ChartEngineLayout {
|
|
|
50
57
|
/** Animating window (lerps toward {@link timeWindow}); use for positioning. */
|
|
51
58
|
displayWindow: SharedValue<number>;
|
|
52
59
|
/**
|
|
53
|
-
* The target window
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
60
|
+
* The *effective* target window — the value `displayWindow` eases toward: the
|
|
61
|
+
* pinch-zoom override ({@link ChartEngineScroll.viewWindow}) when active, else
|
|
62
|
+
* the `timeWindow` prop. Tick *selection* (which X-axis labels exist) must read
|
|
63
|
+
* this, not `displayWindow`: the lerp is asymptotic and never reaches the
|
|
64
|
+
* target, so it settles just above or just below it depending on the prior
|
|
65
|
+
* window. Bucketing that off-by-epsilon value (e.g. via `niceTimeInterval`)
|
|
66
|
+
* would otherwise make the tick cadence depend on where you came from. See
|
|
67
|
+
* issue #126.
|
|
59
68
|
*/
|
|
60
69
|
timeWindow: SharedValue<number>;
|
|
61
70
|
canvasWidth: SharedValue<number>;
|
|
@@ -76,6 +85,42 @@ export interface ChartEngineExtrema {
|
|
|
76
85
|
extremaMaxTime: SharedValue<number>;
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Time-scroll state: lets a pan gesture freeze the window at an absolute time
|
|
90
|
+
* and resume following the live edge. Returned by both engines (intersected onto
|
|
91
|
+
* the state) but kept off {@link ChartEngineLayout} so overlay components — which
|
|
92
|
+
* only read the layout — don't have to carry it.
|
|
93
|
+
*/
|
|
94
|
+
export interface ChartEngineScroll {
|
|
95
|
+
/**
|
|
96
|
+
* Absolute right-edge time (unix seconds) to freeze at, or `null` to follow
|
|
97
|
+
* the live edge. Written by the pan-scroll gesture; read by the engine tick.
|
|
98
|
+
*/
|
|
99
|
+
viewEnd: SharedValue<number | null>;
|
|
100
|
+
/**
|
|
101
|
+
* The right-edge time the engine would use if following live — advances each
|
|
102
|
+
* frame even while {@link ChartEngineLayout.timestamp} is frozen by a pan.
|
|
103
|
+
*/
|
|
104
|
+
liveEdge: SharedValue<number>;
|
|
105
|
+
/**
|
|
106
|
+
* Absolute visible-window width (seconds) to freeze at, or `null` to follow
|
|
107
|
+
* the configured `timeWindow`. Written by the pinch-zoom gesture; the
|
|
108
|
+
* symmetric counterpart of {@link viewEnd} (width vs. right edge). Folded into
|
|
109
|
+
* {@link ChartEngineLayout.timeWindow}, so downstream selection/positioning
|
|
110
|
+
* picks up the zoom for free.
|
|
111
|
+
*/
|
|
112
|
+
viewWindow: SharedValue<number | null>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Single-series only: smoothed price at the visible window's right edge. */
|
|
116
|
+
export interface ChartEngineEdge {
|
|
117
|
+
/**
|
|
118
|
+
* Smoothed value at the window's right edge — the live value while following,
|
|
119
|
+
* the price at `viewEnd` while scrolled back. For a `followViewEdge` badge.
|
|
120
|
+
*/
|
|
121
|
+
edgeValue: SharedValue<number>;
|
|
122
|
+
}
|
|
123
|
+
|
|
79
124
|
export interface SingleEngineState extends ChartEngineLayout, ChartEngineExtrema {
|
|
80
125
|
data: SharedValue<LiveChartPoint[]>;
|
|
81
126
|
value: SharedValue<number>;
|
|
@@ -119,6 +164,14 @@ export interface EngineFrameRefs {
|
|
|
119
164
|
nowOverrideSV?: SharedValue<number | undefined>;
|
|
120
165
|
windowBufferSV?: SharedValue<number>;
|
|
121
166
|
pausedSV: SharedValue<boolean>;
|
|
167
|
+
/** Pan-scroll right-edge override (null = follow live). Optional for callers/tests. */
|
|
168
|
+
viewEndSV?: SharedValue<number | null>;
|
|
169
|
+
/** Pinch-zoom window-width override (null = follow timeWindow). Optional for callers/tests. */
|
|
170
|
+
viewWindowSV?: SharedValue<number | null>;
|
|
171
|
+
/** Receives the computed live edge each frame. Optional for callers/tests. */
|
|
172
|
+
liveEdgeSV?: SharedValue<number>;
|
|
173
|
+
/** Receives the smoothed right-edge value each frame. Optional for callers/tests. */
|
|
174
|
+
edgeValueSV?: SharedValue<number>;
|
|
122
175
|
modeSV: SharedValue<"line" | "candle">;
|
|
123
176
|
candles?: SharedValue<CandlePoint[]>;
|
|
124
177
|
liveCandle?: SharedValue<CandlePoint | null>;
|
|
@@ -144,6 +197,8 @@ export function applyLiveChartEngineFrame(
|
|
|
144
197
|
displayMax: sv.displayMax.value,
|
|
145
198
|
displayWindow: sv.displayWindow.value,
|
|
146
199
|
timestamp: sv.timestamp.value,
|
|
200
|
+
liveEdge: sv.liveEdgeSV?.value ?? 0,
|
|
201
|
+
edgeValue: sv.edgeValueSV?.value ?? 0,
|
|
147
202
|
extremaMinValue: sv.extremaMinValue.value,
|
|
148
203
|
extremaMaxValue: sv.extremaMaxValue.value,
|
|
149
204
|
extremaMinTime: sv.extremaMinTime.value,
|
|
@@ -167,6 +222,8 @@ export function applyLiveChartEngineFrame(
|
|
|
167
222
|
points: sv.data.value,
|
|
168
223
|
nowSeconds: Date.now() / 1000,
|
|
169
224
|
paused: sv.pausedSV.value,
|
|
225
|
+
viewEnd: sv.viewEndSV?.value,
|
|
226
|
+
viewWindow: sv.viewWindowSV?.value,
|
|
170
227
|
mode: sv.modeSV.value,
|
|
171
228
|
candles: sv.candles?.value,
|
|
172
229
|
liveCandle: sv.liveCandle?.value,
|
|
@@ -176,15 +233,27 @@ export function applyLiveChartEngineFrame(
|
|
|
176
233
|
sv.displayMax.value = state.displayMax;
|
|
177
234
|
sv.displayWindow.value = state.displayWindow;
|
|
178
235
|
sv.timestamp.value = state.timestamp;
|
|
236
|
+
if (sv.liveEdgeSV) sv.liveEdgeSV.value = state.liveEdge;
|
|
237
|
+
if (sv.edgeValueSV) sv.edgeValueSV.value = state.edgeValue;
|
|
179
238
|
sv.extremaMinValue.value = state.extremaMinValue;
|
|
180
239
|
sv.extremaMaxValue.value = state.extremaMaxValue;
|
|
181
240
|
sv.extremaMinTime.value = state.extremaMinTime;
|
|
182
241
|
sv.extremaMaxTime.value = state.extremaMaxTime;
|
|
183
242
|
}
|
|
184
243
|
|
|
185
|
-
export function useLiveChartEngine(
|
|
186
|
-
|
|
187
|
-
|
|
244
|
+
export function useLiveChartEngine(
|
|
245
|
+
config: EngineConfig,
|
|
246
|
+
): SingleEngineState & ChartEngineScroll & ChartEngineEdge {
|
|
247
|
+
// Pinch-zoom window-width override (null = follow the configured window).
|
|
248
|
+
// Declared first so `timeWindow` below can fold it in. Defaults to null so
|
|
249
|
+
// charts without `zoom` behave exactly as before.
|
|
250
|
+
const viewWindow = useSharedValue<number | null>(null);
|
|
251
|
+
|
|
252
|
+
// Low-frequency config → UI thread via useDerivedValue. `timeWindow` is the
|
|
253
|
+
// *effective* target window: the zoom override when set, else the prop. Both
|
|
254
|
+
// the tick's window lerp and the X-axis tick selection read it, so zoom flows
|
|
255
|
+
// downstream for free (mirrors how viewEnd drives `timestamp`).
|
|
256
|
+
const timeWindow = useDerivedValue(() => viewWindow.value ?? config.timeWindow);
|
|
188
257
|
// Static charts snap to their target in one tick (smoothing=1), so the single
|
|
189
258
|
// settle reaction below produces the final state with no per-frame easing.
|
|
190
259
|
const smoothing = useDerivedValue(() =>
|
|
@@ -193,7 +262,15 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
193
262
|
const adaptiveSpeedBoostSV = useDerivedValue(() => config.adaptiveSpeedBoost);
|
|
194
263
|
const exaggerateSV = useDerivedValue(() => config.exaggerate ?? false);
|
|
195
264
|
const referenceValue = useDerivedValue(() => config.referenceValue);
|
|
196
|
-
|
|
265
|
+
// Captured directly (not via `config.`) so Reanimated tracks the SharedValue and
|
|
266
|
+
// the merge re-runs each frame while a line is dragged; stable when idle.
|
|
267
|
+
const liveReferenceValuesSV = config.liveReferenceValues;
|
|
268
|
+
const referenceValues = useDerivedValue(() => {
|
|
269
|
+
const base = config.referenceValues;
|
|
270
|
+
const live = liveReferenceValuesSV?.value;
|
|
271
|
+
if (!live || live.length === 0) return base;
|
|
272
|
+
return base && base.length > 0 ? base.concat(live) : live;
|
|
273
|
+
});
|
|
197
274
|
const nonNegativeSV = useDerivedValue(() => config.nonNegative ?? false);
|
|
198
275
|
const maxValueSV = useDerivedValue(() => config.maxValue);
|
|
199
276
|
const nowOverrideSV = useDerivedValue(() => config.nowOverride);
|
|
@@ -212,6 +289,14 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
212
289
|
const [initialTimestamp] = useState(() => Date.now() / 1000);
|
|
213
290
|
const timestamp = useSharedValue(initialTimestamp);
|
|
214
291
|
|
|
292
|
+
// Pan-scroll state. `viewEnd` null = follow live; a number freezes the right
|
|
293
|
+
// edge at that absolute time. `liveEdge` mirrors the would-be-live timestamp
|
|
294
|
+
// each frame so the gesture can clamp / detect catch-up. Both default to
|
|
295
|
+
// "following" so charts without `timeScroll` behave exactly as before.
|
|
296
|
+
const viewEnd = useSharedValue<number | null>(null);
|
|
297
|
+
const liveEdge = useSharedValue(initialTimestamp);
|
|
298
|
+
const edgeValue = useSharedValue(0);
|
|
299
|
+
|
|
215
300
|
// Live data extrema (value + time of the visible high / low). NaN until the
|
|
216
301
|
// first tick finds data — the extrema label stays hidden until then.
|
|
217
302
|
const extremaMinValue = useSharedValue(NaN);
|
|
@@ -250,6 +335,10 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
250
335
|
nowOverrideSV,
|
|
251
336
|
windowBufferSV,
|
|
252
337
|
pausedSV,
|
|
338
|
+
viewEndSV: viewEnd,
|
|
339
|
+
viewWindowSV: viewWindow,
|
|
340
|
+
liveEdgeSV: liveEdge,
|
|
341
|
+
edgeValueSV: edgeValue,
|
|
253
342
|
modeSV,
|
|
254
343
|
candles,
|
|
255
344
|
liveCandle,
|
|
@@ -307,6 +396,10 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
307
396
|
canvasWidth,
|
|
308
397
|
canvasHeight,
|
|
309
398
|
timestamp,
|
|
399
|
+
viewEnd,
|
|
400
|
+
viewWindow,
|
|
401
|
+
liveEdge,
|
|
402
|
+
edgeValue,
|
|
310
403
|
extremaMinValue,
|
|
311
404
|
extremaMaxValue,
|
|
312
405
|
extremaMinTime,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { MS_PER_FRAME_60FPS } from "../constants";
|
|
9
9
|
import type { LiveChartPoint, SeriesConfig } from "../types";
|
|
10
10
|
import { tickLiveChartSeriesEngineFrame } from "./liveChartSeriesEngineTick";
|
|
11
|
-
import type { MultiEngineState } from "./useLiveChartEngine";
|
|
11
|
+
import type { ChartEngineScroll, MultiEngineState } from "./useLiveChartEngine";
|
|
12
12
|
|
|
13
13
|
export interface MultiSeriesEngineConfig {
|
|
14
14
|
series: SharedValue<SeriesConfig[]>;
|
|
@@ -47,6 +47,12 @@ export interface MultiEngineFrameRefs {
|
|
|
47
47
|
nowOverrideSV?: SharedValue<number | undefined>;
|
|
48
48
|
windowBufferSV?: SharedValue<number>;
|
|
49
49
|
pausedSV: SharedValue<boolean>;
|
|
50
|
+
/** Pan-scroll right-edge override (null = follow live). Optional for callers/tests. */
|
|
51
|
+
viewEndSV?: SharedValue<number | null>;
|
|
52
|
+
/** Pinch-zoom window-width override (null = follow timeWindow). Optional for callers/tests. */
|
|
53
|
+
viewWindowSV?: SharedValue<number | null>;
|
|
54
|
+
/** Receives the computed live edge each frame. Optional for callers/tests. */
|
|
55
|
+
liveEdgeSV?: SharedValue<number>;
|
|
50
56
|
extremaMinValue: SharedValue<number>;
|
|
51
57
|
extremaMaxValue: SharedValue<number>;
|
|
52
58
|
extremaMinTime: SharedValue<number>;
|
|
@@ -105,6 +111,7 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
105
111
|
displayMax: sv.displayMax.value,
|
|
106
112
|
displayWindow: sv.displayWindow.value,
|
|
107
113
|
timestamp: sv.timestamp.value,
|
|
114
|
+
liveEdge: sv.liveEdgeSV?.value ?? 0,
|
|
108
115
|
displayValues,
|
|
109
116
|
opacities,
|
|
110
117
|
extremaMinValue: sv.extremaMinValue.value,
|
|
@@ -129,11 +136,14 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
129
136
|
series: seriesSnap,
|
|
130
137
|
nowSeconds: Date.now() / 1000,
|
|
131
138
|
paused: sv.pausedSV.value,
|
|
139
|
+
viewEnd: sv.viewEndSV?.value,
|
|
140
|
+
viewWindow: sv.viewWindowSV?.value,
|
|
132
141
|
});
|
|
133
142
|
sv.displayMin.value = state.displayMin;
|
|
134
143
|
sv.displayMax.value = state.displayMax;
|
|
135
144
|
sv.displayWindow.value = state.displayWindow;
|
|
136
145
|
sv.timestamp.value = state.timestamp;
|
|
146
|
+
if (sv.liveEdgeSV) sv.liveEdgeSV.value = state.liveEdge;
|
|
137
147
|
sv.displaySeriesValues.value = state.displayValues;
|
|
138
148
|
sv.seriesOpacities.value = state.opacities;
|
|
139
149
|
sv.extremaMinValue.value = state.extremaMinValue;
|
|
@@ -148,8 +158,11 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
148
158
|
*/
|
|
149
159
|
export function useLiveChartSeriesEngine(
|
|
150
160
|
config: MultiSeriesEngineConfig,
|
|
151
|
-
): MultiEngineState {
|
|
152
|
-
|
|
161
|
+
): MultiEngineState & ChartEngineScroll {
|
|
162
|
+
// Pinch-zoom window-width override (null = follow the configured window).
|
|
163
|
+
// Declared first so `timeWindow` folds it in — see useLiveChartEngine.
|
|
164
|
+
const viewWindow = useSharedValue<number | null>(null);
|
|
165
|
+
const timeWindow = useDerivedValue(() => viewWindow.value ?? config.timeWindow);
|
|
153
166
|
const smoothing = useDerivedValue(() => config.smoothing);
|
|
154
167
|
const adaptiveSpeedBoostSV = useDerivedValue(() => config.adaptiveSpeedBoost);
|
|
155
168
|
const exaggerateSV = useDerivedValue(() => config.exaggerate ?? false);
|
|
@@ -170,6 +183,10 @@ export function useLiveChartSeriesEngine(
|
|
|
170
183
|
const [initialTimestamp] = useState(() => Date.now() / 1000);
|
|
171
184
|
const timestamp = useSharedValue(initialTimestamp);
|
|
172
185
|
|
|
186
|
+
// Pan-scroll state (see useLiveChartEngine). Defaults to following live.
|
|
187
|
+
const viewEnd = useSharedValue<number | null>(null);
|
|
188
|
+
const liveEdge = useSharedValue(initialTimestamp);
|
|
189
|
+
|
|
173
190
|
const displaySeriesValues = useSharedValue<number[]>([]);
|
|
174
191
|
const seriesOpacities = useSharedValue<number[]>([]);
|
|
175
192
|
|
|
@@ -217,6 +234,9 @@ export function useLiveChartSeriesEngine(
|
|
|
217
234
|
nowOverrideSV,
|
|
218
235
|
windowBufferSV,
|
|
219
236
|
pausedSV,
|
|
237
|
+
viewEndSV: viewEnd,
|
|
238
|
+
viewWindowSV: viewWindow,
|
|
239
|
+
liveEdgeSV: liveEdge,
|
|
220
240
|
extremaMinValue,
|
|
221
241
|
extremaMaxValue,
|
|
222
242
|
extremaMinTime,
|
|
@@ -237,6 +257,9 @@ export function useLiveChartSeriesEngine(
|
|
|
237
257
|
canvasWidth,
|
|
238
258
|
canvasHeight,
|
|
239
259
|
timestamp,
|
|
260
|
+
viewEnd,
|
|
261
|
+
viewWindow,
|
|
262
|
+
liveEdge,
|
|
240
263
|
series,
|
|
241
264
|
displaySeriesValues,
|
|
242
265
|
seriesOpacities,
|
package/src/draw/grid.ts
CHANGED
|
@@ -114,7 +114,19 @@ export function computeGridEntries(
|
|
|
114
114
|
// Phase 1: compute target alpha for every grid line
|
|
115
115
|
const targets: Record<number, number> = {};
|
|
116
116
|
const first = Math.ceil(displayMin / fine) * fine;
|
|
117
|
-
|
|
117
|
+
// Guard against a non-advancing loop. On a near-flat (but not bit-identical)
|
|
118
|
+
// range, `fine` can drop below ulp(val), so `val += fine === val` and the loop
|
|
119
|
+
// never terminates — freezing the UI thread, since this runs in a worklet every
|
|
120
|
+
// frame. `stepResolves` skips the degenerate range (also catches fine === 0);
|
|
121
|
+
// MAX_GRID_LINES is a hard backstop in case the step stalls mid-loop (real
|
|
122
|
+
// grids only have a handful of lines, so it never bites normal data).
|
|
123
|
+
const stepResolves = first + fine !== first;
|
|
124
|
+
const MAX_GRID_LINES = 1000;
|
|
125
|
+
for (
|
|
126
|
+
let val = first, lineCount = 0;
|
|
127
|
+
stepResolves && val <= displayMax && lineCount < MAX_GRID_LINES;
|
|
128
|
+
val += fine, lineCount++
|
|
129
|
+
) {
|
|
118
130
|
const y = gridValueToY(val, displayMax, valRange, padTop, chartH);
|
|
119
131
|
/* istanbul ignore next -- y stays in chart band for v in [first, displayMax] with consistent toY */
|
|
120
132
|
if (y < padTop - 2 || y > canvasHeight - padBottom + 2) continue;
|
package/src/draw/markerAtlas.ts
CHANGED
|
@@ -21,11 +21,42 @@ const PILL_TEXT_COLOR = "#ffffff";
|
|
|
21
21
|
/** Anti-alias breathing room baked around every cell so sprites don't clip. */
|
|
22
22
|
const CELL_MARGIN = 2;
|
|
23
23
|
|
|
24
|
-
/**
|
|
24
|
+
/** Glyphs composed into a collapsed-cluster count badge (digits only; capped "99"). */
|
|
25
|
+
const GROUP_CHARS = "0123456789";
|
|
26
|
+
/** Padding (px) between the digits and the inner circle edge. */
|
|
27
|
+
const BADGE_PAD = 2;
|
|
28
|
+
/** Background-colored halo ring around the badge — matches the icon-pill border. */
|
|
29
|
+
const BADGE_BORDER = 2;
|
|
30
|
+
/** Count-badge digits render at this fraction of the chart font, so two digits fit
|
|
31
|
+
* a small round badge. Shared with the overlay's per-frame digit layout. */
|
|
32
|
+
export const BADGE_TEXT_SCALE = 0.75;
|
|
33
|
+
|
|
34
|
+
/** Atlas cell sig for one count-badge glyph (`0`-`9`), drawn white. */
|
|
35
|
+
export function groupGlyphSig(ch: string): string {
|
|
36
|
+
"worklet";
|
|
37
|
+
return `gd\x1f${ch}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Atlas cell sig for the round count-badge background (one fixed circle per color). */
|
|
41
|
+
export function groupBgSig(color: string): string {
|
|
42
|
+
"worklet";
|
|
43
|
+
return `gbg\x1f${color}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Text shown in a collapsed-cluster count badge — exact up to `"99"`, capped there
|
|
47
|
+
* so it always fits the fixed round badge (≤ 2 digits). */
|
|
48
|
+
export function groupCountText(count: number): string {
|
|
49
|
+
"worklet";
|
|
50
|
+
return count > 99 ? "99" : `${count}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Default glyph color per kind when `marker.color` is unset. Worklet-safe so the
|
|
54
|
+
* per-frame overlay worklet can resolve a collapsed cluster's badge color. */
|
|
25
55
|
export function defaultMarkerColor(
|
|
26
56
|
kind: MarkerKind,
|
|
27
57
|
palette: LiveChartPalette,
|
|
28
58
|
): string {
|
|
59
|
+
"worklet";
|
|
29
60
|
switch (kind) {
|
|
30
61
|
case "trade":
|
|
31
62
|
return palette.line;
|
|
@@ -93,9 +124,13 @@ export interface MarkerAtlas {
|
|
|
93
124
|
* hi-res cell back to its logical on-canvas size. See `buildMarkerAtlas`.
|
|
94
125
|
*/
|
|
95
126
|
scale: number;
|
|
127
|
+
/** Logical ink width (px) of each count-badge digit `"0"`–`"9"`, for laying them
|
|
128
|
+
* out proportionally (so narrow digits like `1` don't leave gaps). Empty when
|
|
129
|
+
* the atlas carries no group cells (clustering off). */
|
|
130
|
+
digitWidths: Record<string, number>;
|
|
96
131
|
}
|
|
97
132
|
|
|
98
|
-
const EMPTY_ATLAS: MarkerAtlas = { image: null, cells: {}, scale: 1 };
|
|
133
|
+
const EMPTY_ATLAS: MarkerAtlas = { image: null, cells: {}, scale: 1, digitWidths: {} };
|
|
99
134
|
|
|
100
135
|
function fillPaint(color: string): SkPaint {
|
|
101
136
|
const p = Skia.Paint();
|
|
@@ -260,6 +295,51 @@ function cellSpec(m: Marker, palette: LiveChartPalette, font: SkFont): CellSpec
|
|
|
260
295
|
};
|
|
261
296
|
}
|
|
262
297
|
|
|
298
|
+
/** One count-badge glyph (`0`-`9` / `+`) in white, on a uniform-width box so the
|
|
299
|
+
* digits lay out tabular. `uw`/`gh` are the shared logical glyph metrics. */
|
|
300
|
+
function groupGlyphCellSpec(
|
|
301
|
+
ch: string,
|
|
302
|
+
uw: number,
|
|
303
|
+
gh: number,
|
|
304
|
+
font: SkFont,
|
|
305
|
+
): CellSpec {
|
|
306
|
+
const b = font.measureText(ch);
|
|
307
|
+
const dx = b.x + b.width / 2;
|
|
308
|
+
const dy = b.y + b.height / 2;
|
|
309
|
+
const m2 = CELL_MARGIN * 2;
|
|
310
|
+
return {
|
|
311
|
+
w: uw + m2,
|
|
312
|
+
h: gh + m2,
|
|
313
|
+
draw: (canvas, cx, cy) =>
|
|
314
|
+
canvas.drawText(ch, cx - dx, cy - dy, fillPaint(PILL_TEXT_COLOR), font),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Round count-badge background — a single fixed-size circle (independent of digit
|
|
319
|
+
* count) with a background-colored halo ring like the icon pills. Sized to fit the
|
|
320
|
+
* scaled two-digit text, so the badge stays small, round, and never changes size.
|
|
321
|
+
* `uw`/`gh` are the shared (full-size) glyph metrics; the text is drawn scaled. */
|
|
322
|
+
function groupBgCellSpec(
|
|
323
|
+
color: string,
|
|
324
|
+
bgColor: string,
|
|
325
|
+
uw: number,
|
|
326
|
+
gh: number,
|
|
327
|
+
): CellSpec {
|
|
328
|
+
const m2 = CELL_MARGIN * 2;
|
|
329
|
+
// Inner circle fits a centered two-digit string at BADGE_TEXT_SCALE plus padding.
|
|
330
|
+
const innerR = Math.ceil(Math.max(2 * uw, gh) * BADGE_TEXT_SCALE) / 2 + BADGE_PAD;
|
|
331
|
+
const outerR = innerR + BADGE_BORDER;
|
|
332
|
+
const diameter = 2 * outerR;
|
|
333
|
+
return {
|
|
334
|
+
w: diameter + m2,
|
|
335
|
+
h: diameter + m2,
|
|
336
|
+
draw: (canvas, cx, cy) => {
|
|
337
|
+
canvas.drawCircle(cx, cy, outerR, fillPaint(bgColor)); // halo ring
|
|
338
|
+
canvas.drawCircle(cx, cy, innerR, fillPaint(color)); // colored badge
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
263
343
|
/**
|
|
264
344
|
* Rasterize every distinct marker appearance into a single packed atlas image,
|
|
265
345
|
* once per appearance-set change (NOT per frame). The per-frame worklet then
|
|
@@ -279,6 +359,9 @@ export function buildMarkerAtlas(
|
|
|
279
359
|
palette: LiveChartPalette,
|
|
280
360
|
font: SkFont,
|
|
281
361
|
scale = 1,
|
|
362
|
+
/** Also bake count-badge cells (digits + per-color backgrounds) for
|
|
363
|
+
* `markerCluster: "stacked"` collapse. Off by default — no atlas bloat. */
|
|
364
|
+
withGroups = false,
|
|
282
365
|
): MarkerAtlas {
|
|
283
366
|
const seen = new Set<string>();
|
|
284
367
|
const specs: { sig: string; spec: CellSpec }[] = [];
|
|
@@ -290,6 +373,37 @@ export function buildMarkerAtlas(
|
|
|
290
373
|
seen.add(sig);
|
|
291
374
|
specs.push({ sig, spec: cellSpec(m, palette, font) });
|
|
292
375
|
}
|
|
376
|
+
|
|
377
|
+
// Count-badge cells: 10 uniform white digit cells shared across colors, plus one
|
|
378
|
+
// fixed round background per cluster color. A collapsed group composes its number
|
|
379
|
+
// (centered, ≤ 2 digits) over the circle in the same `drawAtlas`.
|
|
380
|
+
const digitWidths: Record<string, number> = {};
|
|
381
|
+
if (withGroups && specs.length > 0) {
|
|
382
|
+
let uw = 1;
|
|
383
|
+
let gh = 1;
|
|
384
|
+
for (const ch of GROUP_CHARS) {
|
|
385
|
+
const b = font.measureText(ch);
|
|
386
|
+
digitWidths[ch] = Math.max(1, Math.ceil(b.width));
|
|
387
|
+
if (b.width > uw) uw = b.width;
|
|
388
|
+
if (b.height > gh) gh = b.height;
|
|
389
|
+
}
|
|
390
|
+
uw = Math.ceil(uw);
|
|
391
|
+
gh = Math.ceil(gh);
|
|
392
|
+
for (const ch of GROUP_CHARS) {
|
|
393
|
+
specs.push({ sig: groupGlyphSig(ch), spec: groupGlyphCellSpec(ch, uw, gh, font) });
|
|
394
|
+
}
|
|
395
|
+
const bgColor = `rgb(${palette.bgRgb[0]}, ${palette.bgRgb[1]}, ${palette.bgRgb[2]})`;
|
|
396
|
+
const colors = new Set<string>();
|
|
397
|
+
for (let i = 0; i < markers.length; i++) {
|
|
398
|
+
const m = markers[i];
|
|
399
|
+
if (isConnectorMarker(m)) continue;
|
|
400
|
+
colors.add(m.color ?? defaultMarkerColor(m.kind, palette));
|
|
401
|
+
}
|
|
402
|
+
for (const color of colors) {
|
|
403
|
+
specs.push({ sig: groupBgSig(color), spec: groupBgCellSpec(color, bgColor, uw, gh) });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
293
407
|
if (specs.length === 0) return EMPTY_ATLAS;
|
|
294
408
|
|
|
295
409
|
let totalW = 0;
|
|
@@ -326,5 +440,5 @@ export function buildMarkerAtlas(
|
|
|
326
440
|
}
|
|
327
441
|
const picture = recorder.finishRecordingAsPicture();
|
|
328
442
|
const image = drawAsImageFromPicture(picture, { width: texW, height: texH });
|
|
329
|
-
return { image, cells, scale };
|
|
443
|
+
return { image, cells, scale, digitWidths };
|
|
330
444
|
}
|