react-native-livechart 3.11.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 +18 -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 +8 -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 +12 -4
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.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 +15 -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 +512 -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 +153 -53
- 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 +561 -34
- package/src/components/LiveChartSeries.tsx +110 -5
- package/src/components/MarkerOverlay.tsx +100 -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 +43 -11
- package/src/hooks/useMultiSeriesLinePaths.ts +2 -1
- 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 +54 -9
- 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 +532 -21
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Group,
|
|
3
|
+
RoundedRect,
|
|
4
|
+
Text as SkiaText,
|
|
5
|
+
type SkFont,
|
|
6
|
+
} from "@shopify/react-native-skia";
|
|
7
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
8
|
+
|
|
9
|
+
import type { ChartPadding } from "../draw/line";
|
|
10
|
+
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
11
|
+
import type { ReferenceGrouping } from "../math/referenceGroup";
|
|
12
|
+
import type { ResolvedReferenceGroupBadge } from "../math/referenceLines";
|
|
13
|
+
import type { LiveChartPalette } from "../types";
|
|
14
|
+
|
|
15
|
+
/** Max simultaneous group handles (a fixed Skia slot pool — the per-frame group
|
|
16
|
+
* count varies, so we draw a fixed set of slots and hide the unused ones). */
|
|
17
|
+
const MAX_GROUP_PILLS = 12;
|
|
18
|
+
/** Horizontal padding inside the count pill, in px. */
|
|
19
|
+
const PILL_PAD_X = 6;
|
|
20
|
+
/** Vertical padding inside the count pill, in px. */
|
|
21
|
+
const PILL_PAD_Y = 3;
|
|
22
|
+
/** Pill inset from the anchored plot edge, in px. */
|
|
23
|
+
const EDGE_INSET = 2;
|
|
24
|
+
|
|
25
|
+
interface PillLayout {
|
|
26
|
+
visible: boolean;
|
|
27
|
+
x: number;
|
|
28
|
+
top: number;
|
|
29
|
+
w: number;
|
|
30
|
+
text: string;
|
|
31
|
+
textX: number;
|
|
32
|
+
textY: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const HIDDEN_PILL: PillLayout = {
|
|
36
|
+
visible: false,
|
|
37
|
+
x: 0,
|
|
38
|
+
top: -1,
|
|
39
|
+
w: 0,
|
|
40
|
+
text: "",
|
|
41
|
+
textX: 0,
|
|
42
|
+
textY: -1,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** One collapsed-group count pill (slot `index` of the fixed pool). Reads the
|
|
46
|
+
* cluster at `grouping.groups[index]`; hidden when fewer groups exist this frame. */
|
|
47
|
+
function GroupCountPill({
|
|
48
|
+
grouping,
|
|
49
|
+
index,
|
|
50
|
+
padding,
|
|
51
|
+
canvasWidth,
|
|
52
|
+
font,
|
|
53
|
+
position,
|
|
54
|
+
icon,
|
|
55
|
+
showText,
|
|
56
|
+
format,
|
|
57
|
+
baselineOffset,
|
|
58
|
+
pillH,
|
|
59
|
+
radius,
|
|
60
|
+
borderWidth,
|
|
61
|
+
color,
|
|
62
|
+
background,
|
|
63
|
+
textColor,
|
|
64
|
+
}: {
|
|
65
|
+
grouping: SharedValue<ReferenceGrouping>;
|
|
66
|
+
index: number;
|
|
67
|
+
padding: ChartPadding;
|
|
68
|
+
canvasWidth: SharedValue<number>;
|
|
69
|
+
font: SkFont;
|
|
70
|
+
position: "left" | "center" | "right";
|
|
71
|
+
icon: string;
|
|
72
|
+
showText: boolean;
|
|
73
|
+
format?: (count: number) => string;
|
|
74
|
+
baselineOffset: number;
|
|
75
|
+
pillH: number;
|
|
76
|
+
radius: number;
|
|
77
|
+
borderWidth: number;
|
|
78
|
+
color: string;
|
|
79
|
+
background: string;
|
|
80
|
+
textColor: string;
|
|
81
|
+
}) {
|
|
82
|
+
const layout = useDerivedValue<PillLayout>(() => {
|
|
83
|
+
const gs = grouping.get().groups;
|
|
84
|
+
if (index >= gs.length) return HIDDEN_PILL;
|
|
85
|
+
const g = gs[index];
|
|
86
|
+
const count = showText ? (format ? format(g.count) : String(g.count)) : "";
|
|
87
|
+
const text = icon ? (count ? `${icon} ${count}` : icon) : count;
|
|
88
|
+
const w = measureFontTextWidth(font, text) + PILL_PAD_X * 2;
|
|
89
|
+
// Anchor the pill horizontally to the chosen plot edge (or center).
|
|
90
|
+
const x1 = padding.left;
|
|
91
|
+
const x2 = canvasWidth.get() - padding.right;
|
|
92
|
+
let x: number;
|
|
93
|
+
if (position === "right") x = x2 - EDGE_INSET - w;
|
|
94
|
+
else if (position === "center") x = (x1 + x2) / 2 - w / 2;
|
|
95
|
+
else x = x1 + EDGE_INSET;
|
|
96
|
+
return {
|
|
97
|
+
visible: true,
|
|
98
|
+
x,
|
|
99
|
+
top: g.cy - pillH / 2,
|
|
100
|
+
w,
|
|
101
|
+
text,
|
|
102
|
+
textX: x + PILL_PAD_X,
|
|
103
|
+
textY: g.cy - baselineOffset,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const opacity = useDerivedValue(() => (layout.get().visible ? 1 : 0));
|
|
108
|
+
const x = useDerivedValue(() => layout.get().x);
|
|
109
|
+
const top = useDerivedValue(() => layout.get().top);
|
|
110
|
+
const w = useDerivedValue(() => layout.get().w);
|
|
111
|
+
const text = useDerivedValue(() => layout.get().text);
|
|
112
|
+
const textX = useDerivedValue(() => layout.get().textX);
|
|
113
|
+
const textY = useDerivedValue(() => layout.get().textY);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Group opacity={opacity}>
|
|
117
|
+
<RoundedRect
|
|
118
|
+
x={x}
|
|
119
|
+
y={top}
|
|
120
|
+
width={w}
|
|
121
|
+
height={pillH}
|
|
122
|
+
r={radius}
|
|
123
|
+
color={background}
|
|
124
|
+
/>
|
|
125
|
+
<RoundedRect
|
|
126
|
+
x={x}
|
|
127
|
+
y={top}
|
|
128
|
+
width={w}
|
|
129
|
+
height={pillH}
|
|
130
|
+
r={radius}
|
|
131
|
+
color={color}
|
|
132
|
+
style="stroke"
|
|
133
|
+
strokeWidth={borderWidth}
|
|
134
|
+
/>
|
|
135
|
+
<SkiaText x={textX} y={textY} text={text} font={font} color={textColor} />
|
|
136
|
+
</Group>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Draws the collapsed-group count handles (e.g. a "×3" pill) for reference-line
|
|
142
|
+
* grouping — one pill per multi-line cluster, pinned to the plot at the cluster
|
|
143
|
+
* centroid. The individual tags of clustered lines are suppressed upstream (via
|
|
144
|
+
* `groupHidden`), so a stack of nearby orders reads as one handle. The pill uses the
|
|
145
|
+
* same style/shape config as a per-line badge (`badge`): `position`, `icon`,
|
|
146
|
+
* `text` (showText), `background` / `borderColor` / `borderWidth`, `radius` (corner),
|
|
147
|
+
* `textColor`, per-badge `font`, and `offsetX` / `offsetY`; the `format` fn maps the
|
|
148
|
+
* count to the label. A fixed slot pool keeps the Skia tree stable while the
|
|
149
|
+
* per-frame group count varies.
|
|
150
|
+
*/
|
|
151
|
+
export function ReferenceLineGroupOverlay({
|
|
152
|
+
grouping,
|
|
153
|
+
padding,
|
|
154
|
+
canvasWidth,
|
|
155
|
+
palette,
|
|
156
|
+
font,
|
|
157
|
+
badge,
|
|
158
|
+
format,
|
|
159
|
+
}: {
|
|
160
|
+
grouping: SharedValue<ReferenceGrouping>;
|
|
161
|
+
padding: ChartPadding;
|
|
162
|
+
canvasWidth: SharedValue<number>;
|
|
163
|
+
palette: LiveChartPalette;
|
|
164
|
+
font: SkFont;
|
|
165
|
+
badge: ResolvedReferenceGroupBadge;
|
|
166
|
+
format?: (count: number) => string;
|
|
167
|
+
}) {
|
|
168
|
+
const fm = font.getMetrics();
|
|
169
|
+
const baselineOffset = (fm.ascent + fm.descent) / 2;
|
|
170
|
+
const pillH = fm.descent - fm.ascent + PILL_PAD_Y * 2;
|
|
171
|
+
|
|
172
|
+
// Theme defaults for the unset colors (mirrors the per-line badge resolution).
|
|
173
|
+
const background = badge.background ?? palette.tooltipBg;
|
|
174
|
+
const borderColor = badge.borderColor ?? palette.refLine;
|
|
175
|
+
const textColor = badge.textColor ?? palette.refLabel;
|
|
176
|
+
|
|
177
|
+
const transform =
|
|
178
|
+
badge.offsetX !== 0 || badge.offsetY !== 0
|
|
179
|
+
? [{ translateX: badge.offsetX }, { translateY: badge.offsetY }]
|
|
180
|
+
: undefined;
|
|
181
|
+
|
|
182
|
+
const slots: React.ReactElement[] = [];
|
|
183
|
+
for (let i = 0; i < MAX_GROUP_PILLS; i++) {
|
|
184
|
+
slots.push(
|
|
185
|
+
<GroupCountPill
|
|
186
|
+
key={i}
|
|
187
|
+
grouping={grouping}
|
|
188
|
+
index={i}
|
|
189
|
+
padding={padding}
|
|
190
|
+
canvasWidth={canvasWidth}
|
|
191
|
+
font={font}
|
|
192
|
+
position={badge.position}
|
|
193
|
+
icon={badge.icon}
|
|
194
|
+
showText={badge.showText}
|
|
195
|
+
format={format}
|
|
196
|
+
baselineOffset={baselineOffset}
|
|
197
|
+
pillH={pillH}
|
|
198
|
+
radius={badge.radius}
|
|
199
|
+
borderWidth={badge.borderWidth}
|
|
200
|
+
color={borderColor}
|
|
201
|
+
background={background}
|
|
202
|
+
textColor={textColor}
|
|
203
|
+
/>,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return <Group transform={transform}>{slots}</Group>;
|
|
207
|
+
}
|
|
@@ -6,14 +6,16 @@ import {
|
|
|
6
6
|
Text as SkiaText,
|
|
7
7
|
type SkFont,
|
|
8
8
|
} from "@shopify/react-native-skia";
|
|
9
|
-
import { useDerivedValue } from "react-native-reanimated";
|
|
9
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
10
10
|
|
|
11
11
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
12
12
|
import type { ChartPadding } from "../draw/line";
|
|
13
|
+
import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
|
|
13
14
|
import { usePathBuilder } from "../hooks/usePathBuilder";
|
|
14
15
|
import { useReferenceLine } from "../hooks/useReferenceLine";
|
|
16
|
+
import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
|
|
15
17
|
import { referenceLineForm, resolveReferenceBadge } from "../math/referenceLines";
|
|
16
|
-
import type { LiveChartPalette, ReferenceLine } from "../types";
|
|
18
|
+
import type { FontConfig, LiveChartPalette, ReferenceLine } from "../types";
|
|
17
19
|
|
|
18
20
|
/** Translucent fill alpha for value / time bands. */
|
|
19
21
|
const BAND_FILL_OPACITY = 0.16;
|
|
@@ -35,7 +37,12 @@ export function ReferenceLineOverlay({
|
|
|
35
37
|
palette,
|
|
36
38
|
formatValue,
|
|
37
39
|
font,
|
|
40
|
+
fontProp,
|
|
38
41
|
badgeLayer = false,
|
|
42
|
+
suppressTag = false,
|
|
43
|
+
groupHidden,
|
|
44
|
+
dragValues,
|
|
45
|
+
index = 0,
|
|
39
46
|
}: {
|
|
40
47
|
engine: ChartEngineLayout;
|
|
41
48
|
padding: ChartPadding;
|
|
@@ -43,6 +50,12 @@ export function ReferenceLineOverlay({
|
|
|
43
50
|
palette: LiveChartPalette;
|
|
44
51
|
formatValue: (v: number) => string;
|
|
45
52
|
font: SkFont;
|
|
53
|
+
/**
|
|
54
|
+
* The chart's raw font config — used to build a per-badge font when the line's
|
|
55
|
+
* {@link ReferenceLineBadgeConfig} sets `fontSize` / `fontFamily` / `fontWeight`.
|
|
56
|
+
* When no badge font knob is set, the resolved `font` is used as-is.
|
|
57
|
+
*/
|
|
58
|
+
fontProp?: FontConfig;
|
|
46
59
|
/**
|
|
47
60
|
* Render only the badge + label (`true`) or only the lines / bands (`false`,
|
|
48
61
|
* default). The caller draws the base pass behind the chart content and the
|
|
@@ -50,13 +63,65 @@ export function ReferenceLineOverlay({
|
|
|
50
63
|
* of being erased by the fade's `dstOut` blend.
|
|
51
64
|
*/
|
|
52
65
|
badgeLayer?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Suppress the built-in tag (badge pill + connector + chevron + icon + gutter
|
|
68
|
+
* label) for this line — used when a custom `renderReferenceLine` element owns
|
|
69
|
+
* the tag. The line / band stroke still draws. No effect on the base pass.
|
|
70
|
+
*/
|
|
71
|
+
suppressTag?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Per-frame grouping flags (index-aligned): when this line's slot is `true` it's
|
|
74
|
+
* collapsed into a group count handle, so its tag is suppressed (the line / band
|
|
75
|
+
* stroke still draws).
|
|
76
|
+
*/
|
|
77
|
+
groupHidden?: SharedValue<boolean[]>;
|
|
78
|
+
/** Per-line live value overrides (dragged values), index-aligned with `referenceLines`. */
|
|
79
|
+
dragValues?: SharedValue<number[]>;
|
|
80
|
+
/** This line's index into {@link dragValues}. */
|
|
81
|
+
index?: number;
|
|
53
82
|
}) {
|
|
54
83
|
const form = referenceLineForm(line);
|
|
55
84
|
const isBand = form === "value-band" || form === "time-band";
|
|
56
|
-
const layout = useReferenceLine(engine, padding, line, formatValue, font);
|
|
57
85
|
|
|
58
86
|
const color = line.color ?? palette.refLine;
|
|
59
|
-
|
|
87
|
+
|
|
88
|
+
// Resolved badge appearance (badge config → fallback flat fields → theme).
|
|
89
|
+
const badge = resolveReferenceBadge(line);
|
|
90
|
+
|
|
91
|
+
// Per-badge font (size/family/weight) override; reuses the chart `font` when no
|
|
92
|
+
// badge font knob is set, so plain lines and gutter labels are unchanged. The
|
|
93
|
+
// built font drives pill measurement (via useReferenceLine) and the badge text.
|
|
94
|
+
const badgeHasFontOverride =
|
|
95
|
+
badge != null &&
|
|
96
|
+
(badge.fontSize != null ||
|
|
97
|
+
badge.fontFamily != null ||
|
|
98
|
+
badge.fontWeight != null);
|
|
99
|
+
const badgeFontOverride = useChartSkiaFont(
|
|
100
|
+
badgeHasFontOverride
|
|
101
|
+
? {
|
|
102
|
+
...fontProp,
|
|
103
|
+
fontFamily: badge?.fontFamily ?? fontProp?.fontFamily,
|
|
104
|
+
fontSize: badge?.fontSize ?? fontProp?.fontSize,
|
|
105
|
+
fontWeight: badge?.fontWeight ?? fontProp?.fontWeight,
|
|
106
|
+
}
|
|
107
|
+
: fontProp,
|
|
108
|
+
MONO_FONT_FAMILY,
|
|
109
|
+
palette.labelFontSize,
|
|
110
|
+
);
|
|
111
|
+
const badgeFont = badgeHasFontOverride ? badgeFontOverride : font;
|
|
112
|
+
|
|
113
|
+
const layout = useReferenceLine(
|
|
114
|
+
engine,
|
|
115
|
+
padding,
|
|
116
|
+
line,
|
|
117
|
+
formatValue,
|
|
118
|
+
badgeFont,
|
|
119
|
+
dragValues,
|
|
120
|
+
index,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const labelColor =
|
|
124
|
+
badge?.textColor ?? line.labelColor ?? line.color ?? palette.refLabel;
|
|
60
125
|
const strokeWidth = line.strokeWidth ?? 1;
|
|
61
126
|
const intervals = line.intervals ?? [4, 4];
|
|
62
127
|
|
|
@@ -64,11 +129,17 @@ export function ReferenceLineOverlay({
|
|
|
64
129
|
const bandFillOpacity = line.fillOpacity ?? BAND_FILL_OPACITY;
|
|
65
130
|
const hasBandBorder = isBand && line.strokeWidth !== undefined;
|
|
66
131
|
|
|
67
|
-
// Resolved badge appearance (badge config → fallback flat fields → theme).
|
|
68
|
-
const badge = resolveReferenceBadge(line);
|
|
69
132
|
const badgeBackground = badge?.background ?? palette.tooltipBg;
|
|
70
133
|
const badgeBorderColor = badge?.borderColor ?? color;
|
|
134
|
+
const badgeBorderWidth = badge?.borderWidth ?? 1;
|
|
71
135
|
const badgeRadius = badge?.radius ?? BADGE_PILL_RADIUS;
|
|
136
|
+
// Nudge the whole badge (connector + pill + chevron + icon + label) off its anchor.
|
|
137
|
+
const badgeOffsetX = badge?.offsetX ?? 0;
|
|
138
|
+
const badgeOffsetY = badge?.offsetY ?? 0;
|
|
139
|
+
const badgeTransform =
|
|
140
|
+
badgeOffsetX !== 0 || badgeOffsetY !== 0
|
|
141
|
+
? [{ translateX: badgeOffsetX }, { translateY: badgeOffsetY }]
|
|
142
|
+
: undefined;
|
|
72
143
|
|
|
73
144
|
const lineBuilder = usePathBuilder();
|
|
74
145
|
const bandBuilder = usePathBuilder();
|
|
@@ -79,11 +150,11 @@ export function ReferenceLineOverlay({
|
|
|
79
150
|
const linePath = useDerivedValue(() => {
|
|
80
151
|
const b = lineBuilder.value;
|
|
81
152
|
const l = layout.get();
|
|
82
|
-
// A plain
|
|
83
|
-
//
|
|
84
|
-
if (l.visible &&
|
|
85
|
-
b.moveTo(l.
|
|
86
|
-
b.lineTo(l.
|
|
153
|
+
// A plain horizontal line, drawn at the line extent (full-width when opted in).
|
|
154
|
+
// A non-full-width badge instead draws a pill + connector to the edge, below.
|
|
155
|
+
if (l.visible && l.drawLine && !isBand) {
|
|
156
|
+
b.moveTo(l.lineX1, l.y);
|
|
157
|
+
b.lineTo(l.lineX2, l.y);
|
|
87
158
|
}
|
|
88
159
|
return b.detach();
|
|
89
160
|
});
|
|
@@ -92,10 +163,10 @@ export function ReferenceLineOverlay({
|
|
|
92
163
|
const b = bandBuilder.value;
|
|
93
164
|
const l = layout.get();
|
|
94
165
|
if (l.visible && isBand) {
|
|
95
|
-
b.moveTo(l.
|
|
96
|
-
b.lineTo(l.
|
|
97
|
-
b.lineTo(l.
|
|
98
|
-
b.lineTo(l.
|
|
166
|
+
b.moveTo(l.lineX1, l.y);
|
|
167
|
+
b.lineTo(l.lineX2, l.y);
|
|
168
|
+
b.lineTo(l.lineX2, l.yBottom);
|
|
169
|
+
b.lineTo(l.lineX1, l.yBottom);
|
|
99
170
|
b.close();
|
|
100
171
|
}
|
|
101
172
|
return b.detach();
|
|
@@ -107,16 +178,16 @@ export function ReferenceLineOverlay({
|
|
|
107
178
|
if (l.visible && hasBandBorder) {
|
|
108
179
|
if (form === "time-band") {
|
|
109
180
|
// Vertical edges at the band's left / right.
|
|
110
|
-
b.moveTo(l.
|
|
111
|
-
b.lineTo(l.
|
|
112
|
-
b.moveTo(l.
|
|
113
|
-
b.lineTo(l.
|
|
181
|
+
b.moveTo(l.lineX1, l.y);
|
|
182
|
+
b.lineTo(l.lineX1, l.yBottom);
|
|
183
|
+
b.moveTo(l.lineX2, l.y);
|
|
184
|
+
b.lineTo(l.lineX2, l.yBottom);
|
|
114
185
|
} else {
|
|
115
186
|
// Horizontal edges at the band's top / bottom.
|
|
116
|
-
b.moveTo(l.
|
|
117
|
-
b.lineTo(l.
|
|
118
|
-
b.moveTo(l.
|
|
119
|
-
b.lineTo(l.
|
|
187
|
+
b.moveTo(l.lineX1, l.y);
|
|
188
|
+
b.lineTo(l.lineX2, l.y);
|
|
189
|
+
b.moveTo(l.lineX1, l.yBottom);
|
|
190
|
+
b.lineTo(l.lineX2, l.yBottom);
|
|
120
191
|
}
|
|
121
192
|
}
|
|
122
193
|
return b.detach();
|
|
@@ -155,7 +226,7 @@ export function ReferenceLineOverlay({
|
|
|
155
226
|
|
|
156
227
|
const lineOpacity = useDerivedValue(() => {
|
|
157
228
|
const l = layout.get();
|
|
158
|
-
return l.visible &&
|
|
229
|
+
return l.visible && l.drawLine && !isBand ? 1 : 0;
|
|
159
230
|
});
|
|
160
231
|
const bandOpacity = useDerivedValue(() =>
|
|
161
232
|
layout.get().visible && isBand ? bandFillOpacity : 0,
|
|
@@ -165,12 +236,14 @@ export function ReferenceLineOverlay({
|
|
|
165
236
|
);
|
|
166
237
|
const badgeOpacity = useDerivedValue(() => {
|
|
167
238
|
const l = layout.get();
|
|
168
|
-
|
|
239
|
+
const grouped = groupHidden ? groupHidden.get()[index] === true : false;
|
|
240
|
+
return !suppressTag && !grouped && l.visible && l.badge ? 1 : 0;
|
|
169
241
|
});
|
|
170
242
|
// Text + icon ride in the badge pass too, so they stay crisp above the fade.
|
|
171
243
|
const labelOpacity = useDerivedValue(() => {
|
|
172
244
|
const l = layout.get();
|
|
173
|
-
|
|
245
|
+
const grouped = groupHidden ? groupHidden.get()[index] === true : false;
|
|
246
|
+
return !suppressTag && !grouped && l.visible && l.label.length > 0 ? 1 : 0;
|
|
174
247
|
});
|
|
175
248
|
const iconOpacity = useDerivedValue(() => {
|
|
176
249
|
const l = layout.get();
|
|
@@ -186,7 +259,7 @@ export function ReferenceLineOverlay({
|
|
|
186
259
|
// Font metrics depend only on the (stable) font, so read them once instead of
|
|
187
260
|
// on every frame inside the pill worklet (`getMetrics` allocates + crosses JSI).
|
|
188
261
|
const { ascent: fontAscent, height: pillH } = (() => {
|
|
189
|
-
const fm =
|
|
262
|
+
const fm = badgeFont.getMetrics();
|
|
190
263
|
return {
|
|
191
264
|
ascent: fm.ascent,
|
|
192
265
|
height: fm.descent - fm.ascent + BADGE_PILL_PAD_Y * 2,
|
|
@@ -238,7 +311,7 @@ export function ReferenceLineOverlay({
|
|
|
238
311
|
|
|
239
312
|
{/* Badge pass — connector + pill + chevron + icon, above the left-edge fade. */}
|
|
240
313
|
{badgeLayer && (
|
|
241
|
-
<Group opacity={badgeOpacity}>
|
|
314
|
+
<Group opacity={badgeOpacity} transform={badgeTransform}>
|
|
242
315
|
<Path
|
|
243
316
|
path={connPath}
|
|
244
317
|
style="stroke"
|
|
@@ -263,7 +336,7 @@ export function ReferenceLineOverlay({
|
|
|
263
336
|
r={badgeRadius}
|
|
264
337
|
color={badgeBorderColor}
|
|
265
338
|
style="stroke"
|
|
266
|
-
strokeWidth={
|
|
339
|
+
strokeWidth={badgeBorderWidth}
|
|
267
340
|
/>
|
|
268
341
|
<Path
|
|
269
342
|
path={chevronPath}
|
|
@@ -278,7 +351,7 @@ export function ReferenceLineOverlay({
|
|
|
278
351
|
x={iconX}
|
|
279
352
|
y={labelY}
|
|
280
353
|
text={iconText}
|
|
281
|
-
font={
|
|
354
|
+
font={badgeFont}
|
|
282
355
|
color={labelColor}
|
|
283
356
|
/>
|
|
284
357
|
</Group>
|
|
@@ -287,12 +360,12 @@ export function ReferenceLineOverlay({
|
|
|
287
360
|
|
|
288
361
|
{/* Labels ride in the badge pass too, so they stay crisp above the fade. */}
|
|
289
362
|
{badgeLayer && (
|
|
290
|
-
<Group opacity={labelOpacity}>
|
|
363
|
+
<Group opacity={labelOpacity} transform={badgeTransform}>
|
|
291
364
|
<SkiaText
|
|
292
365
|
x={labelX}
|
|
293
366
|
y={labelY}
|
|
294
367
|
text={labelText}
|
|
295
|
-
font={
|
|
368
|
+
font={badgeFont}
|
|
296
369
|
color={labelColor}
|
|
297
370
|
/>
|
|
298
371
|
</Group>
|
|
@@ -19,12 +19,19 @@ export function XAxisOverlay({
|
|
|
19
19
|
padding,
|
|
20
20
|
palette,
|
|
21
21
|
font,
|
|
22
|
+
volumeBandHeight = 0,
|
|
22
23
|
}: {
|
|
23
24
|
entries: SharedValue<XAxisEntry[]>;
|
|
24
25
|
engine: ChartEngineLayout;
|
|
25
26
|
padding: ChartPadding;
|
|
26
27
|
palette: LiveChartPalette;
|
|
27
28
|
font: SkFont;
|
|
29
|
+
/**
|
|
30
|
+
* Reserved volume-band height (px) folded into `padding.bottom`. The axis line
|
|
31
|
+
* + labels shift back down by this so they stay at the very bottom, below the
|
|
32
|
+
* band, while the price plot above shrinks. `0` = no band (default).
|
|
33
|
+
*/
|
|
34
|
+
volumeBandHeight?: number;
|
|
28
35
|
}) {
|
|
29
36
|
const axisBuilder = usePathBuilder();
|
|
30
37
|
|
|
@@ -33,7 +40,7 @@ export function XAxisOverlay({
|
|
|
33
40
|
const b = axisBuilder.value;
|
|
34
41
|
const w = engine.canvasWidth.get();
|
|
35
42
|
const h = engine.canvasHeight.get();
|
|
36
|
-
const lineY = h - padding.bottom;
|
|
43
|
+
const lineY = h - padding.bottom + volumeBandHeight;
|
|
37
44
|
|
|
38
45
|
// Bottom axis line
|
|
39
46
|
b.moveTo(padding.left, lineY);
|
|
@@ -53,7 +60,7 @@ export function XAxisOverlay({
|
|
|
53
60
|
"worklet";
|
|
54
61
|
const items = entries.get();
|
|
55
62
|
const h = engine.canvasHeight.get();
|
|
56
|
-
const y = h - padding.bottom + LABEL_OFFSET_Y;
|
|
63
|
+
const y = h - padding.bottom + volumeBandHeight + LABEL_OFFSET_Y;
|
|
57
64
|
const n = items.length;
|
|
58
65
|
const out: { x: number; y: number; label: string; alpha: number }[] = [];
|
|
59
66
|
for (let i = 0; i < n; i++) {
|
|
@@ -22,6 +22,8 @@ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
|
22
22
|
import { AnimatedLabel } from "./AnimatedLabel";
|
|
23
23
|
|
|
24
24
|
const MAX_Y_LABELS = 15;
|
|
25
|
+
/** Right margin (px) for floating labels — keeps them just off the canvas edge. */
|
|
26
|
+
const FLOAT_LABEL_RIGHT_MARGIN = 6;
|
|
25
27
|
|
|
26
28
|
export function YAxisOverlay({
|
|
27
29
|
entries,
|
|
@@ -34,6 +36,8 @@ export function YAxisOverlay({
|
|
|
34
36
|
badgeMetrics = BADGE_METRICS_DEFAULTS,
|
|
35
37
|
seriesLabelInset = 0,
|
|
36
38
|
gridStyle,
|
|
39
|
+
variant = "all",
|
|
40
|
+
float = false,
|
|
37
41
|
}: {
|
|
38
42
|
entries: SharedValue<YAxisEntry[]>;
|
|
39
43
|
engine: ChartEngineLayout;
|
|
@@ -50,6 +54,17 @@ export function YAxisOverlay({
|
|
|
50
54
|
seriesLabelInset?: number;
|
|
51
55
|
/** Grid-line styling overrides. Omit for the legacy solid 1px line. */
|
|
52
56
|
gridStyle?: ResolvedGridStyleConfig;
|
|
57
|
+
/**
|
|
58
|
+
* Which parts to draw. `"all"` (default) renders grid lines + labels together.
|
|
59
|
+
* `"grid"` / `"labels"` split them so a chart can draw the grid behind the data
|
|
60
|
+
* and the labels (over a soft fade) on top — see {@link YAxisEdgeFade}.
|
|
61
|
+
*/
|
|
62
|
+
variant?: "all" | "grid" | "labels";
|
|
63
|
+
/**
|
|
64
|
+
* Floating-axis mode: right-align labels at the canvas edge (over a full-width
|
|
65
|
+
* plot) instead of centering them in a reserved gutter. See {@link YAxisConfig.float}.
|
|
66
|
+
*/
|
|
67
|
+
float?: boolean;
|
|
53
68
|
}) {
|
|
54
69
|
const gridColor = gridStyle?.color ?? palette.gridLine;
|
|
55
70
|
const gridWidth = gridStyle?.strokeWidth ?? 1;
|
|
@@ -59,6 +74,7 @@ export function YAxisOverlay({
|
|
|
59
74
|
|
|
60
75
|
const gridLinesPath = useDerivedValue(() => {
|
|
61
76
|
const b = gridBuilder.value;
|
|
77
|
+
if (variant === "labels") return b.detach();
|
|
62
78
|
const items = entries.get();
|
|
63
79
|
const w = engine.canvasWidth.get();
|
|
64
80
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -72,6 +88,7 @@ export function YAxisOverlay({
|
|
|
72
88
|
badgeMetrics.dotGap + badgeTailAndCap(font.getSize(), badgeTail, badgeMetrics);
|
|
73
89
|
|
|
74
90
|
const labelEntries = useDerivedValue(() => {
|
|
91
|
+
if (variant === "grid") return [];
|
|
75
92
|
const items = entries.get();
|
|
76
93
|
const w = engine.canvasWidth.get();
|
|
77
94
|
const fm = font.getMetrics();
|
|
@@ -80,11 +97,13 @@ export function YAxisOverlay({
|
|
|
80
97
|
for (let i = 0; i < items.length; i++) {
|
|
81
98
|
const e = items[i];
|
|
82
99
|
const textW = measureFontTextWidth(font, e.label);
|
|
83
|
-
const x =
|
|
84
|
-
?
|
|
85
|
-
:
|
|
86
|
-
?
|
|
87
|
-
:
|
|
100
|
+
const x = float
|
|
101
|
+
? gutterRightAlignedTextLeftX(w, textW, FLOAT_LABEL_RIGHT_MARGIN)
|
|
102
|
+
: badge
|
|
103
|
+
? pillTextLeftX(w, padding.right, leftInset, textW, badgeMetrics)
|
|
104
|
+
: seriesLabelInset > 0
|
|
105
|
+
? gutterRightAlignedTextLeftX(w, textW)
|
|
106
|
+
: gutterCenteredTextLeftX(w, padding.right, textW);
|
|
88
107
|
result.push({
|
|
89
108
|
x,
|
|
90
109
|
y: e.y - baselineOffset,
|
|
@@ -97,27 +116,30 @@ export function YAxisOverlay({
|
|
|
97
116
|
|
|
98
117
|
return (
|
|
99
118
|
<Group>
|
|
100
|
-
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
{variant !== "labels" && (
|
|
120
|
+
<Group opacity={gridOpacity}>
|
|
121
|
+
<Path
|
|
122
|
+
path={gridLinesPath}
|
|
123
|
+
style="stroke"
|
|
124
|
+
strokeWidth={gridWidth}
|
|
125
|
+
color={gridColor}
|
|
126
|
+
>
|
|
127
|
+
{gridIntervals.length > 0 && (
|
|
128
|
+
<DashPathEffect intervals={gridIntervals} />
|
|
129
|
+
)}
|
|
130
|
+
</Path>
|
|
131
|
+
</Group>
|
|
132
|
+
)}
|
|
133
|
+
{variant !== "grid" &&
|
|
134
|
+
Array.from({ length: MAX_Y_LABELS }, (_, i) => (
|
|
135
|
+
<AnimatedLabel
|
|
136
|
+
key={i}
|
|
137
|
+
entries={labelEntries}
|
|
138
|
+
index={i}
|
|
139
|
+
font={font}
|
|
140
|
+
color={palette.gridLabel}
|
|
141
|
+
/>
|
|
142
|
+
))}
|
|
121
143
|
</Group>
|
|
122
144
|
);
|
|
123
145
|
}
|
package/src/constants.ts
CHANGED
|
@@ -9,6 +9,14 @@ import type {
|
|
|
9
9
|
/** Milliseconds per frame at 60 fps — baseline for frame-rate-independent lerp. */
|
|
10
10
|
export const MS_PER_FRAME_60FPS = 16.67;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Default press-and-hold (ms) before scrub engages in the `holdToScrub`
|
|
14
|
+
* time-scroll mode, so a quick one-finger drag scrolls instead. Overridden by
|
|
15
|
+
* `timeScroll.scrubHoldMs`, then `scrub.panGestureDelay`. Shared by `LiveChart`
|
|
16
|
+
* and `LiveChartSeries`.
|
|
17
|
+
*/
|
|
18
|
+
export const HOLD_TO_SCRUB_MS = 500;
|
|
19
|
+
|
|
12
20
|
// ─── Metric default tokens (single source of truth for LiveChartMetrics) ─────
|
|
13
21
|
// The resolved `metrics` config (see resolveMetrics) is assembled from these
|
|
14
22
|
// objects. Draw/worklet helpers default their metric params to the matching
|
|
@@ -29,6 +37,8 @@ export const CANDLE_METRICS_DEFAULTS: CandleMetrics = {
|
|
|
29
37
|
minBodyPx: 1,
|
|
30
38
|
maxBodyPx: 40,
|
|
31
39
|
bodyWidthRatio: 0.8,
|
|
40
|
+
bodyRadius: 0,
|
|
41
|
+
wickWidth: 1,
|
|
32
42
|
};
|
|
33
43
|
|
|
34
44
|
/** Default grid + axis-label fade speeds. */
|