import { useMemo } from "react"; import { DashPathEffect, Group, Path, RoundedRect, Text as SkiaText, type SkFont, } from "@shopify/react-native-skia"; import { useDerivedValue, type SharedValue } from "react-native-reanimated"; import type { ChartEngineLayout } from "../core/useLiveChartEngine"; import type { YAxisEntry } from "../draw/grid"; import type { ChartPadding } from "../draw/line"; import { useChartSkiaFont } from "../hooks/useChartSkiaFont"; import { usePathBuilder } from "../hooks/usePathBuilder"; import { useReferenceLine, type ReferenceLineLayout, } from "../hooks/useReferenceLine"; import { referenceBadgeFont } from "../hooks/referenceBadgeFont"; import type { ReferenceTagStack } from "../math/referenceTagStack"; import type { AxisBadgeConfig } from "../math/axisBadgeLayout"; import { ReferenceValueBadge } from "./ReferenceValueBadge"; import { MONO_FONT_FAMILY } from "../lib/monoFontFamily"; import { referenceLineForm, resolveReferenceBadge, } from "../math/referenceLines"; import type { FontConfig, LiveChartPalette, ReferenceLine } from "../types"; import { ReferenceLineSeriesOverlay } from "./ReferenceLineSeriesOverlay"; /** Translucent fill alpha for value / time bands. */ const BAND_FILL_OPACITY = 0.16; /** Vertical padding inside the badge pill, in px (kept in sync with the layout). */ const BADGE_PILL_PAD_Y = 3; const BADGE_PILL_RADIUS = 5; /** Gap between a badge's outer edge and its dashed connector, in px. */ const CONNECTOR_GAP = 4; /** Badge inset from its plot edge, in px (matches useReferenceLine). */ const BADGE_EDGE_INSET = 2; /** * Renders one reference line or band into the chart canvas. Handles all four * `ReferenceLine` forms (horizontal line, time-varying line, horizontal value * band, vertical time band) plus the Form-A pill badge (in-range tag + * off-screen chevron pin). * Self-contained so callers can `.map()` over a variable-length array. */ type ReferenceLineOverlayProps = { engine: ChartEngineLayout; padding: ChartPadding; line: ReferenceLine; palette: LiveChartPalette; formatValue: (v: number) => string; font: SkFont; /** * The chart's raw font config — used to build a per-badge font when the line's * {@link ReferenceLineBadgeConfig} sets `fontSize` / `fontFamily` / `fontWeight`. * When no badge font knob is set, the resolved `font` is used as-is. */ fontProp?: FontConfig; valueAxis?: AxisBadgeConfig; /** * Render only the badge + label (`true`) or only the lines / bands (`false`, * default). The caller draws the base pass behind the chart content and the * badge pass **above** the left-edge fade, so badges/labels stay crisp instead * of being erased by the fade's `dstOut` blend. */ badgeLayer?: boolean; /** * Suppress the built-in tag (badge pill + chevron + icon + gutter label) for * this line — used when a custom `renderReferenceLine` element owns the tag. * The line / band stroke and a badged line's dashed connector still draw. No * effect on the base pass. */ suppressTag?: boolean; /** * Suppress the built-in tag only while the line is off-axis. Used by * `renderOffAxisReferenceLine`, so the normal in-range tag remains intact. */ suppressTagWhenOffAxis?: boolean; /** * Measured widths of custom reference-line tags, index-aligned with their * `referenceLines`. When present for a suppressed badged tag, the built-in * connector begins after the custom element instead of the hidden Skia pill. */ customTagWidths?: SharedValue; /** * Per-frame grouping flags (index-aligned): when this line's slot is `true` it's * collapsed into a group count handle, so its tag is suppressed (the line / band * stroke still draws). */ groupHidden?: SharedValue; tagStack?: SharedValue; /** Per-line live value overrides (dragged values), index-aligned with `referenceLines`. */ dragValues?: SharedValue; /** This line's index into {@link dragValues}. */ index?: number; /** Y-axis entries used to match a right-anchored grid endpoint. */ yAxisEntries?: SharedValue | null; /** Enables clipping to the right-anchored Y-axis label column. */ labelRightMargin?: number; /** Gap between the drawn line endpoint and the label column. */ gridEndGap?: number; }; /** * Dispatch to the series or scalar/band renderer without mixing their hook * lifecycles. A stable `id` may therefore switch forms safely across renders. */ export function ReferenceLineOverlay(props: ReferenceLineOverlayProps) { if (referenceLineForm(props.line) === "series") { return ( ); } return ; } function resolveStaticReferenceLineAppearance( line: ReferenceLine, palette: LiveChartPalette, fontProp: FontConfig | undefined, ) { const form = referenceLineForm(line); const isBand = form === "value-band" || form === "time-band"; const color = line.color ?? palette.refLine; const badge = resolveReferenceBadge(line); const badgeHasFontOverride = badge != null && (badge.fontSize != null || badge.fontFamily != null || badge.fontWeight != null); return { form, isBand, color, fillColor: line.fillColor ?? color, strokeOpacity: Math.max(0, Math.min(1, line.strokeOpacity ?? 1)), badge, badgeHasFontOverride, badgeFontConfig: badgeHasFontOverride ? { ...fontProp, fontFamily: badge?.fontFamily ?? fontProp?.fontFamily, fontSize: badge?.fontSize ?? fontProp?.fontSize, fontWeight: badge?.fontWeight ?? fontProp?.fontWeight, } : fontProp, labelColor: badge?.textColor ?? line.labelColor ?? line.color ?? palette.refLabel, strokeWidth: line.strokeWidth ?? 1, intervals: line.intervals ?? [4, 4], bandFillOpacity: line.fillOpacity ?? BAND_FILL_OPACITY, hasBandBorder: isBand && line.strokeWidth !== undefined, badgeBackground: badge?.background ?? palette.tooltipBg, badgeBorderColor: badge?.borderColor ?? color, badgeBorderWidth: badge?.borderWidth ?? 1, badgeRadius: badge?.radius ?? BADGE_PILL_RADIUS, badgeOffsetX: badge?.offsetX ?? 0, badgeOffsetY: badge?.offsetY ?? 0, }; } function ReferenceLineStaticOverlay({ engine, padding, line, palette, formatValue, font, fontProp, badgeLayer = false, suppressTag = false, suppressTagWhenOffAxis = false, customTagWidths, groupHidden, tagStack, dragValues, index = 0, yAxisEntries, labelRightMargin, gridEndGap, valueAxis, }: ReferenceLineOverlayProps) { const { form, isBand, color, fillColor, strokeOpacity, badge, badgeHasFontOverride, badgeFontConfig, labelColor, strokeWidth, intervals, bandFillOpacity, hasBandBorder, badgeBackground, badgeBorderColor, badgeBorderWidth, badgeRadius, badgeOffsetX, badgeOffsetY, } = resolveStaticReferenceLineAppearance(line, palette, fontProp); const badgeFontOverride = useChartSkiaFont( badgeFontConfig, MONO_FONT_FAMILY, palette.labelFontSize, ); const badgeFont = badgeHasFontOverride ? badgeFontOverride : font; const valueStyle = typeof line.valueBadge === "object" ? line.valueBadge : undefined; const valueFont = useMemo(() => referenceBadgeFont(font, fontProp, valueStyle), [font, fontProp, valueStyle]); const layout = useReferenceLine( engine, padding, line, formatValue, badgeFont, dragValues, index, yAxisEntries ?? undefined, labelRightMargin, gridEndGap, font, valueFont, valueAxis, ); const valueHidden = useDerivedValue(() => suppressTag || (suppressTagWhenOffAxis && layout.get().offAxis) || (groupHidden?.get()[index] ?? false)); if (!badgeLayer) { return ( ); } return ( <> {line.valueBadge &&