diff --git a/frontend/src/components/InstrumentChart.tsx b/frontend/src/components/InstrumentChart.tsx index 52228d1..ec0b2ea 100644 --- a/frontend/src/components/InstrumentChart.tsx +++ b/frontend/src/components/InstrumentChart.tsx @@ -38,6 +38,8 @@ interface Props { chartType?: 'candles' | 'line' onDateHover?: (date: string | null) => void theoryCurve?: TheoPoint[] + /** Called once the chart is ready with (dateToCoord, canvasPageLeft) — lets external components align with the chart's x-axis */ + onChartReady?: (dateToCoord: (d: string) => number | null, canvasPageLeft: number) => void } export type { TheoPoint } @@ -67,7 +69,7 @@ const CAT_LABELS: Record = { technical: 'Tech.', } -export default function InstrumentChart({ priceData, indicators, events = [], height = 420, chartType = 'candles', onDateHover, theoryCurve }: Props) { +export default function InstrumentChart({ priceData, indicators, events = [], height = 420, chartType = 'candles', onDateHover, theoryCurve, onChartReady }: Props) { const containerRef = useRef(null) const cleanupRef = useRef<(() => void) | null>(null) const onHoverRef = useRef(onDateHover) @@ -208,6 +210,12 @@ export default function InstrumentChart({ priceData, indicators, events = [], he chart.timeScale().fitContent() + // Expose coordinate mapping so siblings can align with the chart x-axis + if (onChartReady && containerRef.current) { + const canvasLeft = containerRef.current.getBoundingClientRect().left + onChartReady((d: string) => chart.timeScale().timeToCoordinate(d as any), canvasLeft) + } + // ── Star overlay — ★ icons positioned via chart coordinate API ──────── const candleMap: Record = {} for (const c of priceData) candleMap[c.time] = c.high @@ -318,6 +326,7 @@ export default function InstrumentChart({ priceData, indicators, events = [], he chart.timeScale().unsubscribeVisibleLogicalRangeChange(renderStars) if (starsOverlay.parentNode) starsOverlay.parentNode.removeChild(starsOverlay) chart.remove() + onChartReady?.(null as any, 0) } }) @@ -326,7 +335,7 @@ export default function InstrumentChart({ priceData, indicators, events = [], he cleanupRef.current?.() cleanupRef.current = null } - }, [priceData, indicators, events, height, chartType, theoryCurve]) + }, [priceData, indicators, events, height, chartType, theoryCurve, onChartReady]) return (
diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index 2643ba3..d3452e1 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -697,27 +697,37 @@ const FRISE_POPUP_W = 220 // largeur du popup function CausalFrise({ events, templates, priceData, selectedDate, causalInsts, + chartDateToX, chartCanvasLeft, chartReady, }: { - events: SnapshotEvent[] - templates: CausalTemplate[] - priceData: PriceCandle[] - selectedDate: string | null - causalInsts: string[] + events: SnapshotEvent[] + templates: CausalTemplate[] + priceData: PriceCandle[] + selectedDate: string | null + causalInsts: string[] + chartDateToX?: (d: string) => number | null + chartCanvasLeft?: number + chartReady?: number }) { const navigate = useNavigate() const containerRef = useRef(null) const [contWidth, setContWidth] = useState(400) + const [contLeft, setContLeft] = useState(0) const [activeChip, setActiveChip] = useState<{ tmpl: CausalTemplate; ev: SnapshotEvent; chipX: number; chipY: number } | null>(null) useEffect(() => { const el = containerRef.current; if (!el) return - setContWidth(el.getBoundingClientRect().width) - const obs = new ResizeObserver(e => setContWidth(e[0].contentRect.width)) + const update = () => { + const rect = el.getBoundingClientRect() + setContWidth(rect.width) + setContLeft(rect.left) + } + update() + const obs = new ResizeObserver(update) obs.observe(el) return () => obs.disconnect() - }, []) + }, [chartReady]) // re-measure when chart is ready (layout may have changed) // Close popup on outside click (tiny delay avoids self-close) useEffect(() => { @@ -745,7 +755,20 @@ function CausalFrise({ const minDate = priceData[0].time const maxDate = priceData[priceData.length - 1].time const usable = Math.max(contWidth, 1) - const tdToX = makeTdToX(priceData, usable) + + // If chart coordinate bridge is available, use it for pixel-perfect alignment. + // chart.timeToCoordinate() gives x relative to chart canvas left edge. + // We subtract (chartCanvasLeft - contLeft) to convert to frise container coords. + // canvasOffset > 0 means chart canvas starts to the right of the frise container. + const canvasOffset = (chartCanvasLeft ?? 0) - contLeft + const tdToX: (d: string) => number = chartDateToX + ? (d: string) => { + const x = chartDateToX(d) + if (x !== null) return x - canvasOffset + // Fallback for out-of-range dates: clamp to edges + return d <= minDate ? -canvasOffset : usable - canvasOffset + } + : makeTdToX(priceData, usable) // Build chips (one per event × template) type Chip = { @@ -1004,6 +1027,10 @@ export default function InstrumentDashboard() { const [theoryCurve, setTheoryCurve] = useState(null) const [loadingTheory, setLoadingTheory] = useState(false) const [showTheory, setShowTheory] = useState(false) + // Chart coordinate bridge — lets CausalFrise align with the chart's x-axis + const chartDateToXRef = useRef<((d: string) => number | null) | null>(null) + const chartCanvasLeftRef = useRef(0) + const [chartReady, setChartReady] = useState(0) // bump to trigger frise re-render const instrumentId = id.toUpperCase() @@ -1020,6 +1047,8 @@ export default function InstrumentDashboard() { setMacroAtDate(null) setTheoryCurve(null) setShowTheory(false) + chartDateToXRef.current = null + setChartReady(0) api.get(`/instruments/${instrumentId}/snapshot?period=${period}`) .then(r => { setSnapshot(r.data) @@ -1042,6 +1071,12 @@ export default function InstrumentDashboard() { if (date) setSelectedDate(date) }, []) + const handleChartReady = useCallback((fn: ((d: string) => number | null) | null, left: number) => { + chartDateToXRef.current = fn + chartCanvasLeftRef.current = left + setChartReady(k => k + 1) + }, []) + const toggleTheory = useCallback(() => { if (showTheory) { setShowTheory(false) @@ -1253,6 +1288,7 @@ export default function InstrumentDashboard() { chartType={chartStyle} onDateHover={handleDateHover} theoryCurve={showTheory && theoryCurve ? theoryCurve : undefined} + onChartReady={handleChartReady} /> {/* Date badge */} @@ -1342,6 +1378,9 @@ export default function InstrumentDashboard() { priceData={snapshot.price_data} selectedDate={effectiveDate} causalInsts={causalInsts} + chartDateToX={chartDateToXRef.current ?? undefined} + chartCanvasLeft={chartCanvasLeftRef.current} + chartReady={chartReady} />