feat: instrument analysis
This commit is contained in:
@@ -22,6 +22,9 @@ interface CausalTemplate {
|
|||||||
nodes: { id: string; type: string; label: string; instrument?: string }[]
|
nodes: { id: string; type: string; label: string; instrument?: string }[]
|
||||||
edges: { from: string; to: string; lag_days?: number; lag_min?: number; sign?: 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 {
|
interface InstrumentConfig {
|
||||||
@@ -699,6 +702,8 @@ function makeTdToX(priceData: PriceCandle[], width: number): (d: string) => numb
|
|||||||
// ── Comprehension scoring ─────────────────────────────────────────────────────
|
// ── Comprehension scoring ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
function templateAbsorptionDays(tmpl: CausalTemplate): number {
|
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))
|
const maxLag = Math.max(0, ...tmpl.graph_json.edges.map(e => e.lag_days || 0))
|
||||||
return maxLag > 0 ? maxLag : 30
|
return maxLag > 0 ? maxLag : 30
|
||||||
}
|
}
|
||||||
@@ -863,7 +868,7 @@ function CausalFrise({
|
|||||||
// Build chips (one per event × template)
|
// Build chips (one per event × template)
|
||||||
type Chip = {
|
type Chip = {
|
||||||
ev: SnapshotEvent; tmpl: CausalTemplate
|
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
|
const chips: Chip[] = linked
|
||||||
.filter(ev => ev.date <= maxDate)
|
.filter(ev => ev.date <= maxDate)
|
||||||
@@ -878,7 +883,7 @@ function CausalFrise({
|
|||||||
const x1 = tdToX(ev.date)
|
const x1 = tdToX(ev.date)
|
||||||
const raw = tdToX(endDate) - x1
|
const raw = tdToX(endDate) - x1
|
||||||
const w = Math.max(raw, FRISE_MIN_W)
|
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)
|
.filter((c): c is Chip => c !== null)
|
||||||
.sort((a, b) => a.x1 - b.x1)
|
.sort((a, b) => a.x1 - b.x1)
|
||||||
@@ -939,13 +944,15 @@ function CausalFrise({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Chips */}
|
{/* 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 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 isOpen = activeChip?.ev.id === ev.id && activeChip?.tmpl.id === tmpl.id
|
||||||
const charsFit = Math.floor((w - 18) / 5.5)
|
const charsFit = Math.floor((w - 18) / 5.5)
|
||||||
const label = charsFit < 3 ? '' : tmpl.name.length > charsFit
|
const label = charsFit < 3 ? '' : ev.title.length > charsFit
|
||||||
? tmpl.name.slice(0, charsFit - 1) + '…' : tmpl.name
|
? ev.title.slice(0, charsFit - 1) + '…' : ev.title
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -958,13 +965,13 @@ function CausalFrise({
|
|||||||
: clsx('text-slate-500 border-slate-700/40 bg-slate-800/25',
|
: 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'),
|
'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()}
|
onMouseDown={e => e.stopPropagation()}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setActiveChip(isOpen ? null : { tmpl, ev, chipX: x1, chipY })
|
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')} />
|
<span className={clsx('w-1.5 h-1.5 rounded-full bg-current shrink-0', !active && 'opacity-40')} />
|
||||||
{label && <span className="truncate">{label}</span>}
|
{label && <span className="truncate">{label}</span>}
|
||||||
|
|||||||
Reference in New Issue
Block a user