feat: instrument analysis
This commit is contained in:
@@ -22,6 +22,9 @@ interface CausalTemplate {
|
||||
nodes: { id: string; type: string; label: string; instrument?: string }[]
|
||||
edges: { from: string; to: string; lag_days?: number; lag_min?: number; sign?: string }[]
|
||||
}
|
||||
calibration_json: {
|
||||
absorption_days?: number; lag_days?: number; half_life_days?: number; decay_type?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface InstrumentConfig {
|
||||
@@ -699,6 +702,8 @@ function makeTdToX(priceData: PriceCandle[], width: number): (d: string) => numb
|
||||
// ── Comprehension scoring ─────────────────────────────────────────────────────
|
||||
|
||||
function templateAbsorptionDays(tmpl: CausalTemplate): number {
|
||||
const fromCalib = tmpl.calibration_json?.absorption_days
|
||||
if (fromCalib && fromCalib > 0) return fromCalib
|
||||
const maxLag = Math.max(0, ...tmpl.graph_json.edges.map(e => e.lag_days || 0))
|
||||
return maxLag > 0 ? maxLag : 30
|
||||
}
|
||||
@@ -863,7 +868,7 @@ function CausalFrise({
|
||||
// Build chips (one per event × template)
|
||||
type Chip = {
|
||||
ev: SnapshotEvent; tmpl: CausalTemplate
|
||||
x1: number; x2: number; w: number; active: boolean
|
||||
x1: number; x2: number; w: number; active: boolean; absorptionDays: number
|
||||
}
|
||||
const chips: Chip[] = linked
|
||||
.filter(ev => ev.date <= maxDate)
|
||||
@@ -878,7 +883,7 @@ function CausalFrise({
|
||||
const x1 = tdToX(ev.date)
|
||||
const raw = tdToX(endDate) - x1
|
||||
const w = Math.max(raw, FRISE_MIN_W)
|
||||
return { ev, tmpl, x1, x2: x1 + w, w, active: isActiveAt(ev, selectedDate) }
|
||||
return { ev, tmpl, x1, x2: x1 + w, w, active: isActiveAt(ev, selectedDate), absorptionDays }
|
||||
})
|
||||
.filter((c): c is Chip => c !== null)
|
||||
.sort((a, b) => a.x1 - b.x1)
|
||||
@@ -939,13 +944,15 @@ function CausalFrise({
|
||||
)}
|
||||
|
||||
{/* Chips */}
|
||||
{placed.map(({ ev, tmpl, x1, w, lane, active }) => {
|
||||
{placed.map(({ ev, tmpl, x1, w, lane, active, absorptionDays }) => {
|
||||
const catTw = EV_CAT_TW[ev.category] ?? 'text-slate-400 border-slate-700/30 bg-slate-800/40'
|
||||
const chipY = lane * FRISE_LANE_H + FRISE_CHIP_PAD
|
||||
// Height scales with absorption: 13px (short) → 20px (30+ days), centred in lane
|
||||
const chipH = Math.max(13, Math.min(20, Math.round(13 + Math.min(absorptionDays, 30) / 30 * 7)))
|
||||
const chipY = lane * FRISE_LANE_H + Math.floor((FRISE_LANE_H - chipH) / 2)
|
||||
const isOpen = activeChip?.ev.id === ev.id && activeChip?.tmpl.id === tmpl.id
|
||||
const charsFit = Math.floor((w - 18) / 5.5)
|
||||
const label = charsFit < 3 ? '' : tmpl.name.length > charsFit
|
||||
? tmpl.name.slice(0, charsFit - 1) + '…' : tmpl.name
|
||||
const label = charsFit < 3 ? '' : ev.title.length > charsFit
|
||||
? ev.title.slice(0, charsFit - 1) + '…' : ev.title
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -958,13 +965,13 @@ function CausalFrise({
|
||||
: clsx('text-slate-500 border-slate-700/40 bg-slate-800/25',
|
||||
'hover:text-slate-300 hover:border-slate-600/60 hover:bg-slate-800/50'),
|
||||
)}
|
||||
style={{ left: x1, top: chipY, width: w, height: FRISE_CHIP_H }}
|
||||
style={{ left: x1, top: chipY, width: w, height: chipH }}
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
setActiveChip(isOpen ? null : { tmpl, ev, chipX: x1, chipY })
|
||||
}}
|
||||
title={`${tmpl.name} · ${ev.title}\n${fmtDateFR(ev.date)} → ${ev.end_date ? fmtDateFR(ev.end_date) : '+30j'}`}
|
||||
title={`${ev.title} [${tmpl.name}]\n${fmtDateFR(ev.date)} → ${ev.end_date ? fmtDateFR(ev.end_date) : `+${absorptionDays}j`}`}
|
||||
>
|
||||
<span className={clsx('w-1.5 h-1.5 rounded-full bg-current shrink-0', !active && 'opacity-40')} />
|
||||
{label && <span className="truncate">{label}</span>}
|
||||
|
||||
Reference in New Issue
Block a user