feat: causal lab

This commit is contained in:
OpenSquared
2026-07-01 21:01:00 +02:00
parent 1829335ad1
commit e1681edffc
5 changed files with 774 additions and 445 deletions

View File

@@ -372,6 +372,18 @@ export default function MacroSeriesPage() {
const EVO_COLORS = ['#f59e0b', '#38bdf8', '#a78bfa', '#34d399', '#fb923c', '#f472b6']
// Compute focused event date for the Log tab (used by both chart and table)
const logFocusDate = (() => {
if (!forecastEvolution.allDates.length) return null
const today = new Date().toISOString().slice(0, 10)
const twelveMonthsAgo = new Date(); twelveMonthsAgo.setFullYear(twelveMonthsAgo.getFullYear() - 1)
const cutoff = twelveMonthsAgo.toISOString().slice(0, 10)
const upcoming = forecastEvolution.allDates.filter(d => d >= today).sort()
const recentPast = forecastEvolution.allDates.filter(d => d >= cutoff && d < today).sort().reverse()
const ordered = [...upcoming, ...recentPast]
return selectedEventDate ?? ordered[0] ?? null
})()
return (
<div className="flex h-full overflow-hidden" style={{ minHeight: 0 }}>
{/* Sidebar */}
@@ -531,7 +543,12 @@ export default function MacroSeriesPage() {
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-slate-300">
Journal des changements {selectedId}
<span className="text-slate-500 text-xs ml-2">({logEntries.length} entrées)</span>
{logFocusDate && <span className="text-slate-400 text-xs ml-2">· release {logFocusDate}</span>}
<span className="text-slate-500 text-xs ml-2">
({logFocusDate
? logEntries.filter(e => e.event_date === logFocusDate).length
: logEntries.length} entrées)
</span>
</span>
{logEntries.length === 0 && (
<button onClick={handleBackfill} disabled={backfilling}
@@ -543,18 +560,20 @@ export default function MacroSeriesPage() {
{/* Forecast evolution — focus one release at a time */}
{forecastEvolution.allDates.length > 0 && (() => {
const today = new Date().toISOString().slice(0, 10)
const upcoming = forecastEvolution.allDates.filter(d => d >= today).sort()
const past = forecastEvolution.allDates.filter(d => d < today).sort().reverse()
const ordered = [...upcoming, ...past]
const twelveMonthsAgo = new Date(); twelveMonthsAgo.setFullYear(twelveMonthsAgo.getFullYear() - 1)
const cutoff = twelveMonthsAgo.toISOString().slice(0, 10)
const upcoming = forecastEvolution.allDates.filter(d => d >= today).sort()
const recentPast = forecastEvolution.allDates.filter(d => d >= cutoff && d < today).sort().reverse()
const ordered = [...upcoming, ...recentPast]
const focusDate = selectedEventDate ?? ordered[0] ?? null
if (!focusDate) return null
const allPts = forecastEvolution.byDate[focusDate] ?? []
// Keep only the 4 weeks window before event_date
const cutoff = new Date(focusDate)
cutoff.setDate(cutoff.getDate() - 28)
const cutoffStr = cutoff.toISOString().slice(0, 16)
const pts = allPts.filter(p => p.time >= cutoffStr)
const windowStart = new Date(focusDate)
windowStart.setDate(windowStart.getDate() - 28)
const windowStr = windowStart.toISOString().slice(0, 16)
const pts = allPts.filter(p => p.time >= windowStr)
const chartPts = pts.map(p => ({ time: p.time.slice(0, 10), forecast: p.forecast, actual: p.actual }))
const hasActual = pts.some(p => p.actual != null)
const actualVal = pts.find(p => p.actual != null)?.actual ?? null
@@ -647,7 +666,7 @@ export default function MacroSeriesPage() {
</tr>
</thead>
<tbody>
{[...logEntries].reverse().map((e, i) => {
{[...logEntries].filter(e => !logFocusDate || e.event_date === logFocusDate).reverse().map((e, i) => {
const isActual = e.actual_value != null
const isForecastChange = e.changed === 'forecast' || e.changed === 'both'
return (