react-native-livechart 4.12.0 → 4.13.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/CrosshairLine.d.ts +12 -4
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +12 -2
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomTooltipOverlay.d.ts +5 -2
- package/dist/components/CustomTooltipOverlay.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/PerSeriesTooltipOverlay.d.ts +3 -1
- package/dist/components/PerSeriesTooltipOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts +8 -1
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +6 -2
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +16 -0
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/draw/grid.d.ts +13 -0
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +22 -3
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +7 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts +6 -1
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useCrosshairVisibleOpacity.d.ts +8 -0
- package/dist/hooks/useCrosshairVisibleOpacity.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +10 -1
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairLine.tsx +39 -11
- package/src/components/CrosshairOverlay.tsx +49 -15
- package/src/components/CustomTooltipOverlay.tsx +15 -3
- package/src/components/LiveChart.tsx +25 -2
- package/src/components/LiveChartSeries.tsx +83 -16
- package/src/components/PerSeriesTooltipOverlay.tsx +4 -1
- package/src/components/ReferenceLineOverlay.tsx +14 -0
- package/src/components/YAxisOverlay.tsx +34 -9
- package/src/core/resolveConfig.ts +29 -0
- package/src/draw/grid.ts +33 -0
- package/src/hooks/crosshairShared.ts +81 -4
- package/src/hooks/useCrosshair.ts +46 -28
- package/src/hooks/useCrosshairSeries.ts +31 -4
- package/src/hooks/useCrosshairVisibleOpacity.ts +57 -0
- package/src/hooks/useReferenceLine.ts +26 -1
- package/src/types.ts +47 -0
|
@@ -55,6 +55,7 @@ import { useChartReveal } from "../hooks/useChartReveal";
|
|
|
55
55
|
import { useChartOverlayContext } from "../hooks/useChartOverlayContext";
|
|
56
56
|
import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
|
|
57
57
|
import { useCrosshairSeries } from "../hooks/useCrosshairSeries";
|
|
58
|
+
import { useCrosshairVisibleOpacity } from "../hooks/useCrosshairVisibleOpacity";
|
|
58
59
|
import { useMarkers } from "../hooks/useMarkers";
|
|
59
60
|
import { useMultiSeriesDegen } from "../hooks/useMultiSeriesDegen";
|
|
60
61
|
import { useMultiSeriesLinePaths } from "../hooks/useMultiSeriesLinePaths";
|
|
@@ -463,6 +464,7 @@ function useLiveChartSeriesController({
|
|
|
463
464
|
maxTime: tooltipMaxTime,
|
|
464
465
|
}
|
|
465
466
|
: undefined,
|
|
467
|
+
scrubCfg?.clampToPlot ?? false,
|
|
466
468
|
);
|
|
467
469
|
|
|
468
470
|
// Capture only the shared value in the worklets below. Referencing
|
|
@@ -712,6 +714,8 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
712
714
|
badge={false}
|
|
713
715
|
seriesLabelInset={seriesLabelInset}
|
|
714
716
|
gridStyle={gridStyleCfg}
|
|
717
|
+
labelRightMargin={yAxisCfg.labelRightMargin}
|
|
718
|
+
gridEndGap={yAxisCfg.gridEndGap}
|
|
715
719
|
/>
|
|
716
720
|
</Group>
|
|
717
721
|
)}
|
|
@@ -728,6 +732,9 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
728
732
|
palette={palette}
|
|
729
733
|
formatValue={formatValue}
|
|
730
734
|
font={skiaFont}
|
|
735
|
+
yAxisEntries={yAxisEntries}
|
|
736
|
+
labelRightMargin={yAxisCfg?.labelRightMargin}
|
|
737
|
+
gridEndGap={yAxisCfg?.gridEndGap}
|
|
731
738
|
/>
|
|
732
739
|
))}
|
|
733
740
|
</Group>
|
|
@@ -888,6 +895,8 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
888
895
|
palette,
|
|
889
896
|
formatValue,
|
|
890
897
|
skiaFont,
|
|
898
|
+
yAxisCfg,
|
|
899
|
+
yAxisEntries,
|
|
891
900
|
degenShakeTransform,
|
|
892
901
|
overlayScrubFade,
|
|
893
902
|
} = model;
|
|
@@ -907,6 +916,9 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
907
916
|
suppressTag={refLineCustom[i]}
|
|
908
917
|
suppressTagWhenOffAxis={refLineOffAxisCustom[i]}
|
|
909
918
|
customTagWidths={refLineCustomTagWidths}
|
|
919
|
+
yAxisEntries={yAxisEntries}
|
|
920
|
+
labelRightMargin={yAxisCfg?.labelRightMargin}
|
|
921
|
+
gridEndGap={yAxisCfg?.gridEndGap}
|
|
910
922
|
/>
|
|
911
923
|
))}
|
|
912
924
|
</Group>
|
|
@@ -920,6 +932,53 @@ function SeriesCustomConsumerOverlay({ model }: { model: LiveChartSeriesModel })
|
|
|
920
932
|
return <ChartOverlayLayer render={renderOverlay!} context={overlayContext} />;
|
|
921
933
|
}
|
|
922
934
|
|
|
935
|
+
/** Keeps the extra fade mapper opt-in with the per-series tooltip itself. */
|
|
936
|
+
function SeriesTooltipLayer({
|
|
937
|
+
model,
|
|
938
|
+
config,
|
|
939
|
+
}: {
|
|
940
|
+
model: LiveChartSeriesModel;
|
|
941
|
+
config: NonNullable<LiveChartSeriesModel["seriesTooltipCfg"]>;
|
|
942
|
+
}) {
|
|
943
|
+
const {
|
|
944
|
+
crosshair,
|
|
945
|
+
engine,
|
|
946
|
+
effectivePadding,
|
|
947
|
+
scrubCfg,
|
|
948
|
+
skiaFont,
|
|
949
|
+
palette,
|
|
950
|
+
activeSeriesCount,
|
|
951
|
+
} = model;
|
|
952
|
+
const activeOpacity = useCrosshairVisibleOpacity(
|
|
953
|
+
crosshair.scrubX,
|
|
954
|
+
engine.canvasWidth,
|
|
955
|
+
effectivePadding.right,
|
|
956
|
+
crosshair.scrubActive,
|
|
957
|
+
scrubCfg?.crosshairFade ?? true,
|
|
958
|
+
scrubCfg?.crosshairFadeDistance ?? 4,
|
|
959
|
+
);
|
|
960
|
+
const scrubActive = crosshair.scrubActive;
|
|
961
|
+
const alwaysShow = config.alwaysShow;
|
|
962
|
+
const opacity = useDerivedValue(
|
|
963
|
+
() => (scrubActive.get() ? activeOpacity.get() : alwaysShow ? 1 : 0),
|
|
964
|
+
[scrubActive, activeOpacity, alwaysShow],
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
return (
|
|
968
|
+
<PerSeriesTooltipOverlay
|
|
969
|
+
layout={crosshair.tooltipLayout}
|
|
970
|
+
font={skiaFont}
|
|
971
|
+
palette={palette}
|
|
972
|
+
config={config}
|
|
973
|
+
seriesCount={activeSeriesCount}
|
|
974
|
+
opacity={opacity}
|
|
975
|
+
tooltipBackground={scrubCfg?.tooltipBackground}
|
|
976
|
+
tooltipColor={scrubCfg?.tooltipColor}
|
|
977
|
+
tooltipBorderColor={scrubCfg?.tooltipBorderColor}
|
|
978
|
+
/>
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
923
982
|
export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
924
983
|
const model = useLiveChartSeriesController(props);
|
|
925
984
|
const {
|
|
@@ -962,8 +1021,6 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
962
1021
|
refLineCustomTagWidths,
|
|
963
1022
|
overlayScrubFade,
|
|
964
1023
|
canvasMode,
|
|
965
|
-
activeSeriesCount,
|
|
966
|
-
skiaFont,
|
|
967
1024
|
} = model;
|
|
968
1025
|
|
|
969
1026
|
// Mirror the Skia overlay fade onto the RN custom-marker sibling so
|
|
@@ -1065,7 +1122,14 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
1065
1122
|
seriesTooltipCfg?.guideColor ??
|
|
1066
1123
|
scrubCfg.crosshairLineColor
|
|
1067
1124
|
}
|
|
1068
|
-
|
|
1125
|
+
crosshairStrokeWidth={
|
|
1126
|
+
seriesTooltipCfg?.guideWidth ??
|
|
1127
|
+
scrubCfg.crosshairStrokeWidth
|
|
1128
|
+
}
|
|
1129
|
+
crosshairOvershoot={scrubCfg.crosshairOvershoot}
|
|
1130
|
+
crosshairFade={scrubCfg.crosshairFade}
|
|
1131
|
+
crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
|
|
1132
|
+
crosshairLineCap={scrubCfg.crosshairLineCap}
|
|
1069
1133
|
crosshairDash={
|
|
1070
1134
|
seriesTooltipCfg
|
|
1071
1135
|
? seriesTooltipCfg.guideDashPattern
|
|
@@ -1079,19 +1143,6 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
1079
1143
|
{/* Per-series value labels on top of the scrub dim so the dim never
|
|
1080
1144
|
clips them (they track each series' live value, not the scrub). */}
|
|
1081
1145
|
<SeriesValueLabelLayer model={model} />
|
|
1082
|
-
|
|
1083
|
-
{seriesTooltipCfg && (
|
|
1084
|
-
<PerSeriesTooltipOverlay
|
|
1085
|
-
layout={crosshair.tooltipLayout}
|
|
1086
|
-
font={skiaFont}
|
|
1087
|
-
palette={palette}
|
|
1088
|
-
config={seriesTooltipCfg}
|
|
1089
|
-
seriesCount={activeSeriesCount}
|
|
1090
|
-
tooltipBackground={scrubCfg?.tooltipBackground}
|
|
1091
|
-
tooltipColor={scrubCfg?.tooltipColor}
|
|
1092
|
-
tooltipBorderColor={scrubCfg?.tooltipBorderColor}
|
|
1093
|
-
/>
|
|
1094
|
-
)}
|
|
1095
1146
|
</Canvas>
|
|
1096
1147
|
|
|
1097
1148
|
{/* RN labels floated over the canvas (sibling of <Canvas>, an RN
|
|
@@ -1153,6 +1204,22 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
1153
1204
|
</Animated.View>
|
|
1154
1205
|
)}
|
|
1155
1206
|
|
|
1207
|
+
{/* A dedicated transparent Canvas keeps the UI-thread tooltip above
|
|
1208
|
+
custom RN annotations. Rendering it in the primary Canvas would
|
|
1209
|
+
put every post-Canvas `renderReferenceLine` sibling above it and
|
|
1210
|
+
produce mixed stacking through translucent custom tags. */}
|
|
1211
|
+
{seriesTooltipCfg && (
|
|
1212
|
+
<View
|
|
1213
|
+
testID="live-chart-series-tooltip-overlay"
|
|
1214
|
+
pointerEvents="none"
|
|
1215
|
+
style={StyleSheet.absoluteFill}
|
|
1216
|
+
>
|
|
1217
|
+
<Canvas style={StyleSheet.absoluteFill}>
|
|
1218
|
+
<SeriesTooltipLayer model={model} config={seriesTooltipCfg} />
|
|
1219
|
+
</Canvas>
|
|
1220
|
+
</View>
|
|
1221
|
+
)}
|
|
1222
|
+
|
|
1156
1223
|
{/* Custom consumer overlay — topmost RN sibling with the live plot
|
|
1157
1224
|
rect, including this multi-series chart's resolved axis inset. */}
|
|
1158
1225
|
{renderOverlay && <SeriesCustomConsumerOverlay model={model} />}
|
|
@@ -232,6 +232,7 @@ export function PerSeriesTooltipOverlay({
|
|
|
232
232
|
palette,
|
|
233
233
|
config,
|
|
234
234
|
seriesCount,
|
|
235
|
+
opacity,
|
|
235
236
|
tooltipBackground,
|
|
236
237
|
tooltipColor,
|
|
237
238
|
tooltipBorderColor,
|
|
@@ -241,6 +242,8 @@ export function PerSeriesTooltipOverlay({
|
|
|
241
242
|
palette: LiveChartPalette;
|
|
242
243
|
config: ResolvedPerSeriesTooltipConfig;
|
|
243
244
|
seriesCount: number;
|
|
245
|
+
/** Active crosshair opacity; idle `alwaysShow` pills stay fully visible. */
|
|
246
|
+
opacity?: SharedValue<number> | DerivedValue<number>;
|
|
244
247
|
tooltipBackground?: string;
|
|
245
248
|
tooltipColor?: string;
|
|
246
249
|
tooltipBorderColor?: string;
|
|
@@ -258,7 +261,7 @@ export function PerSeriesTooltipOverlay({
|
|
|
258
261
|
config.seriesPillBorderColor ?? tooltipBorderColor;
|
|
259
262
|
|
|
260
263
|
return (
|
|
261
|
-
<Group>
|
|
264
|
+
<Group opacity={opacity}>
|
|
262
265
|
<TimePill
|
|
263
266
|
layout={layout}
|
|
264
267
|
font={font}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
10
10
|
|
|
11
11
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
12
|
+
import type { YAxisEntry } from "../draw/grid";
|
|
12
13
|
import type { ChartPadding } from "../draw/line";
|
|
13
14
|
import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
|
|
14
15
|
import { usePathBuilder } from "../hooks/usePathBuilder";
|
|
@@ -85,6 +86,12 @@ type ReferenceLineOverlayProps = {
|
|
|
85
86
|
dragValues?: SharedValue<number[]>;
|
|
86
87
|
/** This line's index into {@link dragValues}. */
|
|
87
88
|
index?: number;
|
|
89
|
+
/** Y-axis entries used to match a right-anchored grid endpoint. */
|
|
90
|
+
yAxisEntries?: SharedValue<YAxisEntry[]> | null;
|
|
91
|
+
/** Enables clipping to the right-anchored Y-axis label column. */
|
|
92
|
+
labelRightMargin?: number;
|
|
93
|
+
/** Gap between the drawn line endpoint and the label column. */
|
|
94
|
+
gridEndGap?: number;
|
|
88
95
|
};
|
|
89
96
|
|
|
90
97
|
export function ReferenceLineOverlay({
|
|
@@ -102,6 +109,9 @@ export function ReferenceLineOverlay({
|
|
|
102
109
|
groupHidden,
|
|
103
110
|
dragValues,
|
|
104
111
|
index = 0,
|
|
112
|
+
yAxisEntries,
|
|
113
|
+
labelRightMargin,
|
|
114
|
+
gridEndGap,
|
|
105
115
|
}: ReferenceLineOverlayProps) {
|
|
106
116
|
const form = referenceLineForm(line);
|
|
107
117
|
const isBand = form === "value-band" || form === "time-band";
|
|
@@ -141,6 +151,10 @@ export function ReferenceLineOverlay({
|
|
|
141
151
|
badgeFont,
|
|
142
152
|
dragValues,
|
|
143
153
|
index,
|
|
154
|
+
yAxisEntries ?? undefined,
|
|
155
|
+
labelRightMargin,
|
|
156
|
+
gridEndGap,
|
|
157
|
+
font,
|
|
144
158
|
);
|
|
145
159
|
|
|
146
160
|
const labelColor =
|
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
8
8
|
import { BADGE_METRICS_DEFAULTS, MAX_Y_LABELS } from "../constants";
|
|
9
9
|
import type { ResolvedGridStyleConfig } from "../core/resolveConfig";
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
rightAnchoredYAxisColumnLayout,
|
|
12
|
+
type YAxisEntry,
|
|
13
|
+
} from "../draw/grid";
|
|
11
14
|
import {
|
|
12
15
|
badgeTailAndCap,
|
|
13
16
|
gutterCenteredTextLeftX,
|
|
@@ -57,6 +60,8 @@ export function YAxisOverlay({
|
|
|
57
60
|
gridStyle,
|
|
58
61
|
variant = "all",
|
|
59
62
|
float = false,
|
|
63
|
+
labelRightMargin,
|
|
64
|
+
gridEndGap = 0,
|
|
60
65
|
}: {
|
|
61
66
|
entries: SharedValue<YAxisEntry[]>;
|
|
62
67
|
engine: ChartEngineLayout;
|
|
@@ -92,6 +97,10 @@ export function YAxisOverlay({
|
|
|
92
97
|
* plot) instead of centering them in a reserved gutter. See {@link YAxisConfig.float}.
|
|
93
98
|
*/
|
|
94
99
|
float?: boolean;
|
|
100
|
+
/** Fixed canvas-edge margin for a shared left-aligned label column. */
|
|
101
|
+
labelRightMargin?: number;
|
|
102
|
+
/** Gap between the grid-line end and the shared label column. */
|
|
103
|
+
gridEndGap?: number;
|
|
95
104
|
}) {
|
|
96
105
|
const gridColor = gridStyle?.color ?? palette.gridLine;
|
|
97
106
|
const gridWidth = gridStyle?.strokeWidth ?? 1;
|
|
@@ -99,14 +108,26 @@ export function YAxisOverlay({
|
|
|
99
108
|
const gridOpacity = gridStyle?.opacity ?? 1;
|
|
100
109
|
const gridBuilder = usePathBuilder();
|
|
101
110
|
|
|
111
|
+
const rightAnchoredColumn = useDerivedValue(() => {
|
|
112
|
+
if (labelRightMargin === undefined) return null;
|
|
113
|
+
return rightAnchoredYAxisColumnLayout(
|
|
114
|
+
engine.canvasWidth.get(),
|
|
115
|
+
entries.get(),
|
|
116
|
+
font,
|
|
117
|
+
labelRightMargin,
|
|
118
|
+
gridEndGap,
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
102
122
|
const gridLinesPath = useDerivedValue(() => {
|
|
103
123
|
const b = gridBuilder.value;
|
|
104
124
|
if (variant === "labels") return b.detach();
|
|
105
125
|
const items = entries.get();
|
|
106
126
|
const w = engine.canvasWidth.get();
|
|
127
|
+
const lineX2 = rightAnchoredColumn.get()?.gridEndX ?? w - padding.right;
|
|
107
128
|
for (let i = 0; i < items.length; i++) {
|
|
108
129
|
b.moveTo(padding.left, items[i].y);
|
|
109
|
-
b.lineTo(
|
|
130
|
+
b.lineTo(lineX2, items[i].y);
|
|
110
131
|
}
|
|
111
132
|
return b.detach();
|
|
112
133
|
});
|
|
@@ -120,6 +141,7 @@ export function YAxisOverlay({
|
|
|
120
141
|
const w = engine.canvasWidth.get();
|
|
121
142
|
const fm = font.getMetrics();
|
|
122
143
|
const baselineOffset = (fm.ascent + fm.descent) / 2;
|
|
144
|
+
const columnX = rightAnchoredColumn.get()?.labelX;
|
|
123
145
|
const labelHeight = fm.descent - fm.ascent;
|
|
124
146
|
const badgeIsVisible = badgeOpacity === undefined || badgeOpacity.get() > 0;
|
|
125
147
|
const resolvedBadgeCenterY = badgeCenterY && badgeIsVisible
|
|
@@ -133,13 +155,16 @@ export function YAxisOverlay({
|
|
|
133
155
|
for (let i = 0; i < items.length; i++) {
|
|
134
156
|
const e = items[i];
|
|
135
157
|
const textW = measureFontTextWidth(font, e.label);
|
|
136
|
-
const x =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
const x =
|
|
159
|
+
columnX !== undefined
|
|
160
|
+
? columnX
|
|
161
|
+
: float
|
|
162
|
+
? gutterRightAlignedTextLeftX(w, textW, FLOAT_LABEL_RIGHT_MARGIN)
|
|
163
|
+
: badge
|
|
164
|
+
? pillTextLeftX(w, padding.right, leftInset, textW, badgeMetrics)
|
|
165
|
+
: seriesLabelInset > 0
|
|
166
|
+
? gutterRightAlignedTextLeftX(w, textW)
|
|
167
|
+
: gutterCenteredTextLeftX(w, padding.right, textW);
|
|
143
168
|
result.push({
|
|
144
169
|
x,
|
|
145
170
|
y: e.y - baselineOffset,
|
|
@@ -89,6 +89,10 @@ export interface ResolvedYAxisConfig {
|
|
|
89
89
|
minGap: number;
|
|
90
90
|
/** Fixed label count (≥ 2), or 0 for the dynamic nice-interval grid. */
|
|
91
91
|
count: number;
|
|
92
|
+
/** undefined → keep the default centered-gutter label placement. */
|
|
93
|
+
labelRightMargin: number | undefined;
|
|
94
|
+
/** undefined → no gap; inert unless labelRightMargin is set. */
|
|
95
|
+
gridEndGap: number | undefined;
|
|
92
96
|
/** Float the axis over a full-width plot (no reserved right gutter). */
|
|
93
97
|
float: boolean;
|
|
94
98
|
}
|
|
@@ -151,6 +155,16 @@ export interface ResolvedScrubConfig {
|
|
|
151
155
|
dimOpacity: number;
|
|
152
156
|
/** undefined → palette.crosshairLine */
|
|
153
157
|
crosshairLineColor: string | undefined;
|
|
158
|
+
/** Vertical crosshair line width in px. */
|
|
159
|
+
crosshairStrokeWidth: number;
|
|
160
|
+
/** Extension past the plot's top and bottom edges in px. */
|
|
161
|
+
crosshairOvershoot: number;
|
|
162
|
+
/** Fade the crosshair near the live edge. */
|
|
163
|
+
crosshairFade: boolean;
|
|
164
|
+
/** Visible-crosshair fade distance near the live edge in px. */
|
|
165
|
+
crosshairFadeDistance: number;
|
|
166
|
+
/** undefined preserves the existing Skia line-cap default. */
|
|
167
|
+
crosshairLineCap: "butt" | "round" | "square" | undefined;
|
|
154
168
|
/** Dash intervals `[on, off, …]` for the crosshair line; undefined → solid. */
|
|
155
169
|
crosshairDash: number[] | undefined;
|
|
156
170
|
/** undefined → palette.crosshairDim */
|
|
@@ -175,6 +189,8 @@ export interface ResolvedScrubConfig {
|
|
|
175
189
|
panGestureDelay: number;
|
|
176
190
|
/** Fade markers + reference lines out while scrubbing. */
|
|
177
191
|
hideOverlaysOnScrub: boolean;
|
|
192
|
+
/** Reject outside plain-scrub starts and clamp active scrub X to the plot. */
|
|
193
|
+
clampToPlot: boolean;
|
|
178
194
|
}
|
|
179
195
|
|
|
180
196
|
export interface ResolvedPerSeriesTooltipConfig {
|
|
@@ -465,6 +481,8 @@ export function resolveBadge(
|
|
|
465
481
|
const Y_AXIS_DEFAULTS: ResolvedYAxisConfig = {
|
|
466
482
|
minGap: 36,
|
|
467
483
|
count: 0,
|
|
484
|
+
labelRightMargin: undefined,
|
|
485
|
+
gridEndGap: undefined,
|
|
468
486
|
float: false,
|
|
469
487
|
};
|
|
470
488
|
|
|
@@ -664,6 +682,11 @@ const SCRUB_DEFAULTS: ResolvedScrubConfig = {
|
|
|
664
682
|
seriesTooltip: null,
|
|
665
683
|
dimOpacity: 0.3,
|
|
666
684
|
crosshairLineColor: undefined,
|
|
685
|
+
crosshairStrokeWidth: 1,
|
|
686
|
+
crosshairOvershoot: 0,
|
|
687
|
+
crosshairFade: true,
|
|
688
|
+
crosshairFadeDistance: 4,
|
|
689
|
+
crosshairLineCap: undefined,
|
|
667
690
|
crosshairDash: undefined,
|
|
668
691
|
crosshairDimColor: undefined,
|
|
669
692
|
tooltipBackground: undefined,
|
|
@@ -676,6 +699,7 @@ const SCRUB_DEFAULTS: ResolvedScrubConfig = {
|
|
|
676
699
|
tooltipShowTime: true,
|
|
677
700
|
panGestureDelay: 0,
|
|
678
701
|
hideOverlaysOnScrub: false,
|
|
702
|
+
clampToPlot: false,
|
|
679
703
|
};
|
|
680
704
|
|
|
681
705
|
const PER_SERIES_TOOLTIP_DEFAULTS: ResolvedPerSeriesTooltipConfig = {
|
|
@@ -743,6 +767,11 @@ export function resolveScrub(
|
|
|
743
767
|
const seriesTooltip =
|
|
744
768
|
typeof prop === "object" ? prop.seriesTooltip : undefined;
|
|
745
769
|
resolved.seriesTooltip = resolvePerSeriesTooltip(seriesTooltip);
|
|
770
|
+
resolved.crosshairOvershoot = Math.max(0, resolved.crosshairOvershoot);
|
|
771
|
+
resolved.crosshairFadeDistance = Math.max(
|
|
772
|
+
0,
|
|
773
|
+
resolved.crosshairFadeDistance,
|
|
774
|
+
);
|
|
746
775
|
}
|
|
747
776
|
return resolved;
|
|
748
777
|
}
|
package/src/draw/grid.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { GRID_METRICS_DEFAULTS, MAX_Y_LABELS } from "../constants";
|
|
2
|
+
import type { SkFont } from "@shopify/react-native-skia";
|
|
3
|
+
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
2
4
|
import { lerp } from "../math/lerp";
|
|
3
5
|
import type { GridMetrics } from "../types";
|
|
4
6
|
|
|
@@ -8,6 +10,37 @@ export interface YAxisEntry {
|
|
|
8
10
|
alpha: number;
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
export interface RightAnchoredYAxisColumnLayout {
|
|
14
|
+
/** Shared left X for every label in the column. */
|
|
15
|
+
labelX: number;
|
|
16
|
+
/** Grid/reference-line end X immediately before the column gap. */
|
|
17
|
+
gridEndX: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Measure the widest Y-axis label and anchor the shared left-aligned column to
|
|
22
|
+
* a fixed canvas-edge margin. Shared by the axis and reference-line worklets so
|
|
23
|
+
* their right edges cannot drift apart.
|
|
24
|
+
*/
|
|
25
|
+
export function rightAnchoredYAxisColumnLayout(
|
|
26
|
+
canvasWidth: number,
|
|
27
|
+
entries: readonly YAxisEntry[],
|
|
28
|
+
font: SkFont,
|
|
29
|
+
labelRightMargin: number,
|
|
30
|
+
gridEndGap = 0,
|
|
31
|
+
): RightAnchoredYAxisColumnLayout {
|
|
32
|
+
"worklet";
|
|
33
|
+
let maxTextW = 0;
|
|
34
|
+
for (let i = 0; i < entries.length; i++) {
|
|
35
|
+
maxTextW = Math.max(
|
|
36
|
+
maxTextW,
|
|
37
|
+
measureFontTextWidth(font, entries[i].label),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const labelX = canvasWidth - labelRightMargin - maxTextW;
|
|
41
|
+
return { labelX, gridEndX: labelX - gridEndGap };
|
|
42
|
+
}
|
|
43
|
+
|
|
11
44
|
/**
|
|
12
45
|
* Fixed-count Y-axis: place exactly `count` price labels evenly **in pixels**
|
|
13
46
|
* across the plot band — top = `displayMax`, bottom = `displayMin`. Values track
|
|
@@ -12,13 +12,87 @@ const TOOLTIP_LINE_GAP = 4;
|
|
|
12
12
|
const TOOLTIP_OFFSET_X = 12;
|
|
13
13
|
const TOOLTIP_EDGE_GAP = 4;
|
|
14
14
|
const TOOLTIP_TOP_MARGIN = 8;
|
|
15
|
-
const FADE_ZONE = 4;
|
|
16
15
|
|
|
17
16
|
/** Horizontal travel required before a plain scrub claims the touch. */
|
|
18
17
|
export const SCRUB_ACTIVATE_X_PX = 20;
|
|
19
18
|
/** Vertical travel that fails a plain scrub so a parent scroll gesture can win. */
|
|
20
19
|
export const SCRUB_FAIL_Y_PX = 10;
|
|
21
20
|
|
|
21
|
+
/** Clamp an X pixel to the plot's horizontal bounds. */
|
|
22
|
+
export function clampPlotX(
|
|
23
|
+
x: number,
|
|
24
|
+
padLeft: number,
|
|
25
|
+
canvasWidth: number,
|
|
26
|
+
padRight: number,
|
|
27
|
+
): number {
|
|
28
|
+
"worklet";
|
|
29
|
+
return Math.min(canvasWidth - padRight, Math.max(padLeft, x));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ScrubHitSlop {
|
|
33
|
+
left?: number;
|
|
34
|
+
right?: number;
|
|
35
|
+
bottom?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Build touch-down bounds for a scrub recognizer. */
|
|
39
|
+
export function resolveScrubHitSlop(
|
|
40
|
+
padding: ChartPadding,
|
|
41
|
+
clampToPlot: boolean,
|
|
42
|
+
bottomExclude = 0,
|
|
43
|
+
): ScrubHitSlop | undefined {
|
|
44
|
+
if (!clampToPlot && bottomExclude <= 0) return undefined;
|
|
45
|
+
return {
|
|
46
|
+
...(clampToPlot
|
|
47
|
+
? { left: -padding.left, right: -padding.right }
|
|
48
|
+
: {}),
|
|
49
|
+
...(bottomExclude > 0 ? { bottom: -bottomExclude } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ScrubGestureValue<T> {
|
|
54
|
+
get(): T;
|
|
55
|
+
set(value: T): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Start plain scrubbing; the recognizer gates out-of-plot touch origins. */
|
|
59
|
+
export function startPlainScrub(
|
|
60
|
+
x: number,
|
|
61
|
+
padding: ChartPadding,
|
|
62
|
+
canvasWidth: number,
|
|
63
|
+
clampToPlot: boolean,
|
|
64
|
+
scrubX: ScrubGestureValue<number>,
|
|
65
|
+
scrubActive: ScrubGestureValue<boolean>,
|
|
66
|
+
gestureStarted: ScrubGestureValue<boolean>,
|
|
67
|
+
): void {
|
|
68
|
+
"worklet";
|
|
69
|
+
if (clampToPlot) {
|
|
70
|
+
scrubX.set(clampPlotX(x, padding.left, canvasWidth, padding.right));
|
|
71
|
+
} else {
|
|
72
|
+
scrubX.set(x);
|
|
73
|
+
}
|
|
74
|
+
scrubActive.set(true);
|
|
75
|
+
gestureStarted.set(true);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Track plain scrubbing, clamping gestures accepted by the recognizer. */
|
|
79
|
+
export function updatePlainScrub(
|
|
80
|
+
x: number,
|
|
81
|
+
padding: ChartPadding,
|
|
82
|
+
canvasWidth: number,
|
|
83
|
+
clampToPlot: boolean,
|
|
84
|
+
scrubX: ScrubGestureValue<number>,
|
|
85
|
+
scrubActive: ScrubGestureValue<boolean>,
|
|
86
|
+
): void {
|
|
87
|
+
"worklet";
|
|
88
|
+
if (clampToPlot) {
|
|
89
|
+
if (!scrubActive.get()) return;
|
|
90
|
+
scrubX.set(clampPlotX(x, padding.left, canvasWidth, padding.right));
|
|
91
|
+
} else {
|
|
92
|
+
scrubX.set(x);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
22
96
|
/** Measure rendered text so the pill and its content share the same centre. */
|
|
23
97
|
function measureTooltipTextWidth(
|
|
24
98
|
font: SkFont,
|
|
@@ -217,20 +291,23 @@ export function computeScrubTime(
|
|
|
217
291
|
}
|
|
218
292
|
|
|
219
293
|
/**
|
|
220
|
-
* Crosshair opacity: fades 1→0 over
|
|
221
|
-
* approaches the live dot at the right chart edge.
|
|
294
|
+
* Crosshair opacity: fades 1→0 over `fadeDistance` px as the crosshair
|
|
295
|
+
* approaches the live dot at the right chart edge. A zero distance removes
|
|
296
|
+
* the ramp while keeping the crosshair hidden at and beyond the live edge.
|
|
222
297
|
*/
|
|
223
298
|
export function computeCrosshairOpacity(
|
|
224
299
|
scrubActive: boolean,
|
|
225
300
|
scrubX: number,
|
|
226
301
|
canvasWidth: number,
|
|
227
302
|
paddingRight: number,
|
|
303
|
+
fadeDistance = 4,
|
|
228
304
|
): number {
|
|
229
305
|
"worklet";
|
|
230
306
|
if (!scrubActive) return 0;
|
|
231
307
|
const dotX = canvasWidth - paddingRight;
|
|
232
308
|
const dist = dotX - scrubX;
|
|
233
|
-
|
|
309
|
+
if (fadeDistance <= 0) return dist > 0 ? 1 : 0;
|
|
310
|
+
return Math.min(1, Math.max(0, dist / fadeDistance));
|
|
234
311
|
}
|
|
235
312
|
|
|
236
313
|
/**
|