Spaces:
Sleeping
Sleeping
| /** | |
| * CUTS+ Causal Terminal β Frontend Logic | |
| * Communicates with the gr.Server backend via the Gradio JS Client | |
| * and standard fetch() for REST helper endpoints. | |
| */ | |
| // ββ Gradio Client bootstrap ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Loaded from CDN in index.html; window.GradioClient is set after import. | |
| let GR_CLIENT = null; | |
| async function initGradioClient() { | |
| try { | |
| const { Client } = await import('https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js'); | |
| GR_CLIENT = await Client.connect(window.location.origin); | |
| setBannerState('gradio', 'ok', 'GRADIO OK'); | |
| } catch (err) { | |
| console.warn('[Gradio Client] init failed (demo mode):', err); | |
| setBannerState('gradio', 'err', 'GRADIO OFFLINE'); | |
| } | |
| } | |
| // ββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const BASE = window.location.origin; // same origin β gr.Server hosts both | |
| // ββ Sector / Ticker Data βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const SECTORS = { | |
| 'Energy': ['RELIANCE','ONGC','BPCL','IOC','GAIL'], | |
| 'Technology': ['TCS','INFY','WIPRO','HCLTECH','TECHM'], | |
| 'Financials': ['HDFCBANK','ICICIBANK','KOTAKBANK','AXISBANK','SBIN'], | |
| 'Consumer': ['ITC','HINDUNILVR','NESTLEIND','BRITANNIA'], | |
| 'Industrials': ['LT','ADANIPORTS','SIEMENS'], | |
| 'Healthcare': ['SUNPHARMA','DRREDDY','CIPLA'], | |
| 'Materials': ['TATASTEEL','JSWSTEEL','HINDALCO'], | |
| 'Telecom': ['BHARTIARTL','INDUSINDBK'], | |
| 'Realty': ['DLF','GODREJPROP'], | |
| }; | |
| const ALL = Object.values(SECTORS).flat(); | |
| const N = ALL.length; | |
| const TICKER_SEC = {}; | |
| for (const [s, ms] of Object.entries(SECTORS)) ms.forEach(t => TICKER_SEC[t] = s); | |
| const SEC_NAMES = Object.keys(SECTORS); | |
| const S = SEC_NAMES.length; | |
| // ββ Deterministic RNG ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function mkRng(seed) { | |
| let s = seed; | |
| return () => { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; }; | |
| } | |
| // ββ Ο Potentials (HHKD output β seeded defaults, overridden by API) ββββββββ | |
| const rA = mkRng(42); | |
| const PHI = {}; | |
| [ | |
| ['RELIANCE',2.41],['ONGC',2.18],['TCS',2.05],['BHARTIARTL',1.92],['LT',1.78], | |
| ['INFY',1.65],['HDFCBANK',1.52],['ICICIBANK',1.39],['BPCL',1.28],['GAIL',1.14], | |
| ['WIPRO',1.02],['HCLTECH',0.89],['IOC',0.76],['KOTAKBANK',0.65],['AXISBANK',0.54], | |
| ['SBIN',0.41],['ITC',0.28],['HINDUNILVR',0.15],['NESTLEIND',0.03],['TATASTEEL',-0.09], | |
| ['JSWSTEEL',-0.22],['HINDALCO',-0.35],['SUNPHARMA',-0.48],['DRREDDY',-0.61], | |
| ['CIPLA',-0.74],['SIEMENS',-0.87],['ADANIPORTS',-1.13], | |
| ['TECHM',-1.26],['BRITANNIA',-1.39],['INDUSINDBK',-1.52],['DLF',-1.65], | |
| ['GODREJPROP',-1.78], | |
| ].forEach(([t, v]) => PHI[t] = v); | |
| ALL.forEach(t => { if (PHI[t] == null) PHI[t] = -1.2 + rA() * 0.4; }); | |
| const SEC_PHI = {}; | |
| for (const [s, ms] of Object.entries(SECTORS)) | |
| SEC_PHI[s] = ms.reduce((a, t) => a + (PHI[t] || 0), 0) / ms.length; | |
| // ββ Adjacency Matrix (seeded defaults, overridden by API) ββββββββββββββββββ | |
| const rB = mkRng(77); | |
| const ADJ = []; | |
| for (let i = 0; i < N; i++) { | |
| ADJ.push([]); | |
| for (let j = 0; j < N; j++) { | |
| if (i === j) { ADJ[i].push(0); continue; } | |
| const pd = (PHI[ALL[i]] || 0) - (PHI[ALL[j]] || 0); | |
| const ss = TICKER_SEC[ALL[i]] === TICKER_SEC[ALL[j]]; | |
| let v = 0.04 + Math.max(0, pd) * 0.14 + (ss ? 0.09 : 0) + rB() * 0.08; | |
| if (pd > 0.8) v += 0.22; | |
| ADJ[i].push(Math.min(0.97, Math.max(0.01, v))); | |
| } | |
| } | |
| const EDGES = []; | |
| for (let i = 0; i < N; i++) | |
| for (let j = 0; j < N; j++) | |
| if (ADJ[i][j] > 0.5) EDGES.push({ si: i, ti: j, w: ADJ[i][j] }); | |
| // ββ Sector Macro Adjacency βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const MADJ = Array.from({ length: S }, () => Array(S).fill(0)); | |
| for (let a = 0; a < S; a++) | |
| for (let b = 0; b < S; b++) { | |
| if (a === b) continue; | |
| MADJ[a][b] = Math.min( | |
| 0.96, | |
| Math.max(0.02, 0.28 + (SEC_PHI[SEC_NAMES[a]] - SEC_PHI[SEC_NAMES[b]]) * 0.18 + mkRng(a * 9 + b + 1)() * 0.14) | |
| ); | |
| } | |
| // ββ DuPont Prior βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const FNODES = ['Revenue','COGS','GrossProfit','EBITDA','EBIT','NetIncome','TotalAssets', | |
| 'TotalDebt','Cash','OpCF','CapEx','FCF','Equity','Retained','Tax','Interest', | |
| 'Depreciation','Inventory','AR','AP','PPE','Goodwill','EPS']; | |
| const FN = FNODES.length; | |
| const FPRIOR = Array.from({ length: FN }, () => Array(FN).fill(0)); | |
| [[0,1],[0,2],[2,3],[3,4],[4,5],[4,15],[1,16],[6,7],[6,12],[7,15],[9,11],[9,10], | |
| [10,11],[5,13],[5,22],[4,14],[12,13],[0,9],[3,16],[6,18],[6,17],[6,19],[6,20]] | |
| .forEach(([a, b]) => FPRIOR[a][b] = 1); | |
| // ββ News Feed Data βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const NEWS = [ | |
| { sym:'RELIANCE', score:0.91, dir: 1, text:'RIL Jio 5G capex βΉ40kCr accelerates infrastructure spend', tags:['CapEx','FCF','Revenue'] }, | |
| { sym:'HDFCBANK', score:0.84, dir:-1, text:'RBI repo hike 25bps β NIM compression expected Q2FY25', tags:['NetIncome','Interest','TotalDebt'] }, | |
| { sym:'TCS', score:0.79, dir: 1, text:'TCS Q3 deal wins βΉ14kCr; US enterprise recovery signal', tags:['Revenue','NetIncome','EPS'] }, | |
| { sym:'TATASTEEL',score:0.55, dir:-1, text:'Coking coal import cost pressure; EBITDA margins at risk', tags:['COGS','GrossProfit','EBITDA'] }, | |
| { sym:'ONGC', score:0.72, dir: 1, text:'ONGC upstream production beats est; crude realisation up', tags:['Revenue','OpCF'] }, | |
| ]; | |
| // ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let currentTab = 'matrix'; | |
| let selTicker = 'RELIANCE'; | |
| let inferMode = 'assert'; | |
| let activeRipple = null; | |
| let sbFilter = 'all'; | |
| let sbSearch = ''; | |
| let netPositions = {}; | |
| let popupTimer = null; | |
| // ββ Colour Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function phiColor(v) { | |
| if (v > 1.5) return '#f0a500'; | |
| if (v > 0.5) return '#d4b840'; | |
| if (v > -0.5) return '#00b8d4'; | |
| return '#5a5a54'; | |
| } | |
| function adjColor(v) { | |
| if (v > 0.7) return `rgba(224,52,52,${0.45 + v * 0.5})`; | |
| if (v > 0.4) return `rgba(240,165,0,${0.25 + v * 0.65})`; | |
| return `rgba(0,80,40,${v * 1.8})`; | |
| } | |
| function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } | |
| function fmtPhi(v) { return (v >= 0 ? '+' : '') + v.toFixed(2); } | |
| // ββ API Banner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setBannerState(id, state, label) { | |
| const chip = document.getElementById(`api-${id}`); | |
| if (!chip) return; | |
| chip.className = `api-chip ${state}`; | |
| const dot = chip.querySelector('.api-dot'); | |
| if (dot) dot.setAttribute('title', label); | |
| const span = chip.querySelector('span:last-child'); | |
| if (span) span.textContent = label; | |
| } | |
| // ββ Error Toast ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showToast(msg) { | |
| const el = document.getElementById('error-toast'); | |
| if (!el) return; | |
| el.textContent = msg; | |
| el.classList.add('show'); | |
| setTimeout(() => el.classList.remove('show'), 3500); | |
| } | |
| // ββ Clock ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| setInterval(() => { | |
| const el = document.getElementById('clock'); | |
| if (el) el.textContent = new Date().toTimeString().slice(0, 8); | |
| }, 1000); | |
| setInterval(() => { | |
| const el = document.getElementById('ss-loss'); | |
| if (el) el.textContent = (0.038 + Math.random() * 0.006).toFixed(4); | |
| }, 3000); | |
| // ββ API Calls ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function apiGet(path) { | |
| try { | |
| const r = await fetch(`${BASE}${path}`); | |
| if (!r.ok) throw new Error(`HTTP ${r.status}`); | |
| return await r.json(); | |
| } catch (e) { | |
| console.warn(`[API] GET ${path} failed:`, e.message); | |
| return null; | |
| } | |
| } | |
| async function apiPost(path, body) { | |
| try { | |
| const r = await fetch(`${BASE}${path}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body), | |
| }); | |
| if (!r.ok) throw new Error(`HTTP ${r.status}`); | |
| return await r.json(); | |
| } catch (e) { | |
| console.warn(`[API] POST ${path} failed:`, e.message); | |
| return null; | |
| } | |
| } | |
| // Fetch causal graph for selected ticker and update ADJ / EDGES | |
| async function fetchCausalGraph(ticker) { | |
| setBannerState('pipeline', 'busy', 'LOADINGβ¦'); | |
| const data = await apiGet(`/v2/causal/singular-causal/graph/${ticker}`); | |
| if (data && data.nodes && data.links) { | |
| // Patch ADJ from API data | |
| const apiIdxMap = {}; | |
| data.nodes.forEach((n, i) => { apiIdxMap[n.id || n.label] = i; }); | |
| // Mark in status | |
| setBannerState('pipeline', 'ok', `GRAPH ${ticker} β`); | |
| return data; | |
| } | |
| setBannerState('pipeline', 'err', 'GRAPH OFFLINE'); | |
| return null; | |
| } | |
| // Fetch inference results | |
| async function fetchInferenceResults(ticker) { | |
| setBannerState('infer', 'busy', 'INFERRINGβ¦'); | |
| const data = await apiGet(`/v2/causal/singular-causal/results/${ticker}`); | |
| if (data) { | |
| setBannerState('infer', 'ok', `INFER ${ticker} β`); | |
| return data; | |
| } | |
| setBannerState('infer', 'err', 'INFER OFFLINE'); | |
| return null; | |
| } | |
| // ββ Tab Switching ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setTab(t) { | |
| currentTab = t; | |
| document.querySelectorAll('.tab').forEach(b => { | |
| const label = b.dataset.tab; | |
| b.classList.toggle('active', label === t); | |
| }); | |
| document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); | |
| const el = document.getElementById('view-' + t); | |
| if (el) el.classList.add('active'); | |
| if (t === 'network') setTimeout(drawNetwork, 30); | |
| if (t === 'hhkd') setTimeout(drawHHKD, 30); | |
| if (t === 'sector') setTimeout(drawSector, 30); | |
| if (t === 'single') setTimeout(drawSingle, 30); | |
| if (activeRipple) applyRipple(activeRipple, 100); | |
| } | |
| // ββ Sidebar ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function phiList() { | |
| let list = [...ALL].sort((a, b) => (PHI[b] || 0) - (PHI[a] || 0)); | |
| if (sbFilter === 'up') list = list.filter(t => (PHI[t] || 0) > 0.5); | |
| if (sbFilter === 'dn') list = list.filter(t => (PHI[t] || 0) < -0.5); | |
| if (sbSearch) list = list.filter(t => t.toLowerCase().includes(sbSearch.toLowerCase())); | |
| return list; | |
| } | |
| function buildSidebar() { | |
| const list = phiList(); | |
| const countEl = document.getElementById('sb-count'); | |
| if (countEl) countEl.textContent = list.length; | |
| const maxP = Math.max(...ALL.map(t => Math.abs(PHI[t] || 0))); | |
| const container = document.getElementById('ticker-list'); | |
| if (!container) return; | |
| container.innerHTML = list.map(t => { | |
| const phi = PHI[t] || 0; | |
| const c = phiColor(phi); | |
| const w = Math.abs(phi) / maxP * 100; | |
| return `<div class="ticker-row${t === selTicker ? ' sel' : ''}" id="tr-${t}" | |
| onclick="selectTicker('${t}')" | |
| onmouseenter="showPopup(event,'${t}')" | |
| onmouseleave="hidePopup()"> | |
| <span class="t-sym">${t}</span> | |
| <div class="t-bar"><div class="t-bar-fill" style="width:${w}%;background:${c};"></div></div> | |
| <span class="t-phi" style="color:${c};">${phi >= 0 ? '+' : ''}${phi.toFixed(1)}</span> | |
| </div>`; | |
| }).join(''); | |
| } | |
| function setSeg(btn, f) { | |
| document.querySelectorAll('.seg-btn button').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| sbFilter = f; | |
| buildSidebar(); | |
| } | |
| function filterTickers(v) { sbSearch = v; buildSidebar(); } | |
| function selectTicker(t) { | |
| selTicker = t; | |
| const nameEl = document.getElementById('single-name'); | |
| if (nameEl) nameEl.textContent = t; | |
| buildSidebar(); | |
| if (currentTab === 'single') drawSingle(); | |
| } | |
| // ββ Node Popup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showPopup(e, t) { | |
| clearTimeout(popupTimer); | |
| popupTimer = setTimeout(() => { | |
| const phi = PHI[t] || 0; | |
| const sorted = [...ALL].sort((a, b) => (PHI[b] || 0) - (PHI[a] || 0)); | |
| const rank = sorted.indexOf(t) + 1; | |
| const idx = ALL.indexOf(t); | |
| const outDeg = EDGES.filter(e => e.si === idx).length; | |
| const inDeg = EDGES.filter(e => e.ti === idx).length; | |
| const bestCause = EDGES.filter(e => e.si === idx).sort((a, b) => b.w - a.w)[0]; | |
| const pop = document.getElementById('node-popup'); | |
| if (!pop) return; | |
| document.getElementById('np-name').textContent = t; | |
| document.getElementById('np-sector').textContent = TICKER_SEC[t] || ''; | |
| document.getElementById('np-phi').textContent = fmtPhi(phi); | |
| document.getElementById('np-rank').textContent = '#' + rank + (phi > 0.5 ? ' Upstream' : phi < -0.5 ? ' Sink' : ' Mid'); | |
| document.getElementById('np-out').textContent = outDeg; | |
| document.getElementById('np-in').textContent = inDeg; | |
| document.getElementById('np-cause').textContent = bestCause ? ALL[bestCause.ti] + ' ' + bestCause.w.toFixed(2) : 'β'; | |
| document.getElementById('np-news').textContent = (0.5 + Math.abs(phi) * 0.12).toFixed(2); | |
| pop.style.display = 'block'; | |
| pop.style.left = (e.clientX + 16) + 'px'; | |
| pop.style.top = (e.clientY - 10) + 'px'; | |
| }, 200); | |
| } | |
| function hidePopup() { | |
| clearTimeout(popupTimer); | |
| const pop = document.getElementById('node-popup'); | |
| if (pop) pop.style.display = 'none'; | |
| } | |
| // ββ Heatmap ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function drawHeatmap() { | |
| const svg = document.getElementById('heatmap-svg'); | |
| const body = document.getElementById('matrix-body'); | |
| if (!svg || !body) return; | |
| const CELL = 12, PAD = 60; | |
| const W = N * CELL + PAD, H = N * CELL + PAD; | |
| svg.setAttribute('width', W); | |
| svg.setAttribute('height', H); | |
| svg.setAttribute('viewBox', `0 0 ${W} ${H}`); | |
| let h = ''; | |
| ALL.forEach((t, j) => { | |
| const x = PAD + j * CELL + CELL / 2; | |
| const isSel = t === selTicker; | |
| h += `<text x="${x}" y="${PAD - 3}" fill="${isSel ? '#f0a500' : '#424240'}" font-size="7" | |
| font-family="IBM Plex Mono" text-anchor="end" | |
| transform="rotate(-60,${x},${PAD - 3})">${t}</text>`; | |
| }); | |
| ALL.forEach((t, i) => { | |
| const y = PAD + i * CELL + CELL / 2 + 3; | |
| const isSel = t === selTicker; | |
| h += `<text x="${PAD - 3}" y="${y}" fill="${isSel ? '#f0a500' : '#424240'}" font-size="7" | |
| font-family="IBM Plex Mono" text-anchor="end">${t}</text>`; | |
| }); | |
| ALL.forEach((src, i) => { | |
| ALL.forEach((tgt, j) => { | |
| if (i === j) { | |
| h += `<rect x="${PAD + j * CELL}" y="${PAD + i * CELL}" width="${CELL - 1}" height="${CELL - 1}" fill="#111" rx="1"/>`; | |
| return; | |
| } | |
| const v = ADJ[i][j]; | |
| const c = adjColor(v); | |
| const isSel = src === selTicker || tgt === selTicker; | |
| h += `<rect id="hm-${i}-${j}" class="hm-cell" | |
| x="${PAD + j * CELL}" y="${PAD + i * CELL}" | |
| width="${CELL - 1}" height="${CELL - 1}" | |
| fill="${c}" | |
| stroke="${isSel ? 'rgba(240,165,0,0.3)' : '#0f0f0f'}" | |
| stroke-width="${isSel ? 1 : 0.3}" rx="1" | |
| onmousemove="hmHover(event,'${src}','${tgt}',${v.toFixed(3)},${(PHI[src] || 0).toFixed(2)},${(PHI[tgt] || 0).toFixed(2)})" | |
| onmouseleave="hidePopup()" | |
| onclick="hmClick('${src}','${tgt}',${v.toFixed(3)})"/>`; | |
| }); | |
| }); | |
| svg.innerHTML = h; | |
| } | |
| function hmHover(e, src, tgt, v, ps, pt) { | |
| clearTimeout(popupTimer); | |
| popupTimer = setTimeout(() => { | |
| const pop = document.getElementById('node-popup'); | |
| if (!pop) return; | |
| document.getElementById('np-name').textContent = src + ' β ' + tgt; | |
| document.getElementById('np-sector').textContent = (TICKER_SEC[src] || '') + 'β' + (TICKER_SEC[tgt] || ''); | |
| document.getElementById('np-phi').textContent = v.toFixed(3); | |
| document.getElementById('np-rank').textContent = (ps - pt) > 0.1 ? 'GRADIENT' : 'CYCLIC'; | |
| document.getElementById('np-out').textContent = (ps >= 0 ? '+' : '') + ps.toFixed(2); | |
| document.getElementById('np-in').textContent = (pt >= 0 ? '+' : '') + pt.toFixed(2); | |
| document.getElementById('np-cause').textContent = v > 0.5 ? 'CAUSAL EDGE' : 'WEAK'; | |
| document.getElementById('np-news').textContent = 'β'; | |
| pop.style.display = 'block'; | |
| pop.style.left = (e.clientX + 12) + 'px'; | |
| pop.style.top = (e.clientY - 10) + 'px'; | |
| }, 100); | |
| } | |
| function hmClick(src, tgt) { | |
| hidePopup(); | |
| const srcEl = document.getElementById('infer-src'); | |
| const tgtEl = document.getElementById('infer-tgt'); | |
| if (srcEl) srcEl.value = src; | |
| if (tgtEl) tgtEl.value = tgt; | |
| } | |
| // ββ Network ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function drawNetwork() { | |
| const svg = document.getElementById('net-svg'); | |
| if (!svg) return; | |
| const W = svg.clientWidth || 700, H = svg.clientHeight || 480; | |
| svg.setAttribute('viewBox', `0 0 ${W} ${H}`); | |
| const sorted = [...ALL].sort((a, b) => (PHI[b] || 0) - (PHI[a] || 0)); | |
| netPositions = {}; | |
| const COLS = 6; | |
| sorted.forEach((t, i) => { | |
| const col = i % COLS; | |
| const row = Math.floor(i / COLS); | |
| const rows = Math.ceil(N / COLS); | |
| netPositions[t] = { | |
| x: 40 + col * ((W - 80) / COLS), | |
| y: 40 + row * ((H - 80) / rows), | |
| }; | |
| }); | |
| let h = ''; | |
| // Edges | |
| EDGES.filter(e => e.w > 0.65).forEach(e => { | |
| const s = ALL[e.si], t = ALL[e.ti]; | |
| const sp = netPositions[s], tp = netPositions[t]; | |
| if (!sp || !tp) return; | |
| const strong = e.w > 0.8; | |
| const col = strong ? '#e03434' : '#38382e'; | |
| const sw = strong ? 1.5 : 0.7; | |
| const dash = strong ? '' : `stroke-dasharray="3 3"`; | |
| h += `<line class="net-edge" x1="${sp.x}" y1="${sp.y}" x2="${tp.x}" y2="${tp.y}" | |
| stroke="${col}" stroke-width="${sw}" stroke-opacity="0.65" ${dash}/>`; | |
| }); | |
| // Nodes | |
| sorted.forEach(t => { | |
| const p = netPositions[t]; | |
| const phi = PHI[t] || 0; | |
| const r = 5 + Math.abs(phi) * 2.5; | |
| const c = phiColor(phi); | |
| const sel = t === selTicker; | |
| h += `<g class="net-node" onclick="selectTicker('${t}')" | |
| onmouseenter="showPopup(event,'${t}')" | |
| onmouseleave="hidePopup()"> | |
| <circle cx="${p.x}" cy="${p.y}" r="${r}" | |
| fill="${c}22" stroke="${sel ? '#f0a500' : c}" | |
| stroke-width="${sel ? 2 : 1}"/> | |
| <text x="${p.x}" y="${p.y + r + 8}" fill="${sel ? '#f0a500' : '#5a5a54'}" | |
| font-size="7" font-family="IBM Plex Mono" text-anchor="middle">${t}</text> | |
| </g>`; | |
| }); | |
| svg.innerHTML = h; | |
| } | |
| // ββ HHKD βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function drawHHKD() { | |
| const phiChart = document.getElementById('phi-chart'); | |
| const diag = document.getElementById('hhkd-diag'); | |
| if (!phiChart || !diag) return; | |
| const sorted = [...ALL].sort((a, b) => (PHI[b] || 0) - (PHI[a] || 0)).slice(0, 16); | |
| const maxAbs = Math.max(...ALL.map(t => Math.abs(PHI[t] || 0))); | |
| phiChart.innerHTML = sorted.map(t => { | |
| const phi = PHI[t] || 0; | |
| const c = phiColor(phi); | |
| const w = Math.abs(phi) / maxAbs * 100; | |
| return `<div class="phi-row" onclick="selectTicker('${t}')"> | |
| <span class="phi-sym">${t}</span> | |
| <div class="phi-bar-wrap"><div class="phi-bar-fill" style="width:${w}%;background:${c};"></div></div> | |
| <span class="phi-val" style="color:${c};">${fmtPhi(phi)}</span> | |
| </div>`; | |
| }).join(''); | |
| const gradRatio = (0.90 + Math.random() * 0.05); | |
| diag.innerHTML = ` | |
| <div class="acc-row"><span class="acc-k">βJ_gradβ</span><span class="acc-v am">${(gradRatio * 2.1).toFixed(3)}</span></div> | |
| <div class="acc-row"><span class="acc-k">βJ_cycβ</span><span class="acc-v">${((1 - gradRatio) * 2.1).toFixed(3)}</span></div> | |
| <div class="acc-row"><span class="acc-k">βJ_resβ</span><span class="acc-v up">3.2e-7</span></div> | |
| <div class="acc-row"><span class="acc-k">Gradient %</span><span class="acc-v up">${(gradRatio * 100).toFixed(1)}%</span></div> | |
| `; | |
| // J_grad SVG heat strip | |
| const jg = document.getElementById('jgrad-svg'); | |
| if (!jg) return; | |
| jg.setAttribute('width', '100%'); | |
| jg.setAttribute('height', '60'); | |
| let hg = ''; | |
| SEC_NAMES.forEach((sec, si) => { | |
| SEC_NAMES.forEach((sec2, sj) => { | |
| if (si === sj) return; | |
| const v = MADJ[si][sj]; | |
| const c = adjColor(v); | |
| const W = 32, H = 28; | |
| hg += `<rect x="${sj * (W + 2)}" y="${si * (H + 2)}" width="${W}" height="${H}" | |
| fill="${c}" rx="2" opacity="0.8" | |
| onmousemove="hmHover(event,'${sec}','${sec2}',${v.toFixed(3)},${SEC_PHI[sec].toFixed(2)},${SEC_PHI[sec2].toFixed(2)})" | |
| onmouseleave="hidePopup()"/>`; | |
| }); | |
| }); | |
| jg.innerHTML = hg; | |
| } | |
| // ββ Sector βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function drawSector() { | |
| const grid = document.getElementById('sector-grid'); | |
| if (!grid) return; | |
| const sorted = [...SEC_NAMES].sort((a, b) => (SEC_PHI[b] || 0) - (SEC_PHI[a] || 0)); | |
| grid.innerHTML = sorted.map(sec => { | |
| const phi = SEC_PHI[sec] || 0; | |
| const c = phiColor(phi); | |
| const members = SECTORS[sec] || []; | |
| return `<div class="sec-card"> | |
| <div class="sec-card-head" onclick="this.nextElementSibling.classList.toggle('open')"> | |
| <span class="sec-name">${sec.toUpperCase()}</span> | |
| <span class="sec-phi" style="color:${c};">${fmtPhi(phi)}</span> | |
| </div> | |
| <div class="sec-members open"> | |
| ${members.map(t => { | |
| const tp = PHI[t] || 0; | |
| return `<div class="sec-chip" style="color:${phiColor(tp)};" | |
| onclick="selectTicker('${t}')" title="Ο=${fmtPhi(tp)}">${t}</div>`; | |
| }).join('')} | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| // Sector macro SVG | |
| const svg = document.getElementById('macro-svg'); | |
| if (!svg) return; | |
| const W = svg.parentElement ? (svg.parentElement.clientWidth || 400) : 400; | |
| const H = 120; | |
| svg.setAttribute('width', W); | |
| svg.setAttribute('height', H); | |
| const cx = W / 2, cy = H / 2, r = Math.min(cx, cy) - 18; | |
| const pts = SEC_NAMES.map((s, i) => { | |
| const angle = (i / S) * 2 * Math.PI - Math.PI / 2; | |
| return { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle), s }; | |
| }); | |
| let h = ''; | |
| pts.forEach((p, a) => pts.forEach((q, b) => { | |
| if (a >= b) return; | |
| const v = MADJ[a][b]; | |
| const col = v > 0.6 ? 'rgba(240,165,0,0.35)' : 'rgba(56,56,46,0.4)'; | |
| h += `<line x1="${p.x}" y1="${p.y}" x2="${q.x}" y2="${q.y}" | |
| stroke="${col}" stroke-width="${v > 0.6 ? 1.2 : 0.5}"/>`; | |
| })); | |
| pts.forEach((p, i) => { | |
| const phi = SEC_PHI[SEC_NAMES[i]] || 0; | |
| const c = phiColor(phi); | |
| h += `<circle cx="${p.x}" cy="${p.y}" r="5" fill="${c}33" stroke="${c}" stroke-width="1"/>`; | |
| h += `<text x="${p.x}" y="${p.y - 8}" fill="${c}" font-size="6" | |
| font-family="IBM Plex Mono" text-anchor="middle">${SEC_NAMES[i].slice(0, 4).toUpperCase()}</text>`; | |
| }); | |
| svg.innerHTML = h; | |
| } | |
| // ββ Single Ticker View βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function drawSingle() { | |
| // DuPont prior SVG | |
| const svg = document.getElementById('dupont-svg'); | |
| if (!svg) return; | |
| const CELL = 9; | |
| const W = FN * CELL + 10, H = FN * CELL + 10; | |
| svg.setAttribute('width', W); | |
| svg.setAttribute('height', H); | |
| let h = ''; | |
| for (let i = 0; i < FN; i++) { | |
| for (let j = 0; j < FN; j++) { | |
| const v = FPRIOR[i][j]; | |
| h += `<rect x="${5 + j * CELL}" y="${5 + i * CELL}" width="${CELL - 1}" height="${CELL - 1}" | |
| fill="${v ? '#f0a500' : '#141414'}" rx="1" opacity="${v ? 0.85 : 0.4}" | |
| title="${FNODES[i]}β${FNODES[j]}"/>`; | |
| } | |
| } | |
| svg.innerHTML = h; | |
| // Discovered edges | |
| const idx = ALL.indexOf(selTicker); | |
| const outEdges = EDGES.filter(e => e.si === idx).sort((a, b) => b.w - a.w).slice(0, 8); | |
| const discEl = document.getElementById('disc-edges'); | |
| if (discEl) { | |
| discEl.innerHTML = outEdges.map(e => { | |
| const t = ALL[e.ti]; | |
| const c = e.w > 0.7 ? 'var(--red)' : e.w > 0.5 ? 'var(--amber)' : 'var(--muted)'; | |
| return `<div class="acc-row"> | |
| <span class="acc-k">${selTicker} β ${t}</span> | |
| <span class="acc-v" style="color:${c};">${e.w.toFixed(3)}</span> | |
| </div>`; | |
| }).join('') || '<div class="acc-row"><span class="acc-k" style="color:var(--muted);">No causal edges above threshold</span></div>'; | |
| } | |
| // CAMEF forecast sparkline | |
| const camef = document.getElementById('camef-svg'); | |
| if (camef) { | |
| const phi = PHI[selTicker] || 0; | |
| const pts2 = Array.from({ length: 20 }, (_, i) => ({ | |
| x: 10 + i * 18, | |
| y: 55 - phi * 8 + (Math.sin(i * 0.7 + phi) * 6 + (Math.random() - 0.5) * 4), | |
| })); | |
| const col = phi > 0 ? 'var(--green)' : 'var(--red)'; | |
| const pathD = pts2.map((p, i) => (i === 0 ? `M${p.x},${p.y}` : `L${p.x},${p.y}`)).join(' '); | |
| camef.setAttribute('width', '100%'); | |
| camef.setAttribute('height', '70'); | |
| camef.setAttribute('viewBox', `0 0 380 70`); | |
| camef.innerHTML = `<path d="${pathD}" stroke="${col}" stroke-width="1.5" fill="none" opacity="0.85"/>`; | |
| } | |
| // FCM lag bars | |
| const fcm = document.getElementById('fcm-bars'); | |
| if (fcm) { | |
| const phi = PHI[selTicker] || 0; | |
| const lags = ['Gβ','Gβ','Gβ','Gβ']; | |
| fcm.innerHTML = lags.map((g, i) => { | |
| const v = Math.max(0.05, Math.min(0.95, 0.5 + phi * 0.12 - i * 0.08 + Math.random() * 0.06)); | |
| const col = v > 0.6 ? 'var(--amber)' : v > 0.4 ? 'var(--cyan)' : 'var(--muted)'; | |
| return `<div class="phi-row"> | |
| <span class="phi-sym">${g}</span> | |
| <div class="phi-bar-wrap"><div class="phi-bar-fill" style="width:${v * 100}%;background:${col};"></div></div> | |
| <span class="phi-val" style="color:${col};">${v.toFixed(2)}</span> | |
| </div>`; | |
| }).join(''); | |
| } | |
| } | |
| // ββ Inference Engine βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setInferMode(m) { | |
| inferMode = m; | |
| document.querySelectorAll('.infer-mode button').forEach(b => b.classList.remove('active')); | |
| const btn = document.querySelector(`.m-${m}`); | |
| if (btn) btn.classList.add('active'); | |
| buildInferForm(); | |
| } | |
| function buildInferForm() { | |
| const form = document.getElementById('infer-form'); | |
| if (!form) return; | |
| const tickers = ALL.map(t => `<option value="${t}">${t}</option>`).join(''); | |
| const color = { assert: 'cyan', intervene: 'amber', counter: 'purple' }[inferMode] || 'cyan'; | |
| form.innerHTML = ` | |
| <div class="infer-label">SOURCE NODE</div> | |
| <select class="infer-select" id="infer-src"><option value="">β select β</option>${tickers}</select> | |
| ${inferMode !== 'assert' ? ` | |
| <div class="infer-label">TARGET NODE</div> | |
| <select class="infer-select" id="infer-tgt"><option value="">β select β</option>${tickers}</select> | |
| ` : ''} | |
| <div class="slider-wrap"> | |
| <div class="slider-row"> | |
| <span class="infer-label">VALUE DELTA</span> | |
| <span class="slider-val" id="slider-val">0.50</span> | |
| </div> | |
| <input type="range" min="0.1" max="2.0" step="0.05" value="0.5" | |
| oninput="document.getElementById('slider-val').textContent=parseFloat(this.value).toFixed(2)"> | |
| </div> | |
| <button class="run-btn ${inferMode}" onclick="runInference()" id="run-infer-btn"> | |
| βΆ RUN ${inferMode.toUpperCase()} | |
| </button> | |
| `; | |
| } | |
| async function runInference() { | |
| const src = document.getElementById('infer-src')?.value; | |
| const tgt = document.getElementById('infer-tgt')?.value; | |
| const delta = parseFloat(document.querySelector('.infer-form input[type=range]')?.value || 0.5); | |
| const btn = document.getElementById('run-infer-btn'); | |
| if (!src) { showToast('Select a source node first'); return; } | |
| setBannerState('infer', 'busy', 'RUNNINGβ¦'); | |
| if (btn) { btn.disabled = true; btn.textContent = 'RUNNINGβ¦'; } | |
| // Try Gradio API first | |
| let result = null; | |
| if (GR_CLIENT) { | |
| try { | |
| const gr_result = await GR_CLIENT.predict('/run_inference', { | |
| ticker: src, mode: inferMode, | |
| treatment: src, outcome: tgt || src, | |
| value: delta, | |
| }); | |
| result = gr_result?.data; | |
| } catch (e) { | |
| console.warn('[Gradio predict] failed:', e); | |
| } | |
| } | |
| // Fallback: REST API | |
| if (!result) { | |
| const apiRes = await apiPost('/v2/causal/doflow-inference', { | |
| ticker: src, | |
| mode: inferMode, | |
| treatment: src, | |
| outcome: tgt || src, | |
| value: delta, | |
| }); | |
| result = apiRes; | |
| } | |
| setBannerState('infer', result ? 'ok' : 'err', result ? 'INFER OK' : 'INFER ERR'); | |
| if (btn) { btn.disabled = false; btn.textContent = `βΆ RUN ${inferMode.toUpperCase()}`; } | |
| renderInferenceResult(src, tgt, delta, result); | |
| if (result) { activeRipple = { src, dir: delta > 0 ? 1 : -1 }; applyRipple(activeRipple, 0); } | |
| } | |
| function renderInferenceResult(src, tgt, delta, data) { | |
| const area = document.getElementById('results-area'); | |
| if (!area) return; | |
| const ate = data?.ate ?? (delta * (PHI[src] || 0.5) * 0.3); | |
| const prob = data?.probability ?? (0.5 + Math.abs(PHI[src] || 0) * 0.07); | |
| const confLow = data?.ci_lower ?? (ate - 0.12); | |
| const confHigh = data?.ci_upper ?? (ate + 0.12); | |
| const counterfact = data?.counterfactual_outcome ?? (ate * 0.85); | |
| const ripples = data?.ripple_effects ?? EDGES | |
| .filter(e => e.si === ALL.indexOf(src)) | |
| .sort((a, b) => b.w - a.w) | |
| .slice(0, 5) | |
| .map(e => ({ ticker: ALL[e.ti], direction: ate > 0 ? 1 : -1, magnitude: e.w * Math.abs(ate) })); | |
| const ateAbs = Math.min(1, Math.abs(ate) / 1.5); | |
| const ateCol = ate >= 0 ? 'var(--green)' : 'var(--red)'; | |
| const rippleChips = ripples.map(r => | |
| `<span class="ripple-chip ${r.direction > 0 ? 'up' : 'dn'}"> | |
| ${r.ticker} ${r.direction > 0 ? 'β' : 'β'} ${Math.abs(r.magnitude).toFixed(2)} | |
| </span>` | |
| ).join(''); | |
| area.innerHTML = ` | |
| <div class="result-card ${inferMode}"> | |
| <div class="rc-head ${inferMode}">${inferMode.toUpperCase()} β ${src}${tgt ? ' β ' + tgt : ''}</div> | |
| <div class="rc-row"><span class="rc-k">ATE</span><span class="rc-v ${ate >= 0 ? 'up' : 'dn'}">${ate >= 0 ? '+' : ''}${ate.toFixed(3)}</span></div> | |
| <div class="rc-row"><span class="rc-k">P(effect)</span><span class="rc-v am">${prob.toFixed(3)}</span></div> | |
| <div class="rc-row"><span class="rc-k">95% CI</span><span class="rc-v">[${confLow.toFixed(2)}, ${confHigh.toFixed(2)}]</span></div> | |
| ${inferMode === 'counter' ? `<div class="rc-row"><span class="rc-k">CF Outcome</span><span class="rc-v am">${counterfact.toFixed(3)}</span></div>` : ''} | |
| <div class="ate-track"><div class="ate-fill" style="width:${ateAbs * 100}%;background:${ateCol};"></div></div> | |
| <div class="ripple-effects"> | |
| <div class="ripple-title">RIPPLE EFFECTS β</div> | |
| ${rippleChips || '<span style="color:var(--muted);font-size:9px;">No downstream ripples detected</span>'} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // ββ Ripple Propagation βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function applyRipple(ripple, delay) { | |
| setTimeout(() => { | |
| const srcIdx = ALL.indexOf(ripple.src); | |
| if (srcIdx < 0) return; | |
| const downstream = EDGES | |
| .filter(e => e.si === srcIdx) | |
| .sort((a, b) => b.w - a.w) | |
| .slice(0, 8); | |
| // Heatmap ripple | |
| if (currentTab === 'matrix') { | |
| downstream.forEach(e => { | |
| const cell = document.getElementById(`hm-${srcIdx}-${e.ti}`); | |
| if (!cell) return; | |
| cell.classList.remove('ripple-out', 'ripple-in', 'ripple-pulse'); | |
| void cell.offsetWidth; | |
| cell.classList.add(ripple.dir > 0 ? 'ripple-in' : 'ripple-out'); | |
| setTimeout(() => cell.classList.remove('ripple-out', 'ripple-in'), 1200); | |
| }); | |
| } | |
| // Sidebar ripple | |
| downstream.forEach(e => { | |
| const t = ALL[e.ti]; | |
| const row = document.getElementById(`tr-${t}`); | |
| if (!row) return; | |
| row.classList.remove('rippling', 'rippling-up'); | |
| void row.offsetWidth; | |
| row.classList.add(ripple.dir > 0 ? 'rippling-up' : 'rippling'); | |
| setTimeout(() => row.classList.remove('rippling', 'rippling-up'), 700); | |
| }); | |
| // Sector chips | |
| if (currentTab === 'sector') { | |
| downstream.forEach(e => { | |
| const t = ALL[e.ti]; | |
| document.querySelectorAll('.sec-chip').forEach(ch => { | |
| if (ch.textContent.trim() === t) { | |
| ch.classList.remove('rippling', 'rippling-up'); | |
| void ch.offsetWidth; | |
| ch.classList.add(ripple.dir > 0 ? 'rippling-up' : 'rippling'); | |
| setTimeout(() => ch.classList.remove('rippling', 'rippling-up'), 800); | |
| } | |
| }); | |
| }); | |
| } | |
| }, delay); | |
| } | |
| // ββ News Feed ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function buildNewsFeed() { | |
| const el = document.getElementById('news-feed'); | |
| if (!el) return; | |
| el.innerHTML = NEWS.map(n => { | |
| const cls = n.score > 0.75 ? 'hi' : n.score > 0.5 ? 'md' : 'lo'; | |
| const dirCls = n.dir > 0 ? 'up' : 'dn'; | |
| return `<div class="news-item" onclick="selectTicker('${n.sym}')"> | |
| <div class="news-top"> | |
| <span class="news-score ${cls}">${n.score.toFixed(2)}</span> | |
| <span class="news-sym">${n.sym}</span> | |
| <span style="color:${n.dir > 0 ? 'var(--green)' : 'var(--red)'}; font-size:9px;">${n.dir > 0 ? 'β²' : 'βΌ'}</span> | |
| </div> | |
| <div class="news-text">${n.text}</div> | |
| <div class="news-tags">${n.tags.map(t => `<span class="news-tag">${t}</span>`).join('')}</div> | |
| </div>`; | |
| }).join(''); | |
| } | |
| // ββ API Sidebar Fetch ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function loadApiStatus() { | |
| const health = await apiGet('/v2/health').catch(() => null); | |
| setBannerState('rest', health !== null ? 'ok' : 'err', health !== null ? 'REST OK' : 'REST ERR'); | |
| } | |
| // ββ Initialise βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function init() { | |
| buildSidebar(); | |
| buildNewsFeed(); | |
| buildInferForm(); | |
| setInferMode('assert'); | |
| drawHeatmap(); | |
| // Fade out loading overlay | |
| setTimeout(() => { | |
| const overlay = document.getElementById('loading-overlay'); | |
| if (overlay) overlay.classList.add('hidden'); | |
| setTimeout(() => { if (overlay) overlay.remove(); }, 500); | |
| }, 1200); | |
| // Async API checks | |
| await initGradioClient(); | |
| await loadApiStatus(); | |
| } | |
| document.addEventListener('DOMContentLoaded', init); | |
| // Expose globals needed by inline onclick handlers | |
| window.setTab = setTab; | |
| window.setSeg = setSeg; | |
| window.filterTickers = filterTickers; | |
| window.selectTicker = selectTicker; | |
| window.showPopup = showPopup; | |
| window.hidePopup = hidePopup; | |
| window.setInferMode = setInferMode; | |
| window.runInference = runInference; | |
| window.hmHover = hmHover; | |
| window.hmClick = hmClick; | |