diff --git a/frontend/src/components/InstrumentChart.tsx b/frontend/src/components/InstrumentChart.tsx index 34ccc0a..eaccc8a 100644 --- a/frontend/src/components/InstrumentChart.tsx +++ b/frontend/src/components/InstrumentChart.tsx @@ -18,6 +18,10 @@ interface ChartEvent { date: string title: string level: string + category?: string + impact_score?: number + end_date?: string | null + description?: string } interface Props { @@ -35,10 +39,22 @@ const MA_COLORS: Record = { ma200: '#a855f7', } -const LEVEL_COLORS: Record = { - long: '#7c3aed', - medium: '#3b82f6', - short: '#10b981', +const CAT_COLORS: Record = { + event_calendar: '#f59e0b', + geopolitical: '#ef4444', + fundamental: '#10b981', + report: '#3b82f6', + sentiment: '#8b5cf6', + technical: '#06b6d4', +} + +const CAT_LABELS: Record = { + event_calendar: 'Cal.', + geopolitical: 'Géo.', + fundamental: 'Fund.', + report: 'Report', + sentiment: 'Sent.', + technical: 'Tech.', } export default function InstrumentChart({ priceData, indicators, events = [], height = 420, onDateHover }: Props) { @@ -142,23 +158,77 @@ export default function InstrumentChart({ priceData, indicators, events = [], he chart.addLineSeries(bbOpts).setData(indicators.bb_lower) } - // Event markers - const timeSet = new Set(priceData.map(d => d.time)) - const markers = events - .filter(ev => timeSet.has(ev.date)) - .map(ev => ({ - time: ev.date, - position: 'aboveBar' as const, - color: LEVEL_COLORS[ev.level] ?? '#f59e0b', - shape: 'arrowDown' as const, - text: ev.title.slice(0, 18), - })) - .sort((a, b) => a.time.localeCompare(b.time)) - - if (markers.length) candleSeries.setMarkers(markers) - chart.timeScale().fitContent() + // ── Star overlay — ★ icons positioned via chart coordinate API ──────── + const candleMap: Record = {} + for (const c of priceData) candleMap[c.time] = c.high + + const starsOverlay = document.createElement('div') + starsOverlay.style.cssText = [ + 'position:absolute', 'top:0', 'left:0', 'width:100%', 'height:100%', + 'pointer-events:none', 'overflow:hidden', 'z-index:10', + ].join(';') + containerRef.current!.appendChild(starsOverlay) + + const renderStars = () => { + if (!active) return + starsOverlay.innerHTML = '' + const cw = containerRef.current?.clientWidth ?? 0 + const ch = containerRef.current?.clientHeight ?? 0 + + for (const ev of events) { + const xCoord = chart.timeScale().timeToCoordinate(ev.date as any) + if (xCoord === null || xCoord < 0 || xCoord > cw) continue + + const highPrice = candleMap[ev.date] + let yBase = ch * 0.15 // fallback: top 15% + if (highPrice !== undefined) { + const yHigh = candleSeries.priceToCoordinate(highPrice) + if (yHigh !== null) yBase = Math.max(20, yHigh - 52) + } + + const cat = ev.category ?? 'fundamental' + const color = CAT_COLORS[cat] ?? '#94a3b8' + const score = ev.impact_score ?? 0.5 + const sz = score >= 0.8 ? 15 : score >= 0.6 ? 12 : 10 + + const wrap = document.createElement('div') + wrap.style.cssText = [ + `position:absolute`, `left:${xCoord}px`, `top:${yBase}px`, + `transform:translateX(-50%)`, + `display:flex`, `flex-direction:column`, `align-items:center`, `gap:1px`, + `pointer-events:auto`, `cursor:default`, + ].join(';') + wrap.title = `${ev.title}\n${ev.date}${ev.end_date ? ' → ' + ev.end_date : ''}` + + const lbl = document.createElement('div') + lbl.style.cssText = [ + `font-size:8px`, `font-family:ui-monospace,monospace`, + `color:${color}`, `white-space:nowrap`, + `text-shadow:0 1px 3px #000,0 0 6px #000`, + `line-height:1`, `max-width:90px`, + `overflow:hidden`, `text-overflow:ellipsis`, + ].join(';') + lbl.textContent = ev.title.slice(0, 22) + + const star = document.createElement('div') + star.style.cssText = [ + `font-size:${sz}px`, `color:${color}`, `line-height:1`, + `filter:drop-shadow(0 0 4px ${color}80)`, + `text-shadow:0 1px 3px #000`, + ].join(';') + star.textContent = '★' + + wrap.appendChild(lbl) + wrap.appendChild(star) + starsOverlay.appendChild(wrap) + } + } + + renderStars() + chart.timeScale().subscribeVisibleLogicalRangeChange(renderStars) + // Crosshair → date callback chart.subscribeCrosshairMove((param: any) => { if (param?.time && typeof param.time === 'string') { @@ -173,7 +243,12 @@ export default function InstrumentChart({ priceData, indicators, events = [], he }) ro.observe(containerRef.current) - cleanupRef.current = () => { ro.disconnect(); chart.remove() } + cleanupRef.current = () => { + ro.disconnect() + chart.timeScale().unsubscribeVisibleLogicalRangeChange(renderStars) + if (starsOverlay.parentNode) starsOverlay.parentNode.removeChild(starsOverlay) + chart.remove() + } }) return () => { @@ -195,11 +270,11 @@ export default function InstrumentChart({ priceData, indicators, events = [], he BB(20,2) - - {Object.entries(LEVEL_COLORS).map(([l, c]) => ( - - - {l === 'long' ? 'LT' : l === 'medium' ? 'MT' : 'CT'} + + {Object.entries(CAT_COLORS).map(([cat, c]) => ( + + + {CAT_LABELS[cat]} ))} @@ -209,7 +284,7 @@ export default function InstrumentChart({ priceData, indicators, events = [], he Chargement... ) : ( -
+
)}
) diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index 418f6fd..2db9bb3 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -109,8 +109,6 @@ function fmtDateFR(s: string | null): string { return `${d}/${m}/${y}` } -function dateToMs(s: string) { return new Date(s).getTime() } - function pctN(a: number | undefined, b: number | undefined): number { if (a === undefined || b === undefined || b === 0) return 0 return ((a - b) / b) * 100 @@ -507,63 +505,6 @@ function NarrativeCard({ narrative, loading, onLoad, instrument }: { ) } -// ── EventStarsStrip — étoiles au-dessus du graphe ──────────────────────────── - -const CAT_STAR_COLORS: Record = { - event_calendar: '#f59e0b', // amber - geopolitical: '#ef4444', // red - fundamental: '#10b981', // emerald - report: '#3b82f6', // blue - sentiment: '#8b5cf6', // violet - technical: '#06b6d4', // cyan -} - -function EventStarsStrip({ - events, priceData, -}: { - events: SnapshotEvent[] - priceData: PriceCandle[] -}) { - if (priceData.length < 2 || events.length === 0) return null - - const chartStart = dateToMs(priceData[0].time) - const chartEnd = dateToMs(priceData[priceData.length - 1].time) - const totalMs = chartEnd - chartStart - if (totalMs <= 0) return null - - function leftPct(d: string) { - return Math.max(0.5, Math.min(98.5, ((dateToMs(d) - chartStart) / totalMs) * 100)) - } - - return ( -
- {events.map((ev, i) => { - const left = leftPct(ev.date) - const color = CAT_STAR_COLORS[ev.category] ?? '#94a3b8' - const score = ev.impact_score ?? 0.5 - const size = score >= 0.8 ? 16 : score >= 0.6 ? 13 : 11 - return ( - - ★ - - ) - })} -
- ) -} - // ── DriversPanel — édition inline ──────────────────────────────────────────── function DriversPanel({ instrumentId, drivers, onSave, onClose }: { @@ -915,11 +856,6 @@ export default function InstrumentDashboard() { {/* ── Content ── */} {!loading && snapshot && ( <> - -