feat: replace arrowDown markers with ★ stars overlay on chart, colored by category
- InstrumentChart: remove setMarkers arrowDown (level-colored) Add DOM overlay div with ★ HTML characters positioned via timeToCoordinate + priceToCoordinate(candle.high) - 52px; sized by impact_score; colored by category Label (title, 22 chars) rendered above each star; redrawn on scroll/zoom via subscribeVisibleLogicalRangeChange; cleaned up on unmount ChartEvent interface: add category, impact_score, end_date, description Header legend: replace ▼ LT/MT/CT with ★ category color legend - InstrumentDashboard: remove EventStarsStrip (now internal to chart) Remove unused dateToMs helper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,10 @@ interface ChartEvent {
|
|||||||
date: string
|
date: string
|
||||||
title: string
|
title: string
|
||||||
level: string
|
level: string
|
||||||
|
category?: string
|
||||||
|
impact_score?: number
|
||||||
|
end_date?: string | null
|
||||||
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -35,10 +39,22 @@ const MA_COLORS: Record<string, string> = {
|
|||||||
ma200: '#a855f7',
|
ma200: '#a855f7',
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEVEL_COLORS: Record<string, string> = {
|
const CAT_COLORS: Record<string, string> = {
|
||||||
long: '#7c3aed',
|
event_calendar: '#f59e0b',
|
||||||
medium: '#3b82f6',
|
geopolitical: '#ef4444',
|
||||||
short: '#10b981',
|
fundamental: '#10b981',
|
||||||
|
report: '#3b82f6',
|
||||||
|
sentiment: '#8b5cf6',
|
||||||
|
technical: '#06b6d4',
|
||||||
|
}
|
||||||
|
|
||||||
|
const CAT_LABELS: Record<string, string> = {
|
||||||
|
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) {
|
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)
|
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()
|
chart.timeScale().fitContent()
|
||||||
|
|
||||||
|
// ── Star overlay — ★ icons positioned via chart coordinate API ────────
|
||||||
|
const candleMap: Record<string, number> = {}
|
||||||
|
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
|
// Crosshair → date callback
|
||||||
chart.subscribeCrosshairMove((param: any) => {
|
chart.subscribeCrosshairMove((param: any) => {
|
||||||
if (param?.time && typeof param.time === 'string') {
|
if (param?.time && typeof param.time === 'string') {
|
||||||
@@ -173,7 +243,12 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
})
|
})
|
||||||
ro.observe(containerRef.current)
|
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 () => {
|
return () => {
|
||||||
@@ -195,11 +270,11 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
<span className="flex items-center gap-1.5 opacity-40">
|
<span className="flex items-center gap-1.5 opacity-40">
|
||||||
<span className="inline-block w-5 border-t border-dashed border-slate-400" />BB(20,2)
|
<span className="inline-block w-5 border-t border-dashed border-slate-400" />BB(20,2)
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-auto flex items-center gap-3">
|
<span className="ml-auto flex items-center gap-2.5">
|
||||||
{Object.entries(LEVEL_COLORS).map(([l, c]) => (
|
{Object.entries(CAT_COLORS).map(([cat, c]) => (
|
||||||
<span key={l} className="flex items-center gap-1">
|
<span key={cat} className="flex items-center gap-0.5" style={{ color: c }}>
|
||||||
<span style={{ color: c }}>▼</span>
|
<span style={{ fontSize: 10 }}>★</span>
|
||||||
<span>{l === 'long' ? 'LT' : l === 'medium' ? 'MT' : 'CT'}</span>
|
<span style={{ fontSize: 9 }}>{CAT_LABELS[cat]}</span>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
@@ -209,7 +284,7 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
<span className="text-slate-500 text-sm">Chargement...</span>
|
<span className="text-slate-500 text-sm">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div ref={containerRef} style={{ height: `${height}px`, width: '100%' }} />
|
<div ref={containerRef} style={{ height: `${height}px`, width: '100%', position: 'relative' }} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -109,8 +109,6 @@ function fmtDateFR(s: string | null): string {
|
|||||||
return `${d}/${m}/${y}`
|
return `${d}/${m}/${y}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function dateToMs(s: string) { return new Date(s).getTime() }
|
|
||||||
|
|
||||||
function pctN(a: number | undefined, b: number | undefined): number {
|
function pctN(a: number | undefined, b: number | undefined): number {
|
||||||
if (a === undefined || b === undefined || b === 0) return 0
|
if (a === undefined || b === undefined || b === 0) return 0
|
||||||
return ((a - b) / b) * 100
|
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<string, string> = {
|
|
||||||
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 (
|
|
||||||
<div className="relative" style={{ height: 28, paddingRight: 62 }}>
|
|
||||||
{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 (
|
|
||||||
<span
|
|
||||||
key={i}
|
|
||||||
className="absolute -translate-x-1/2 select-none cursor-default transition-transform hover:scale-150"
|
|
||||||
style={{
|
|
||||||
left: `${left}%`,
|
|
||||||
bottom: 2,
|
|
||||||
fontSize: size,
|
|
||||||
color,
|
|
||||||
lineHeight: 1,
|
|
||||||
filter: `drop-shadow(0 0 3px ${color}60)`,
|
|
||||||
}}
|
|
||||||
title={`${ev.title}\n${ev.date}${ev.end_date ? ' → ' + ev.end_date : ''}\n${ev.description ?? ''}`}
|
|
||||||
>
|
|
||||||
★
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── DriversPanel — édition inline ────────────────────────────────────────────
|
// ── DriversPanel — édition inline ────────────────────────────────────────────
|
||||||
|
|
||||||
function DriversPanel({ instrumentId, drivers, onSave, onClose }: {
|
function DriversPanel({ instrumentId, drivers, onSave, onClose }: {
|
||||||
@@ -915,11 +856,6 @@ export default function InstrumentDashboard() {
|
|||||||
{/* ── Content ── */}
|
{/* ── Content ── */}
|
||||||
{!loading && snapshot && (
|
{!loading && snapshot && (
|
||||||
<>
|
<>
|
||||||
<EventStarsStrip
|
|
||||||
events={snapshot.events}
|
|
||||||
priceData={snapshot.price_data}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InstrumentChart
|
<InstrumentChart
|
||||||
priceData={snapshot.price_data}
|
priceData={snapshot.price_data}
|
||||||
indicators={snapshot.indicators}
|
indicators={snapshot.indicators}
|
||||||
|
|||||||
Reference in New Issue
Block a user