import {
DashPathEffect,
Group,
Path,
Text as SkiaText,
type SkFont,
} from "@shopify/react-native-skia";
import { useDerivedValue } from "react-native-reanimated";
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
import type { ChartPadding } from "../draw/line";
import { usePathBuilder } from "../hooks/usePathBuilder";
import { useReferenceLineSeries } from "../hooks/useReferenceLineSeries";
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
import { thresholdDashPhase } from "../math/threshold";
import type { LiveChartPalette, ReferenceLine } from "../types";
/**
* Draws Form-B `ReferenceLine.series` geometry. The base pass is a clipped Skia
* polyline behind the chart; the badge pass is the live-edge label above the
* chart fade, matching the existing two-pass reference-line layering.
*/
export function ReferenceLineSeriesOverlay({
engine,
padding,
line,
palette,
formatValue,
font,
badgeLayer,
}: {
engine: ChartEngineLayout;
padding: ChartPadding;
line: ReferenceLine;
palette: LiveChartPalette;
formatValue: (value: number) => string;
font: SkFont;
badgeLayer: boolean;
}) {
const points = line.series ?? [];
const extendToStart = line.extendToStart ?? true;
const extendToNow = line.extendToNow ?? true;
const geometry = useReferenceLineSeries(
engine,
padding,
points,
extendToStart,
extendToNow,
);
const color = line.color ?? palette.refLine;
const labelColor = line.labelColor ?? line.color ?? palette.refLabel;
const strokeOpacity = Math.max(0, Math.min(1, line.strokeOpacity ?? 1));
const strokeWidth = line.strokeWidth ?? 1;
const intervals = line.intervals ?? [4, 4];
const dashCycle = intervals[0] + intervals[1];
const builder = usePathBuilder();
const path = useDerivedValue(() => {
const b = builder.value;
const screen = geometry.screenPts.get();
if (geometry.visible.get() && screen.length >= 4) {
b.moveTo(screen[0], screen[1]);
for (let i = 2; i < screen.length; i += 2) {
b.lineTo(screen[i], screen[i + 1]);
}
}
return b.detach();
});
const pathOpacity = useDerivedValue(() =>
geometry.visible.get() ? strokeOpacity : 0,
);
const dashPhase = useDerivedValue(() =>
thresholdDashPhase(
engine.timestamp.get(),
engine.displayWindow.get(),
padding.left,
engine.canvasWidth.get() - padding.right,
dashCycle,
),
);
const plotClip = useDerivedValue(() => ({
x: padding.left,
y: padding.top,
width: Math.max(0, engine.canvasWidth.get() - padding.left - padding.right),
height: Math.max(
0,
engine.canvasHeight.get() - padding.top - padding.bottom,
),
}));
const metrics = font.getMetrics();
const baselineOffset = (metrics.ascent + metrics.descent) / 2;
const labelText = useDerivedValue(() => {
const value = formatValue(geometry.currentValue.get());
if (line.label) return line.showValue ? `${line.label} ${value}` : line.label;
return value;
});
const labelX = useDerivedValue(() => {
const left = padding.left;
const right = engine.canvasWidth.get() - padding.right;
const position = line.labelPosition ?? "right";
if (position === "left") return left + 4;
if (position === "center") {
return (left + right) / 2 - measureFontTextWidth(font, labelText.get()) / 2;
}
return right + 4;
});
const labelY = useDerivedValue(
() => geometry.currentY.get() - baselineOffset,
);
const labelOpacity = useDerivedValue(() =>
geometry.currentVisible.get() ? 1 : 0,
);
if (badgeLayer) {
return (
);
}
return (
);
}