react-native-livechart 4.20.0 → 4.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AxisLabelOverlay.d.ts +5 -1
- package/dist/components/AxisLabelOverlay.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +4 -2
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/CustomThresholdBadgeOverlay.d.ts +18 -0
- package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +4 -2
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/ExtremaConnectorOverlay.d.ts +5 -1
- package/dist/components/ExtremaConnectorOverlay.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 +4 -2
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts +10 -5
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineSeriesOverlay.d.ts +19 -0
- package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -0
- package/dist/core/chartDataPresence.d.ts +8 -0
- package/dist/core/chartDataPresence.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +6 -1
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +32 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +9 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/candle.d.ts +3 -1
- package/dist/draw/candle.d.ts.map +1 -1
- package/dist/draw/candleGap.d.ts +14 -0
- package/dist/draw/candleGap.d.ts.map +1 -0
- package/dist/draw/lineGap.d.ts +25 -0
- package/dist/draw/lineGap.d.ts.map +1 -0
- package/dist/draw/markerAtlas.d.ts +3 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +7 -3
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useCandleGapPaths.d.ts +12 -0
- package/dist/hooks/useCandleGapPaths.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +4 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useChartReveal.d.ts +1 -1
- package/dist/hooks/useChartReveal.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -5
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useLineGapPaths.d.ts +10 -0
- package/dist/hooks/useLineGapPaths.d.ts.map +1 -0
- package/dist/hooks/useLiveChartHasData.d.ts +2 -2
- package/dist/hooks/useLiveChartHasData.d.ts.map +1 -1
- package/dist/hooks/useReferenceLine.d.ts +4 -3
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLineSeries.d.ts +23 -0
- package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -1
- package/dist/hooks/useThreshold.d.ts +13 -13
- package/dist/hooks/useThreshold.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/candleGaps.d.ts +18 -0
- package/dist/math/candleGaps.d.ts.map +1 -0
- package/dist/math/referenceLineSeries.d.ts +12 -0
- package/dist/math/referenceLineSeries.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +5 -5
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/math/spline.d.ts +5 -1
- package/dist/math/spline.d.ts.map +1 -1
- package/dist/types.d.ts +191 -17
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AxisLabelOverlay.tsx +162 -81
- package/src/components/CrosshairOverlay.tsx +394 -147
- package/src/components/CustomMarkerOverlay.tsx +9 -6
- package/src/components/CustomThresholdBadgeOverlay.tsx +74 -0
- package/src/components/CustomTooltipOverlay.tsx +7 -3
- package/src/components/DegenParticlesOverlay.tsx +34 -23
- package/src/components/ExtremaConnectorOverlay.tsx +43 -3
- package/src/components/LiveChart.tsx +1918 -974
- package/src/components/LiveChartSeries.tsx +445 -296
- package/src/components/MarkerOverlay.tsx +215 -156
- package/src/components/ReferenceLineOverlay.tsx +113 -55
- package/src/components/ReferenceLineSeriesOverlay.tsx +141 -0
- package/src/core/chartDataPresence.ts +24 -0
- package/src/core/liveChartEngineTick.ts +34 -1
- package/src/core/resolveConfig.ts +186 -0
- package/src/core/useLiveChartEngine.ts +36 -1
- package/src/core/useLiveChartSeriesEngine.ts +2 -0
- package/src/draw/candle.ts +1 -1
- package/src/draw/candleGap.ts +132 -0
- package/src/draw/lineGap.ts +180 -0
- package/src/draw/markerAtlas.ts +8 -3
- package/src/hooks/crosshairShared.ts +92 -11
- package/src/hooks/useCandleGapPaths.ts +88 -0
- package/src/hooks/useChartPaths.ts +81 -22
- package/src/hooks/useChartReveal.ts +1 -1
- package/src/hooks/useCrosshair.ts +178 -69
- package/src/hooks/useDegen.ts +82 -53
- package/src/hooks/useLineGapPaths.ts +76 -0
- package/src/hooks/useLiveChartHasData.ts +8 -4
- package/src/hooks/useReferenceLine.ts +6 -5
- package/src/hooks/useReferenceLineSeries.ts +101 -0
- package/src/hooks/useReverseMorphEngineInputs.ts +11 -20
- package/src/hooks/useThreshold.ts +31 -22
- package/src/index.ts +15 -0
- package/src/math/candleGaps.ts +120 -0
- package/src/math/referenceLineSeries.ts +83 -0
- package/src/math/referenceLines.ts +34 -6
- package/src/math/spline.ts +15 -5
- package/src/types.ts +202 -17
|
@@ -18,8 +18,12 @@ import {
|
|
|
18
18
|
type ReferenceLineLayout,
|
|
19
19
|
} from "../hooks/useReferenceLine";
|
|
20
20
|
import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
referenceLineForm,
|
|
23
|
+
resolveReferenceBadge,
|
|
24
|
+
} from "../math/referenceLines";
|
|
22
25
|
import type { FontConfig, LiveChartPalette, ReferenceLine } from "../types";
|
|
26
|
+
import { ReferenceLineSeriesOverlay } from "./ReferenceLineSeriesOverlay";
|
|
23
27
|
|
|
24
28
|
/** Translucent fill alpha for value / time bands. */
|
|
25
29
|
const BAND_FILL_OPACITY = 0.16;
|
|
@@ -33,9 +37,10 @@ const CONNECTOR_GAP = 4;
|
|
|
33
37
|
const BADGE_EDGE_INSET = 2;
|
|
34
38
|
|
|
35
39
|
/**
|
|
36
|
-
* Renders one reference line or band into the chart canvas. Handles all
|
|
37
|
-
* `ReferenceLine` forms (horizontal line,
|
|
38
|
-
* band) plus the Form-A pill badge (in-range tag +
|
|
40
|
+
* Renders one reference line or band into the chart canvas. Handles all four
|
|
41
|
+
* `ReferenceLine` forms (horizontal line, time-varying line, horizontal value
|
|
42
|
+
* band, vertical time band) plus the Form-A pill badge (in-range tag +
|
|
43
|
+
* off-screen chevron pin).
|
|
39
44
|
* Self-contained so callers can `.map()` over a variable-length array.
|
|
40
45
|
*/
|
|
41
46
|
type ReferenceLineOverlayProps = {
|
|
@@ -63,7 +68,7 @@ type ReferenceLineOverlayProps = {
|
|
|
63
68
|
* this line — used when a custom `renderReferenceLine` element owns the tag.
|
|
64
69
|
* The line / band stroke and a badged line's dashed connector still draw. No
|
|
65
70
|
* effect on the base pass.
|
|
66
|
-
|
|
71
|
+
*/
|
|
67
72
|
suppressTag?: boolean;
|
|
68
73
|
/**
|
|
69
74
|
* Suppress the built-in tag only while the line is off-axis. Used by
|
|
@@ -94,7 +99,74 @@ type ReferenceLineOverlayProps = {
|
|
|
94
99
|
gridEndGap?: number;
|
|
95
100
|
};
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Dispatch to the series or scalar/band renderer without mixing their hook
|
|
104
|
+
* lifecycles. A stable `id` may therefore switch forms safely across renders.
|
|
105
|
+
*/
|
|
106
|
+
export function ReferenceLineOverlay(props: ReferenceLineOverlayProps) {
|
|
107
|
+
if (referenceLineForm(props.line) === "series") {
|
|
108
|
+
return (
|
|
109
|
+
<ReferenceLineSeriesOverlay
|
|
110
|
+
engine={props.engine}
|
|
111
|
+
padding={props.padding}
|
|
112
|
+
line={props.line}
|
|
113
|
+
palette={props.palette}
|
|
114
|
+
formatValue={props.formatValue}
|
|
115
|
+
font={props.font}
|
|
116
|
+
badgeLayer={props.badgeLayer ?? false}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return <ReferenceLineStaticOverlay {...props} />;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolveStaticReferenceLineAppearance(
|
|
124
|
+
line: ReferenceLine,
|
|
125
|
+
palette: LiveChartPalette,
|
|
126
|
+
fontProp: FontConfig | undefined,
|
|
127
|
+
) {
|
|
128
|
+
const form = referenceLineForm(line);
|
|
129
|
+
const isBand = form === "value-band" || form === "time-band";
|
|
130
|
+
const color = line.color ?? palette.refLine;
|
|
131
|
+
const badge = resolveReferenceBadge(line);
|
|
132
|
+
const badgeHasFontOverride =
|
|
133
|
+
badge != null &&
|
|
134
|
+
(badge.fontSize != null ||
|
|
135
|
+
badge.fontFamily != null ||
|
|
136
|
+
badge.fontWeight != null);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
form,
|
|
140
|
+
isBand,
|
|
141
|
+
color,
|
|
142
|
+
fillColor: line.fillColor ?? color,
|
|
143
|
+
strokeOpacity: Math.max(0, Math.min(1, line.strokeOpacity ?? 1)),
|
|
144
|
+
badge,
|
|
145
|
+
badgeHasFontOverride,
|
|
146
|
+
badgeFontConfig: badgeHasFontOverride
|
|
147
|
+
? {
|
|
148
|
+
...fontProp,
|
|
149
|
+
fontFamily: badge?.fontFamily ?? fontProp?.fontFamily,
|
|
150
|
+
fontSize: badge?.fontSize ?? fontProp?.fontSize,
|
|
151
|
+
fontWeight: badge?.fontWeight ?? fontProp?.fontWeight,
|
|
152
|
+
}
|
|
153
|
+
: fontProp,
|
|
154
|
+
labelColor:
|
|
155
|
+
badge?.textColor ?? line.labelColor ?? line.color ?? palette.refLabel,
|
|
156
|
+
strokeWidth: line.strokeWidth ?? 1,
|
|
157
|
+
intervals: line.intervals ?? [4, 4],
|
|
158
|
+
bandFillOpacity: line.fillOpacity ?? BAND_FILL_OPACITY,
|
|
159
|
+
hasBandBorder: isBand && line.strokeWidth !== undefined,
|
|
160
|
+
badgeBackground: badge?.background ?? palette.tooltipBg,
|
|
161
|
+
badgeBorderColor: badge?.borderColor ?? color,
|
|
162
|
+
badgeBorderWidth: badge?.borderWidth ?? 1,
|
|
163
|
+
badgeRadius: badge?.radius ?? BADGE_PILL_RADIUS,
|
|
164
|
+
badgeOffsetX: badge?.offsetX ?? 0,
|
|
165
|
+
badgeOffsetY: badge?.offsetY ?? 0,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function ReferenceLineStaticOverlay({
|
|
98
170
|
engine,
|
|
99
171
|
padding,
|
|
100
172
|
line,
|
|
@@ -113,31 +185,30 @@ export function ReferenceLineOverlay({
|
|
|
113
185
|
labelRightMargin,
|
|
114
186
|
gridEndGap,
|
|
115
187
|
}: ReferenceLineOverlayProps) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
188
|
+
const {
|
|
189
|
+
form,
|
|
190
|
+
isBand,
|
|
191
|
+
color,
|
|
192
|
+
fillColor,
|
|
193
|
+
strokeOpacity,
|
|
194
|
+
badge,
|
|
195
|
+
badgeHasFontOverride,
|
|
196
|
+
badgeFontConfig,
|
|
197
|
+
labelColor,
|
|
198
|
+
strokeWidth,
|
|
199
|
+
intervals,
|
|
200
|
+
bandFillOpacity,
|
|
201
|
+
hasBandBorder,
|
|
202
|
+
badgeBackground,
|
|
203
|
+
badgeBorderColor,
|
|
204
|
+
badgeBorderWidth,
|
|
205
|
+
badgeRadius,
|
|
206
|
+
badgeOffsetX,
|
|
207
|
+
badgeOffsetY,
|
|
208
|
+
} = resolveStaticReferenceLineAppearance(line, palette, fontProp);
|
|
123
209
|
|
|
124
|
-
// Per-badge font (size/family/weight) override; reuses the chart `font` when no
|
|
125
|
-
// badge font knob is set, so plain lines and gutter labels are unchanged. The
|
|
126
|
-
// built font drives pill measurement (via useReferenceLine) and the badge text.
|
|
127
|
-
const badgeHasFontOverride =
|
|
128
|
-
badge != null &&
|
|
129
|
-
(badge.fontSize != null ||
|
|
130
|
-
badge.fontFamily != null ||
|
|
131
|
-
badge.fontWeight != null);
|
|
132
210
|
const badgeFontOverride = useChartSkiaFont(
|
|
133
|
-
|
|
134
|
-
? {
|
|
135
|
-
...fontProp,
|
|
136
|
-
fontFamily: badge?.fontFamily ?? fontProp?.fontFamily,
|
|
137
|
-
fontSize: badge?.fontSize ?? fontProp?.fontSize,
|
|
138
|
-
fontWeight: badge?.fontWeight ?? fontProp?.fontWeight,
|
|
139
|
-
}
|
|
140
|
-
: fontProp,
|
|
211
|
+
badgeFontConfig,
|
|
141
212
|
MONO_FONT_FAMILY,
|
|
142
213
|
palette.labelFontSize,
|
|
143
214
|
);
|
|
@@ -157,32 +228,13 @@ export function ReferenceLineOverlay({
|
|
|
157
228
|
font,
|
|
158
229
|
);
|
|
159
230
|
|
|
160
|
-
const labelColor =
|
|
161
|
-
badge?.textColor ?? line.labelColor ?? line.color ?? palette.refLabel;
|
|
162
|
-
const strokeWidth = line.strokeWidth ?? 1;
|
|
163
|
-
const intervals = line.intervals ?? [4, 4];
|
|
164
|
-
|
|
165
|
-
// Band fill + optional dashed border (border only when strokeWidth is set).
|
|
166
|
-
const bandFillOpacity = line.fillOpacity ?? BAND_FILL_OPACITY;
|
|
167
|
-
const hasBandBorder = isBand && line.strokeWidth !== undefined;
|
|
168
|
-
|
|
169
|
-
const badgeBackground = badge?.background ?? palette.tooltipBg;
|
|
170
|
-
const badgeBorderColor = badge?.borderColor ?? color;
|
|
171
|
-
const badgeBorderWidth = badge?.borderWidth ?? 1;
|
|
172
|
-
const badgeRadius = badge?.radius ?? BADGE_PILL_RADIUS;
|
|
173
|
-
// Nudge the whole badge (connector + pill + chevron + icon + label) off its anchor.
|
|
174
|
-
const badgeOffsetX = badge?.offsetX ?? 0;
|
|
175
|
-
const badgeOffsetY = badge?.offsetY ?? 0;
|
|
176
|
-
const badgeTransform =
|
|
177
|
-
badgeOffsetX !== 0 || badgeOffsetY !== 0
|
|
178
|
-
? [{ translateX: badgeOffsetX }, { translateY: badgeOffsetY }]
|
|
179
|
-
: undefined;
|
|
180
|
-
|
|
181
231
|
if (!badgeLayer) {
|
|
182
232
|
return (
|
|
183
233
|
<ReferenceLineBasePass
|
|
184
234
|
layout={layout}
|
|
185
235
|
color={color}
|
|
236
|
+
fillColor={fillColor}
|
|
237
|
+
strokeOpacity={strokeOpacity}
|
|
186
238
|
strokeWidth={strokeWidth}
|
|
187
239
|
intervals={intervals}
|
|
188
240
|
isBand={isBand}
|
|
@@ -221,6 +273,8 @@ export function ReferenceLineOverlay({
|
|
|
221
273
|
function ReferenceLineBasePass({
|
|
222
274
|
layout,
|
|
223
275
|
color,
|
|
276
|
+
fillColor,
|
|
277
|
+
strokeOpacity,
|
|
224
278
|
strokeWidth,
|
|
225
279
|
intervals,
|
|
226
280
|
isBand,
|
|
@@ -230,6 +284,8 @@ function ReferenceLineBasePass({
|
|
|
230
284
|
}: {
|
|
231
285
|
layout: SharedValue<ReferenceLineLayout>;
|
|
232
286
|
color: string;
|
|
287
|
+
fillColor: string;
|
|
288
|
+
strokeOpacity: number;
|
|
233
289
|
strokeWidth: number;
|
|
234
290
|
intervals: [number, number];
|
|
235
291
|
isBand: boolean;
|
|
@@ -282,20 +338,20 @@ function ReferenceLineBasePass({
|
|
|
282
338
|
});
|
|
283
339
|
const lineOpacity = useDerivedValue(() => {
|
|
284
340
|
const l = layout.get();
|
|
285
|
-
return l.visible && l.drawLine && !isBand ?
|
|
341
|
+
return l.visible && l.drawLine && !isBand ? strokeOpacity : 0;
|
|
286
342
|
});
|
|
287
343
|
const bandOpacity = useDerivedValue(() =>
|
|
288
344
|
layout.get().visible && isBand ? bandFillOpacity : 0,
|
|
289
345
|
);
|
|
290
346
|
const bandBorderOpacity = useDerivedValue(() =>
|
|
291
|
-
layout.get().visible && hasBandBorder ?
|
|
347
|
+
layout.get().visible && hasBandBorder ? strokeOpacity : 0,
|
|
292
348
|
);
|
|
293
349
|
|
|
294
350
|
return (
|
|
295
351
|
<Group>
|
|
296
352
|
{isBand && (
|
|
297
353
|
<Group opacity={bandOpacity}>
|
|
298
|
-
<Path path={bandPath} style="fill" color={
|
|
354
|
+
<Path path={bandPath} style="fill" color={fillColor} />
|
|
299
355
|
</Group>
|
|
300
356
|
)}
|
|
301
357
|
{hasBandBorder && (
|
|
@@ -381,7 +437,7 @@ function ReferenceLineBadgePass({
|
|
|
381
437
|
const customTagActive =
|
|
382
438
|
suppressTag || (suppressTagWhenOffAxis && l.offAxis);
|
|
383
439
|
const customWidth = customTagActive
|
|
384
|
-
? customTagWidths?.get()[index] ?? 0
|
|
440
|
+
? (customTagWidths?.get()[index] ?? 0)
|
|
385
441
|
: 0;
|
|
386
442
|
if (customWidth > 0) {
|
|
387
443
|
if (badgePosition === "left") {
|
|
@@ -435,7 +491,9 @@ function ReferenceLineBadgePass({
|
|
|
435
491
|
const grouped = groupHidden ? groupHidden.get()[index] === true : false;
|
|
436
492
|
const customTagActive =
|
|
437
493
|
suppressTag || (suppressTagWhenOffAxis && l.offAxis);
|
|
438
|
-
return !customTagActive && !grouped && l.visible && l.label.length > 0
|
|
494
|
+
return !customTagActive && !grouped && l.visible && l.label.length > 0
|
|
495
|
+
? 1
|
|
496
|
+
: 0;
|
|
439
497
|
});
|
|
440
498
|
const iconOpacity = useDerivedValue(() => {
|
|
441
499
|
const l = layout.get();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DashPathEffect,
|
|
3
|
+
Group,
|
|
4
|
+
Path,
|
|
5
|
+
Text as SkiaText,
|
|
6
|
+
type SkFont,
|
|
7
|
+
} from "@shopify/react-native-skia";
|
|
8
|
+
import { useDerivedValue } from "react-native-reanimated";
|
|
9
|
+
|
|
10
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
11
|
+
import type { ChartPadding } from "../draw/line";
|
|
12
|
+
import { usePathBuilder } from "../hooks/usePathBuilder";
|
|
13
|
+
import { useReferenceLineSeries } from "../hooks/useReferenceLineSeries";
|
|
14
|
+
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
15
|
+
import { thresholdDashPhase } from "../math/threshold";
|
|
16
|
+
import type { LiveChartPalette, ReferenceLine } from "../types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Draws Form-B `ReferenceLine.series` geometry. The base pass is a clipped Skia
|
|
20
|
+
* polyline behind the chart; the badge pass is the live-edge label above the
|
|
21
|
+
* chart fade, matching the existing two-pass reference-line layering.
|
|
22
|
+
*/
|
|
23
|
+
export function ReferenceLineSeriesOverlay({
|
|
24
|
+
engine,
|
|
25
|
+
padding,
|
|
26
|
+
line,
|
|
27
|
+
palette,
|
|
28
|
+
formatValue,
|
|
29
|
+
font,
|
|
30
|
+
badgeLayer,
|
|
31
|
+
}: {
|
|
32
|
+
engine: ChartEngineLayout;
|
|
33
|
+
padding: ChartPadding;
|
|
34
|
+
line: ReferenceLine;
|
|
35
|
+
palette: LiveChartPalette;
|
|
36
|
+
formatValue: (value: number) => string;
|
|
37
|
+
font: SkFont;
|
|
38
|
+
badgeLayer: boolean;
|
|
39
|
+
}) {
|
|
40
|
+
const points = line.series ?? [];
|
|
41
|
+
const extendToNow = line.extendToNow ?? true;
|
|
42
|
+
const geometry = useReferenceLineSeries(
|
|
43
|
+
engine,
|
|
44
|
+
padding,
|
|
45
|
+
points,
|
|
46
|
+
extendToNow,
|
|
47
|
+
);
|
|
48
|
+
const color = line.color ?? palette.refLine;
|
|
49
|
+
const labelColor = line.labelColor ?? line.color ?? palette.refLabel;
|
|
50
|
+
const strokeOpacity = Math.max(0, Math.min(1, line.strokeOpacity ?? 1));
|
|
51
|
+
const strokeWidth = line.strokeWidth ?? 1;
|
|
52
|
+
const intervals = line.intervals ?? [4, 4];
|
|
53
|
+
const dashCycle = intervals[0] + intervals[1];
|
|
54
|
+
|
|
55
|
+
const builder = usePathBuilder();
|
|
56
|
+
const path = useDerivedValue(() => {
|
|
57
|
+
const b = builder.value;
|
|
58
|
+
const screen = geometry.screenPts.get();
|
|
59
|
+
if (geometry.visible.get() && screen.length >= 4) {
|
|
60
|
+
b.moveTo(screen[0], screen[1]);
|
|
61
|
+
for (let i = 2; i < screen.length; i += 2) {
|
|
62
|
+
b.lineTo(screen[i], screen[i + 1]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return b.detach();
|
|
66
|
+
});
|
|
67
|
+
const pathOpacity = useDerivedValue(() =>
|
|
68
|
+
geometry.visible.get() ? strokeOpacity : 0,
|
|
69
|
+
);
|
|
70
|
+
const dashPhase = useDerivedValue(() =>
|
|
71
|
+
thresholdDashPhase(
|
|
72
|
+
engine.timestamp.get(),
|
|
73
|
+
engine.displayWindow.get(),
|
|
74
|
+
padding.left,
|
|
75
|
+
engine.canvasWidth.get() - padding.right,
|
|
76
|
+
dashCycle,
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
const plotClip = useDerivedValue(() => ({
|
|
80
|
+
x: padding.left,
|
|
81
|
+
y: padding.top,
|
|
82
|
+
width: Math.max(0, engine.canvasWidth.get() - padding.left - padding.right),
|
|
83
|
+
height: Math.max(
|
|
84
|
+
0,
|
|
85
|
+
engine.canvasHeight.get() - padding.top - padding.bottom,
|
|
86
|
+
),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
const metrics = font.getMetrics();
|
|
90
|
+
const baselineOffset = (metrics.ascent + metrics.descent) / 2;
|
|
91
|
+
const labelText = useDerivedValue(() => {
|
|
92
|
+
const value = formatValue(geometry.currentValue.get());
|
|
93
|
+
if (line.label) return line.showValue ? `${line.label} ${value}` : line.label;
|
|
94
|
+
return value;
|
|
95
|
+
});
|
|
96
|
+
const labelX = useDerivedValue(() => {
|
|
97
|
+
const left = padding.left;
|
|
98
|
+
const right = engine.canvasWidth.get() - padding.right;
|
|
99
|
+
const position = line.labelPosition ?? "right";
|
|
100
|
+
if (position === "left") return left + 4;
|
|
101
|
+
if (position === "center") {
|
|
102
|
+
return (left + right) / 2 - measureFontTextWidth(font, labelText.get()) / 2;
|
|
103
|
+
}
|
|
104
|
+
return right + 4;
|
|
105
|
+
});
|
|
106
|
+
const labelY = useDerivedValue(
|
|
107
|
+
() => geometry.currentY.get() - baselineOffset,
|
|
108
|
+
);
|
|
109
|
+
const labelOpacity = useDerivedValue(() =>
|
|
110
|
+
geometry.currentVisible.get() ? 1 : 0,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (badgeLayer) {
|
|
114
|
+
return (
|
|
115
|
+
<Group opacity={labelOpacity}>
|
|
116
|
+
<SkiaText
|
|
117
|
+
x={labelX}
|
|
118
|
+
y={labelY}
|
|
119
|
+
text={labelText}
|
|
120
|
+
font={font}
|
|
121
|
+
color={labelColor}
|
|
122
|
+
/>
|
|
123
|
+
</Group>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<Group clip={plotClip} opacity={pathOpacity}>
|
|
129
|
+
<Path
|
|
130
|
+
path={path}
|
|
131
|
+
style="stroke"
|
|
132
|
+
strokeWidth={strokeWidth}
|
|
133
|
+
strokeCap="round"
|
|
134
|
+
strokeJoin="round"
|
|
135
|
+
color={color}
|
|
136
|
+
>
|
|
137
|
+
<DashPathEffect intervals={intervals} phase={dashPhase} />
|
|
138
|
+
</Path>
|
|
139
|
+
</Group>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CandlePoint, LiveChartPoint, SeriesConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
/** A single historical line sample is enough to anchor the live tip. */
|
|
4
|
+
export function hasLineChartData(points: LiveChartPoint[]): boolean {
|
|
5
|
+
"worklet";
|
|
6
|
+
return points.length > 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A single committed OHLC bucket is drawable as a candle. */
|
|
10
|
+
export function hasCandleChartData(
|
|
11
|
+
candles: CandlePoint[] | undefined,
|
|
12
|
+
): boolean {
|
|
13
|
+
"worklet";
|
|
14
|
+
return (candles?.length ?? 0) > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Multi-series charts are non-empty when any series has one sample. */
|
|
18
|
+
export function hasMultiSeriesChartData(series: SeriesConfig[]): boolean {
|
|
19
|
+
"worklet";
|
|
20
|
+
for (let i = 0; i < series.length; i++) {
|
|
21
|
+
if (series[i].data.length > 0) return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { CandlePoint, LiveChartPoint } from "../types";
|
|
1
|
+
import type { CandleGap, CandlePoint, LiveChartPoint } from "../types";
|
|
2
2
|
|
|
3
3
|
import { MOTION_METRICS_DEFAULTS } from "../constants";
|
|
4
4
|
import { lerp } from "../math/lerp";
|
|
5
5
|
import { thresholdRangeMinMax } from "../math/threshold";
|
|
6
|
+
import { previousCandleCloseAtTime } from "../math/candleGaps";
|
|
6
7
|
|
|
7
8
|
/** Scratch for the threshold range fold — only alive within one tick call. */
|
|
8
9
|
const THRESHOLD_RANGE_SCRATCH: [number, number] = [0, 0];
|
|
@@ -131,6 +132,11 @@ export interface EngineTickInput {
|
|
|
131
132
|
candles?: CandlePoint[];
|
|
132
133
|
/** In-progress candle. Included in Y range when mode is `"candle"`. */
|
|
133
134
|
liveCandle?: CandlePoint | null;
|
|
135
|
+
/** Explicit semantic gaps used only for previous-close range anchoring. */
|
|
136
|
+
candleGaps?: CandleGap[];
|
|
137
|
+
candleGapBridgeNoTrades?: boolean;
|
|
138
|
+
candleGapBridgeUnavailable?: boolean;
|
|
139
|
+
candleGapBridgeUnknown?: boolean;
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
/**
|
|
@@ -303,6 +309,33 @@ export function tickLiveChartEngineFrame(
|
|
|
303
309
|
state.extremaMinTime = hasMin ? minTime : NaN;
|
|
304
310
|
state.extremaMaxTime = hasMax ? maxTime : NaN;
|
|
305
311
|
|
|
312
|
+
// A previous-close bridge is presentation, not a traded observation: include
|
|
313
|
+
// its anchor in the fitted range (so a viewport parked wholly inside an outage
|
|
314
|
+
// stays framed correctly) only after snapshotting the real-data extrema above.
|
|
315
|
+
if (input.mode === "candle" && input.candleGaps) {
|
|
316
|
+
const cs = input.candles ?? [];
|
|
317
|
+
const lc = input.liveCandle ?? null;
|
|
318
|
+
for (let i = 0; i < input.candleGaps.length; i++) {
|
|
319
|
+
const gap = input.candleGaps[i];
|
|
320
|
+
if (gap.from >= state.timestamp || gap.to <= winStart) continue;
|
|
321
|
+
const bridge =
|
|
322
|
+
gap.kind === "no-trades"
|
|
323
|
+
? input.candleGapBridgeNoTrades
|
|
324
|
+
: gap.kind === "unavailable"
|
|
325
|
+
? input.candleGapBridgeUnavailable
|
|
326
|
+
: input.candleGapBridgeUnknown;
|
|
327
|
+
if (!bridge) continue;
|
|
328
|
+
const anchor = previousCandleCloseAtTime(
|
|
329
|
+
cs,
|
|
330
|
+
lc,
|
|
331
|
+
Math.min(gap.to, state.timestamp),
|
|
332
|
+
);
|
|
333
|
+
if (anchor === null) continue;
|
|
334
|
+
if (anchor < tMin) tMin = anchor;
|
|
335
|
+
if (anchor > tMax) tMax = anchor;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
306
339
|
// Keep the animated live tip inside the range — but only while it's actually
|
|
307
340
|
// in the window. Scrolled back, the live value sits beyond the frozen right
|
|
308
341
|
// edge; folding it in would pin the Y range to the live price and stop the
|
|
@@ -3,6 +3,12 @@ import type {
|
|
|
3
3
|
AxisLabelConfig,
|
|
4
4
|
BadgeConfig,
|
|
5
5
|
BadgeVariant,
|
|
6
|
+
CandleGap,
|
|
7
|
+
CandleGapBandStyle,
|
|
8
|
+
CandleGapBridgeStyle,
|
|
9
|
+
CandleGapKind,
|
|
10
|
+
CandleGapLabelStyle,
|
|
11
|
+
CandleGapsConfig,
|
|
6
12
|
DegenOptions,
|
|
7
13
|
FontConfig,
|
|
8
14
|
FontWeight,
|
|
@@ -114,6 +120,38 @@ export interface ResolvedVolumeConfig {
|
|
|
114
120
|
opacity: number;
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
export interface ResolvedCandleGapBridgeStyle {
|
|
124
|
+
color: string | undefined;
|
|
125
|
+
opacity: number;
|
|
126
|
+
strokeWidth: number;
|
|
127
|
+
strokeCap: "butt" | "round" | "square";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface ResolvedCandleGapBandStyle {
|
|
131
|
+
fillColor: string | undefined;
|
|
132
|
+
fillOpacity: number;
|
|
133
|
+
borderColor: string | undefined;
|
|
134
|
+
borderOpacity: number;
|
|
135
|
+
borderWidth: number;
|
|
136
|
+
intervals: [number, number];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ResolvedCandleGapLabelStyle {
|
|
140
|
+
color: string | undefined;
|
|
141
|
+
position: "left" | "right";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ResolvedCandleGapStyle {
|
|
145
|
+
bridge: ResolvedCandleGapBridgeStyle | null;
|
|
146
|
+
band: ResolvedCandleGapBandStyle | null;
|
|
147
|
+
label: ResolvedCandleGapLabelStyle | null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ResolvedCandleGapsConfig {
|
|
151
|
+
gaps: CandleGap[];
|
|
152
|
+
styles: Record<CandleGapKind, ResolvedCandleGapStyle>;
|
|
153
|
+
}
|
|
154
|
+
|
|
117
155
|
/** Resolved straight-line styling (connector, etc.). `color: undefined` → caller default. */
|
|
118
156
|
export interface ResolvedLineStyleConfig {
|
|
119
157
|
color: string | undefined;
|
|
@@ -379,6 +417,8 @@ export interface ResolvedThresholdLineConfig {
|
|
|
379
417
|
label: string | undefined;
|
|
380
418
|
/** Label side; `"left"` sits inside the plot (clear of the y-axis gutter). */
|
|
381
419
|
labelPosition: "left" | "right";
|
|
420
|
+
/** Series badge value/Y source; `"last"` preserves the live-edge default. */
|
|
421
|
+
labelAnchor: "first" | "last";
|
|
382
422
|
/** undefined → use palette.refLine (line) / palette.refLabel (label) at render time. */
|
|
383
423
|
color: string | undefined;
|
|
384
424
|
intervals: [number, number];
|
|
@@ -415,6 +455,7 @@ export interface ResolvedThresholdConfig {
|
|
|
415
455
|
const THRESHOLD_LINE_DEFAULTS: ResolvedThresholdLineConfig = {
|
|
416
456
|
label: undefined,
|
|
417
457
|
labelPosition: "left",
|
|
458
|
+
labelAnchor: "last",
|
|
418
459
|
color: undefined,
|
|
419
460
|
intervals: [4, 4],
|
|
420
461
|
strokeWidth: 1,
|
|
@@ -535,6 +576,151 @@ export function resolveVolume(
|
|
|
535
576
|
return resolveToggle(prop, VOLUME_DEFAULTS, false);
|
|
536
577
|
}
|
|
537
578
|
|
|
579
|
+
const CANDLE_GAP_BRIDGE_DEFAULTS: ResolvedCandleGapBridgeStyle = {
|
|
580
|
+
color: undefined,
|
|
581
|
+
opacity: 0.7,
|
|
582
|
+
strokeWidth: 2,
|
|
583
|
+
strokeCap: "round",
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const CANDLE_GAP_BAND_DEFAULTS: ResolvedCandleGapBandStyle = {
|
|
587
|
+
fillColor: undefined,
|
|
588
|
+
fillOpacity: 0.12,
|
|
589
|
+
borderColor: undefined,
|
|
590
|
+
borderOpacity: 1,
|
|
591
|
+
borderWidth: 2,
|
|
592
|
+
intervals: [4, 4],
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const CANDLE_GAP_LABEL_DEFAULTS: ResolvedCandleGapLabelStyle = {
|
|
596
|
+
color: undefined,
|
|
597
|
+
position: "left",
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
function clampOpacity(value: number): number {
|
|
601
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const CANDLE_GAP_STYLE_DEFAULTS: Record<CandleGapKind, ResolvedCandleGapStyle> = {
|
|
605
|
+
"no-trades": {
|
|
606
|
+
bridge: CANDLE_GAP_BRIDGE_DEFAULTS,
|
|
607
|
+
band: null,
|
|
608
|
+
label: null,
|
|
609
|
+
},
|
|
610
|
+
unavailable: {
|
|
611
|
+
bridge: { ...CANDLE_GAP_BRIDGE_DEFAULTS, opacity: 0.55 },
|
|
612
|
+
band: CANDLE_GAP_BAND_DEFAULTS,
|
|
613
|
+
label: CANDLE_GAP_LABEL_DEFAULTS,
|
|
614
|
+
},
|
|
615
|
+
unknown: {
|
|
616
|
+
bridge: null,
|
|
617
|
+
band: {
|
|
618
|
+
...CANDLE_GAP_BAND_DEFAULTS,
|
|
619
|
+
fillOpacity: 0.08,
|
|
620
|
+
borderWidth: 1,
|
|
621
|
+
},
|
|
622
|
+
label: CANDLE_GAP_LABEL_DEFAULTS,
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
function resolveCandleGapBridge(
|
|
627
|
+
override: false | CandleGapBridgeStyle | undefined,
|
|
628
|
+
semanticDefault: ResolvedCandleGapBridgeStyle | null,
|
|
629
|
+
): ResolvedCandleGapBridgeStyle | null {
|
|
630
|
+
if (override === false) return null;
|
|
631
|
+
if (override === undefined) {
|
|
632
|
+
return semanticDefault ? { ...semanticDefault } : null;
|
|
633
|
+
}
|
|
634
|
+
const base = semanticDefault ?? CANDLE_GAP_BRIDGE_DEFAULTS;
|
|
635
|
+
const strokeCap = override.strokeCap;
|
|
636
|
+
return {
|
|
637
|
+
color: override.color ?? base.color,
|
|
638
|
+
opacity: clampOpacity(override.opacity ?? base.opacity),
|
|
639
|
+
strokeWidth: Math.max(0, override.strokeWidth ?? base.strokeWidth),
|
|
640
|
+
strokeCap:
|
|
641
|
+
strokeCap === "butt" || strokeCap === "round" || strokeCap === "square"
|
|
642
|
+
? strokeCap
|
|
643
|
+
: base.strokeCap,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function resolveCandleGapBand(
|
|
648
|
+
override: false | CandleGapBandStyle | undefined,
|
|
649
|
+
semanticDefault: ResolvedCandleGapBandStyle | null,
|
|
650
|
+
): ResolvedCandleGapBandStyle | null {
|
|
651
|
+
if (override === false) return null;
|
|
652
|
+
if (override === undefined) {
|
|
653
|
+
return semanticDefault ? { ...semanticDefault } : null;
|
|
654
|
+
}
|
|
655
|
+
const base = semanticDefault ?? CANDLE_GAP_BAND_DEFAULTS;
|
|
656
|
+
return {
|
|
657
|
+
fillColor: override.fillColor ?? base.fillColor,
|
|
658
|
+
fillOpacity: clampOpacity(override.fillOpacity ?? base.fillOpacity),
|
|
659
|
+
borderColor: override.borderColor ?? base.borderColor,
|
|
660
|
+
borderOpacity: clampOpacity(override.borderOpacity ?? base.borderOpacity),
|
|
661
|
+
borderWidth: Math.max(0, override.borderWidth ?? base.borderWidth),
|
|
662
|
+
intervals: override.intervals ?? base.intervals,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function resolveCandleGapLabel(
|
|
667
|
+
override: false | CandleGapLabelStyle | undefined,
|
|
668
|
+
semanticDefault: ResolvedCandleGapLabelStyle | null,
|
|
669
|
+
): ResolvedCandleGapLabelStyle | null {
|
|
670
|
+
if (override === false) return null;
|
|
671
|
+
if (override === undefined) {
|
|
672
|
+
return semanticDefault ? { ...semanticDefault } : null;
|
|
673
|
+
}
|
|
674
|
+
const base = semanticDefault ?? CANDLE_GAP_LABEL_DEFAULTS;
|
|
675
|
+
return {
|
|
676
|
+
color: override.color ?? base.color,
|
|
677
|
+
position:
|
|
678
|
+
override.position === "left" || override.position === "right"
|
|
679
|
+
? override.position
|
|
680
|
+
: base.position,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** Resolve and defensively normalize explicit candle-gap metadata. */
|
|
685
|
+
export function resolveCandleGaps(
|
|
686
|
+
prop: CandleGap[] | CandleGapsConfig | undefined,
|
|
687
|
+
): ResolvedCandleGapsConfig | null {
|
|
688
|
+
if (prop == null) return null;
|
|
689
|
+
const gaps = (Array.isArray(prop) ? prop : prop.gaps)
|
|
690
|
+
.filter(
|
|
691
|
+
(gap) =>
|
|
692
|
+
Number.isFinite(gap.from) &&
|
|
693
|
+
Number.isFinite(gap.to) &&
|
|
694
|
+
gap.to > gap.from &&
|
|
695
|
+
(gap.kind === "no-trades" ||
|
|
696
|
+
gap.kind === "unavailable" ||
|
|
697
|
+
gap.kind === "unknown"),
|
|
698
|
+
)
|
|
699
|
+
.slice()
|
|
700
|
+
.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
701
|
+
if (gaps.length === 0) return null;
|
|
702
|
+
|
|
703
|
+
const overrides = Array.isArray(prop) ? undefined : prop.styles;
|
|
704
|
+
const resolveStyle = (kind: CandleGapKind): ResolvedCandleGapStyle => {
|
|
705
|
+
const base = CANDLE_GAP_STYLE_DEFAULTS[kind];
|
|
706
|
+
const override = overrides?.[kind];
|
|
707
|
+
return {
|
|
708
|
+
bridge: resolveCandleGapBridge(override?.bridge, base.bridge),
|
|
709
|
+
band: resolveCandleGapBand(override?.band, base.band),
|
|
710
|
+
label: resolveCandleGapLabel(override?.label, base.label),
|
|
711
|
+
};
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
return {
|
|
715
|
+
gaps,
|
|
716
|
+
styles: {
|
|
717
|
+
"no-trades": resolveStyle("no-trades"),
|
|
718
|
+
unavailable: resolveStyle("unavailable"),
|
|
719
|
+
unknown: resolveStyle("unknown"),
|
|
720
|
+
},
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
538
724
|
export interface ResolvedZoomConfig {
|
|
539
725
|
/** Tightest window (max zoom-in), seconds. `undefined` → `timeWindow / 8`. */
|
|
540
726
|
minTimeWindow: number | undefined;
|