react-native-livechart 4.12.0 → 4.13.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/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/SegmentLineGradient.d.ts +23 -0
- package/dist/components/SegmentLineGradient.d.ts.map +1 -0
- 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/hooks/useSegmentLineGradient.d.ts.map +1 -1
- package/dist/math/segments.d.ts +4 -0
- package/dist/math/segments.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 +34 -23
- package/src/components/LiveChartSeries.tsx +83 -16
- package/src/components/PerSeriesTooltipOverlay.tsx +4 -1
- package/src/components/ReferenceLineOverlay.tsx +14 -0
- package/src/components/SegmentLineGradient.tsx +57 -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/hooks/useSegmentLineGradient.ts +13 -5
- package/src/math/segments.ts +26 -0
- package/src/types.ts +47 -0
|
@@ -89,7 +89,6 @@ import {
|
|
|
89
89
|
import { AXIS_GRAB_MIN_PX, usePanScroll } from "../hooks/usePanScroll";
|
|
90
90
|
import { usePinchZoom } from "../hooks/usePinchZoom";
|
|
91
91
|
import { useVisibleRange } from "../hooks/useVisibleRange";
|
|
92
|
-
import { useSegmentLineGradient } from "../hooks/useSegmentLineGradient";
|
|
93
92
|
import { useSingleChartReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
|
|
94
93
|
import {
|
|
95
94
|
useThreshold,
|
|
@@ -162,6 +161,7 @@ import { ReferenceLineGroupOverlay } from "./ReferenceLineGroupOverlay";
|
|
|
162
161
|
import { ReferenceLineOverlay } from "./ReferenceLineOverlay";
|
|
163
162
|
import { ScrubActionOverlay } from "./ScrubActionOverlay";
|
|
164
163
|
import { SegmentDividerOverlay } from "./SegmentDividerOverlay";
|
|
164
|
+
import { SegmentLineGradient } from "./SegmentLineGradient";
|
|
165
165
|
import { TradeStreamOverlay } from "./TradeStreamOverlay";
|
|
166
166
|
import { ValueLineOverlay } from "./ValueLineOverlay";
|
|
167
167
|
import { XAxisOverlay } from "./XAxisOverlay";
|
|
@@ -1066,6 +1066,7 @@ function useLiveChartController({
|
|
|
1066
1066
|
? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
|
|
1067
1067
|
: 0,
|
|
1068
1068
|
scrollActive,
|
|
1069
|
+
scrubCfg?.clampToPlot ?? false,
|
|
1069
1070
|
);
|
|
1070
1071
|
|
|
1071
1072
|
// Capture only the shared value in the worklets below. Referencing
|
|
@@ -1199,19 +1200,6 @@ function useLiveChartController({
|
|
|
1199
1200
|
effectivePadding,
|
|
1200
1201
|
);
|
|
1201
1202
|
|
|
1202
|
-
// Scrub-focus gradient painted onto the line stroke: uniform at rest, and while
|
|
1203
|
-
// scrubbing (or with an `active` segment) the focused segment stays full while
|
|
1204
|
-
// the others are de-emphasized. Declared after `crosshair` so it can read the
|
|
1205
|
-
// live scrub state.
|
|
1206
|
-
const segmentGradient = useSegmentLineGradient(
|
|
1207
|
-
engine,
|
|
1208
|
-
resolvedSegments,
|
|
1209
|
-
effectivePadding,
|
|
1210
|
-
lineProp?.color ?? palette.line,
|
|
1211
|
-
crosshair.scrubX,
|
|
1212
|
-
crosshair.scrubActive,
|
|
1213
|
-
);
|
|
1214
|
-
|
|
1215
1203
|
// Hide the live dot while scrubbing when a selection dot is marking the scrub
|
|
1216
1204
|
// point instead — otherwise both dots show at once. Applies on static charts
|
|
1217
1205
|
// too, now that they're scrubbable.
|
|
@@ -1325,7 +1313,6 @@ function useLiveChartController({
|
|
|
1325
1313
|
refGroupFormat,
|
|
1326
1314
|
resolvedSegments,
|
|
1327
1315
|
hasRecolorSegments,
|
|
1328
|
-
segmentGradient,
|
|
1329
1316
|
thresholdCfg,
|
|
1330
1317
|
thresholdGeom,
|
|
1331
1318
|
thresholdStrokeColors: thresholdStopColors?.stroke ?? null,
|
|
@@ -1495,6 +1482,7 @@ function ChartYAxisLayer({
|
|
|
1495
1482
|
metricsCfg,
|
|
1496
1483
|
gridStyleCfg,
|
|
1497
1484
|
yAxisFloat,
|
|
1485
|
+
yAxisCfg,
|
|
1498
1486
|
liveBadgeOpacity,
|
|
1499
1487
|
} = model;
|
|
1500
1488
|
return (
|
|
@@ -1515,6 +1503,8 @@ function ChartYAxisLayer({
|
|
|
1515
1503
|
badgeOffsetY={badgeCfg?.offsetY ?? 0}
|
|
1516
1504
|
badgeOpacity={badgeUsesRightGutter ? liveBadgeOpacity : undefined}
|
|
1517
1505
|
gridStyle={gridStyleCfg}
|
|
1506
|
+
labelRightMargin={yAxisCfg?.labelRightMargin}
|
|
1507
|
+
gridEndGap={yAxisCfg?.gridEndGap}
|
|
1518
1508
|
/>
|
|
1519
1509
|
</Group>
|
|
1520
1510
|
);
|
|
@@ -1760,7 +1750,7 @@ function ChartStack({
|
|
|
1760
1750
|
dragValues,
|
|
1761
1751
|
resolvedSegments,
|
|
1762
1752
|
hasRecolorSegments,
|
|
1763
|
-
|
|
1753
|
+
crosshair,
|
|
1764
1754
|
thresholdCfg,
|
|
1765
1755
|
thresholdGeom,
|
|
1766
1756
|
thresholdStrokeColors,
|
|
@@ -1846,6 +1836,9 @@ function ChartStack({
|
|
|
1846
1836
|
fontProp={fontProp}
|
|
1847
1837
|
dragValues={dragValues}
|
|
1848
1838
|
index={i}
|
|
1839
|
+
yAxisEntries={yAxisEntries}
|
|
1840
|
+
labelRightMargin={yAxisCfg?.labelRightMargin}
|
|
1841
|
+
gridEndGap={yAxisCfg?.gridEndGap}
|
|
1849
1842
|
/>
|
|
1850
1843
|
))}
|
|
1851
1844
|
</Group>
|
|
@@ -1898,11 +1891,13 @@ function ChartStack({
|
|
|
1898
1891
|
positions={thresholdGeom.splitPositions}
|
|
1899
1892
|
/>
|
|
1900
1893
|
) : hasRecolorSegments ? (
|
|
1901
|
-
<
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1894
|
+
<SegmentLineGradient
|
|
1895
|
+
engine={engine}
|
|
1896
|
+
segments={resolvedSegments}
|
|
1897
|
+
padding={effectivePadding}
|
|
1898
|
+
baseColor={lineProp?.color ?? palette.line}
|
|
1899
|
+
scrubX={crosshair.scrubX}
|
|
1900
|
+
scrubActive={crosshair.scrubActive}
|
|
1906
1901
|
/>
|
|
1907
1902
|
) : lineProp?.colors?.length ? (
|
|
1908
1903
|
<LinearGradient
|
|
@@ -2119,6 +2114,11 @@ function ChartScrubLayer({
|
|
|
2119
2114
|
dimOpacity={scrubCfg.dimOpacity}
|
|
2120
2115
|
liveDotExtent={liveDotExtent}
|
|
2121
2116
|
crosshairLineColor={scrubCfg.crosshairLineColor}
|
|
2117
|
+
crosshairStrokeWidth={scrubCfg.crosshairStrokeWidth}
|
|
2118
|
+
crosshairOvershoot={scrubCfg.crosshairOvershoot}
|
|
2119
|
+
crosshairFade={scrubCfg.crosshairFade}
|
|
2120
|
+
crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
|
|
2121
|
+
crosshairLineCap={scrubCfg.crosshairLineCap}
|
|
2122
2122
|
crosshairDash={scrubCfg.crosshairDash}
|
|
2123
2123
|
crosshairDimColor={scrubCfg.crosshairDimColor}
|
|
2124
2124
|
tooltipBackground={scrubCfg.tooltipBackground}
|
|
@@ -2262,9 +2262,11 @@ function ChartBadgeLayer({
|
|
|
2262
2262
|
function ChartRefBadgeLayer({
|
|
2263
2263
|
model,
|
|
2264
2264
|
degen,
|
|
2265
|
+
yAxisEntries,
|
|
2265
2266
|
}: {
|
|
2266
2267
|
model: LiveChartModel;
|
|
2267
2268
|
degen: DegenState | null;
|
|
2269
|
+
yAxisEntries: YAxisEntries | null;
|
|
2268
2270
|
}) {
|
|
2269
2271
|
const {
|
|
2270
2272
|
allRefLines,
|
|
@@ -2285,6 +2287,7 @@ function ChartRefBadgeLayer({
|
|
|
2285
2287
|
formatValue,
|
|
2286
2288
|
skiaFont,
|
|
2287
2289
|
fontProp,
|
|
2290
|
+
yAxisCfg,
|
|
2288
2291
|
overlayScrubFade,
|
|
2289
2292
|
} = model;
|
|
2290
2293
|
if (allRefLines.length === 0) return null;
|
|
@@ -2307,6 +2310,9 @@ function ChartRefBadgeLayer({
|
|
|
2307
2310
|
suppressTagWhenOffAxis={refLineOffAxisCustom[i]}
|
|
2308
2311
|
customTagWidths={refLineCustomTagWidths}
|
|
2309
2312
|
groupHidden={refGroupingActive ? groupHidden : undefined}
|
|
2313
|
+
yAxisEntries={yAxisEntries}
|
|
2314
|
+
labelRightMargin={yAxisCfg?.labelRightMargin}
|
|
2315
|
+
gridEndGap={yAxisCfg?.gridEndGap}
|
|
2310
2316
|
/>
|
|
2311
2317
|
))}
|
|
2312
2318
|
{/* Collapsed count handles for grouped (near-value) lines. */}
|
|
@@ -2547,7 +2553,11 @@ function ChartView({
|
|
|
2547
2553
|
)}
|
|
2548
2554
|
|
|
2549
2555
|
{/* Reference-line badges + labels above the fade so they stay crisp. */}
|
|
2550
|
-
<ChartRefBadgeLayer
|
|
2556
|
+
<ChartRefBadgeLayer
|
|
2557
|
+
model={model}
|
|
2558
|
+
degen={degen}
|
|
2559
|
+
yAxisEntries={yAxisEntries}
|
|
2560
|
+
/>
|
|
2551
2561
|
|
|
2552
2562
|
<ChartValueOverlay model={model} degen={degen} />
|
|
2553
2563
|
|
|
@@ -2601,12 +2611,13 @@ function ChartView({
|
|
|
2601
2611
|
scrubTime={crosshair.scrubTime}
|
|
2602
2612
|
scrubActive={crosshair.scrubActive}
|
|
2603
2613
|
scrubCandle={crosshair.scrubCandle}
|
|
2604
|
-
crosshairOpacity={crosshair.crosshairOpacity}
|
|
2605
2614
|
tooltipLayout={crosshair.tooltipLayout}
|
|
2606
2615
|
engine={engine}
|
|
2607
2616
|
padding={effectivePadding}
|
|
2608
2617
|
placement={scrubCfg.tooltipPlacement}
|
|
2609
2618
|
margin={scrubCfg.tooltipMargin}
|
|
2619
|
+
crosshairFade={scrubCfg.crosshairFade}
|
|
2620
|
+
crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
|
|
2610
2621
|
lineTop={crosshair.tooltipLineTop}
|
|
2611
2622
|
scrubDotY={crosshair.scrubDotY}
|
|
2612
2623
|
/>
|
|
@@ -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 =
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { LinearGradient, vec } from "@shopify/react-native-skia";
|
|
2
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
3
|
+
|
|
4
|
+
import type { ResolvedSegment } from "../core/resolveSegment";
|
|
5
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
6
|
+
import type { ChartPadding } from "../draw/line";
|
|
7
|
+
import { useSegmentLineGradient } from "../hooks/useSegmentLineGradient";
|
|
8
|
+
import { segmentGradientStopCount } from "../math/segments";
|
|
9
|
+
|
|
10
|
+
interface SegmentLineGradientProps {
|
|
11
|
+
engine: ChartEngineLayout;
|
|
12
|
+
segments: ResolvedSegment[];
|
|
13
|
+
padding: ChartPadding;
|
|
14
|
+
baseColor: string;
|
|
15
|
+
scrubX: SharedValue<number>;
|
|
16
|
+
scrubActive: SharedValue<boolean>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Keeps the two SharedValues passed to Skia on one stop-count lifecycle.
|
|
21
|
+
*
|
|
22
|
+
* Scrubbing can change which stops are visible without changing `stopCount`, so
|
|
23
|
+
* padding keeps those mapper updates safe. When the segment props themselves
|
|
24
|
+
* change the count, the keyed child remounts both SharedValues together instead
|
|
25
|
+
* of letting one retain the previous length for a frame.
|
|
26
|
+
*/
|
|
27
|
+
export function SegmentLineGradient(props: SegmentLineGradientProps) {
|
|
28
|
+
const stopCount = segmentGradientStopCount(props.segments);
|
|
29
|
+
return <SegmentLineGradientPaint key={stopCount} {...props} />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function SegmentLineGradientPaint({
|
|
33
|
+
engine,
|
|
34
|
+
segments,
|
|
35
|
+
padding,
|
|
36
|
+
baseColor,
|
|
37
|
+
scrubX,
|
|
38
|
+
scrubActive,
|
|
39
|
+
}: SegmentLineGradientProps) {
|
|
40
|
+
const gradient = useSegmentLineGradient(
|
|
41
|
+
engine,
|
|
42
|
+
segments,
|
|
43
|
+
padding,
|
|
44
|
+
baseColor,
|
|
45
|
+
scrubX,
|
|
46
|
+
scrubActive,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<LinearGradient
|
|
51
|
+
start={vec(0, 0)}
|
|
52
|
+
end={gradient.gradientEnd}
|
|
53
|
+
colors={gradient.colors}
|
|
54
|
+
positions={gradient.positions}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -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
|