feat: market events filters, no-auto-bootstrap, star label deoverlap
- MarketEvents: date presets (7j/30j/3m/6m/1an/Tout/Perso), instrument impact filter (ticker + min score + direction → auto-sort by inst score), sort controls (date/score/nom + asc/desc), clear-all button, count bar - Market events list endpoint: full SQL JOIN rewrite supporting instrument filter, date range, origin, sort_by=instrument_score - Disable auto-bootstrap on startup (macro/eco/categories) — manual only - CycleActions: bootstrap group (Macro/Eco/Categories) with force checkbox, grouped layout (detection / bootstrap / data / ai / portfolio) - cycle_actions router: /bootstrap-macro, /bootstrap-eco, /bootstrap-categories endpoints + group field on all action catalogue entries - InstrumentChart: greedy row placement for star labels (4 rows × 20px) to eliminate horizontal overlap; labels now inline (★ + text) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -175,56 +175,75 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
||||
if (!active) return
|
||||
starsOverlay.innerHTML = ''
|
||||
const cw = containerRef.current?.clientWidth ?? 0
|
||||
const ch = containerRef.current?.clientHeight ?? 0
|
||||
|
||||
// ── Collect visible events with their x coords ──────────────────────
|
||||
type Placed = { xCoord: number; halfW: number; ev: typeof events[0] }
|
||||
const visible: Placed[] = []
|
||||
for (const ev of events) {
|
||||
const xCoord = chart.timeScale().timeToCoordinate(ev.date as any)
|
||||
if (xCoord === null || xCoord < 0 || xCoord > cw) continue
|
||||
const title = ev.title.slice(0, 20)
|
||||
const halfW = Math.max(30, title.length * 6 + 12) // approx px half-width
|
||||
visible.push({ xCoord, halfW, ev })
|
||||
}
|
||||
|
||||
// Place stars at a fixed band near top of chart — never following the bars
|
||||
const highPrice = candleMap[ev.date]
|
||||
let yBase = 22
|
||||
if (highPrice !== undefined) {
|
||||
const yHigh = candleSeries.priceToCoordinate(highPrice)
|
||||
// 130px above bar high, but always capped to top 30px of chart
|
||||
if (yHigh !== null) yBase = Math.min(30, Math.max(8, yHigh - 130))
|
||||
// ── Greedy row placement (up to 4 rows, 20px apart) ─────────────────
|
||||
const ROW_H = 20 // pixels between rows
|
||||
const MAX_ROWS = 4
|
||||
// rowEnds[r] = rightmost x already occupied in row r
|
||||
const rowEnds: number[] = Array(MAX_ROWS).fill(-Infinity)
|
||||
|
||||
// Sort by x so greedy works left-to-right
|
||||
visible.sort((a, b) => a.xCoord - b.xCoord)
|
||||
|
||||
for (const { xCoord, halfW, ev } of visible) {
|
||||
const left = xCoord - halfW
|
||||
const right = xCoord + halfW + 4
|
||||
|
||||
// Find first row where the label fits (no overlap)
|
||||
let row = 0
|
||||
for (let r = 0; r < MAX_ROWS; r++) {
|
||||
if (rowEnds[r] <= left) { row = r; break }
|
||||
if (r === MAX_ROWS - 1) row = r // last resort: use bottom row
|
||||
}
|
||||
rowEnds[row] = right
|
||||
|
||||
const yBase = 6 + row * ROW_H
|
||||
|
||||
const cat = ev.category ?? 'fundamental'
|
||||
const color = CAT_COLORS[cat] ?? '#94a3b8'
|
||||
const score = ev.impact_score ?? 0.5
|
||||
const sz = score >= 0.8 ? 18 : score >= 0.6 ? 15 : 12
|
||||
const sz = score >= 0.8 ? 16 : score >= 0.6 ? 13 : 11
|
||||
|
||||
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:2px`,
|
||||
`display:flex`, `align-items:center`, `gap:3px`,
|
||||
`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:11px`, `font-family:ui-monospace,monospace`,
|
||||
`color:${color}`, `white-space:nowrap`,
|
||||
`text-shadow:0 1px 4px #000,0 0 8px #000,-1px 0 4px #000,1px 0 4px #000`,
|
||||
`line-height:1`, `max-width:110px`,
|
||||
`overflow:hidden`, `text-overflow:ellipsis`,
|
||||
`font-weight:600`,
|
||||
].join(';')
|
||||
lbl.textContent = ev.title.slice(0, 22)
|
||||
|
||||
const star = document.createElement('div')
|
||||
const star = document.createElement('span')
|
||||
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`,
|
||||
`filter:drop-shadow(0 0 3px ${color}80)`,
|
||||
].join(';')
|
||||
star.textContent = '★'
|
||||
|
||||
wrap.appendChild(lbl)
|
||||
const lbl = document.createElement('span')
|
||||
lbl.style.cssText = [
|
||||
`font-size:10px`, `font-family:ui-monospace,monospace`,
|
||||
`color:${color}`, `white-space:nowrap`,
|
||||
`text-shadow:0 1px 4px #000,0 0 6px #000`,
|
||||
`line-height:1`, `max-width:100px`,
|
||||
`overflow:hidden`, `text-overflow:ellipsis`,
|
||||
`font-weight:600`,
|
||||
].join(';')
|
||||
lbl.textContent = ev.title.slice(0, 20)
|
||||
|
||||
wrap.appendChild(star)
|
||||
wrap.appendChild(lbl)
|
||||
starsOverlay.appendChild(wrap)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user