| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0b1528"> |
| <meta name="description" content="BudgetForge: local-first, exact budget portfolio optimization and equity planning with reproducible sensitivity analysis."> |
| <title>BudgetForge | Strategic Budget Intelligence Studio</title> |
| <style> |
| :root{--bg:#08111f;--sidebar:#0d182a;--panel:#101d31;--panel2:#14233a;--border:#26364e;--text:#e8eef9;--muted:#9cacc2;--accent:#59ddb6;--accent2:#64a5f5;--warning:#ffc37e;--danger:#fb8999;--shadow:0 12px 40px rgba(2,7,20,.24)} |
| *{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;font:14px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:var(--bg);color:var(--text)}button,input,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.5}a{color:var(--accent2)}.app{display:grid;grid-template-columns:246px minmax(0,1fr);min-height:100vh}.side{background:var(--sidebar);border-right:1px solid var(--border);padding:26px 15px;position:sticky;top:0;height:100vh;display:flex;flex-direction:column}.brand{display:flex;gap:11px;align-items:center;padding:3px 8px 25px}.logo{width:39px;height:39px;border-radius:12px;display:grid;place-items:center;font-size:21px;color:#041525;background:linear-gradient(130deg,#5ee5b2,#84b9ff)}.brand strong{font-size:18px;letter-spacing:-.6px}.brand small,.subdued{color:var(--muted);font-size:11px;display:block}.section-label{font-size:10px;color:#7c90ab;letter-spacing:1.4px;text-transform:uppercase;padding:8px 12px;margin:9px 0 6px;font-weight:800}.nav{display:grid;gap:5px}.nav button{background:none;border:1px solid transparent;color:#afbed2;text-align:left;padding:12px 13px;border-radius:10px;display:flex;align-items:center;gap:10px;font-weight:600}.nav button.active{background:#163347;border-color:#295b62;color:#77ebc6}.nav button:hover{background:#172840;color:#fff}.nav .nicon{width:22px;font-size:16px;text-align:center}.side-bottom{margin-top:auto;padding:15px 10px;color:var(--muted);font-size:11px;border-top:1px solid var(--border)}.side-bottom b{color:#e3f1fa}.main{padding:28px clamp(17px,3.3vw,53px) 48px;min-width:0;max-width:1700px;width:100%;margin:0 auto}.topbar{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:27px}.crumb{color:#a8bbd4;font-size:12px}.topright{display:flex;align-items:center;gap:9px}.chip{border:1px solid #2c725f;background:#12392f;padding:6px 11px;border-radius:99px;color:#8aedd0;font-size:11px;font-weight:750;white-space:nowrap}.chip.secondary{border-color:#344967;background:#13243c;color:#b8cef3}.btn{background:var(--accent);border:1px solid #7ee5c9;color:#05231f;border-radius:9px;padding:10px 14px;font-weight:800;display:inline-flex;align-items:center;justify-content:center;gap:7px}.btn:hover{filter:brightness(1.08)}.btn.alt{background:#172841;border-color:#395171;color:#cbdcf3}.btn.small{padding:7px 10px;font-size:12px}.btn.danger{background:#3c2030;color:#ffe1e5;border-color:#794053}h1{font-size:clamp(26px,2.7vw,39px);letter-spacing:-1.35px;line-height:1.1;margin:0 0 11px;font-weight:830}h2{font-size:17px;letter-spacing:-.4px;margin:0 0 5px}h3{font-size:13px;margin:0 0 6px}.muted{color:var(--muted)}.lead{max-width:750px;color:var(--muted);margin:0 0 24px}.headline{display:flex;justify-content:space-between;align-items:start;gap:20px}.title-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.small-note{font-size:11px;color:var(--muted)}.controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.formfield{display:flex;flex-direction:column;gap:5px;min-width:120px}.formfield label,.range-label{font-size:11px;color:#b5c5da;font-weight:700}select,input[type=number],input[type=text],input[type=search]{background:#0b182a;color:var(--text);border:1px solid #364a65;border-radius:8px;padding:9px 11px;min-width:0;width:100%;outline:none}input:focus,select:focus{border-color:#62d9b8;box-shadow:0 0 0 2px #4acfa62a}input[type=range]{width:100%;accent-color:var(--accent)}.grid{display:grid;gap:15px}.kpis{grid-template-columns:repeat(4,minmax(0,1fr));margin:22px 0}.card{background:var(--panel);border:1px solid var(--border);border-radius:14px;box-shadow:var(--shadow);min-width:0}.cardpad{padding:20px}.kpi{padding:19px 20px;position:relative;overflow:hidden}.kpi:before{position:absolute;content:"";height:3px;background:linear-gradient(90deg,#51ddb3,transparent);left:0;right:0;top:0}.kpi .label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;font-weight:800}.kpi .num{font-size:29px;font-weight:820;letter-spacing:-1.3px;margin:8px 0 2px;white-space:nowrap}.kpi .detail{color:#a7bbcd;font-size:11px}.split{grid-template-columns:minmax(0,1.48fr) minmax(310px,1fr)}.cols3{grid-template-columns:repeat(3,minmax(0,1fr))}.cardheader{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:16px}.eyebrow{font-size:10px;letter-spacing:1.5px;font-weight:800;color:#79dbba;text-transform:uppercase;margin-bottom:8px}.pill{border-radius:99px;border:1px solid #345774;background:#152b40;padding:4px 9px;color:#c7d9ec;font-size:10px;white-space:nowrap}.pill.good{color:#73ebbe;border-color:#326b61;background:#173b35}.pill.bad{color:#ffb2b2;border-color:#74505d;background:#402432}.progress{height:9px;background:#21344c;border-radius:20px;overflow:hidden}.progress>div{height:100%;background:linear-gradient(90deg,#65d9ad,#5ca6ed);border-radius:20px}.stat-row{display:flex;justify-content:space-between;align-items:center;gap:13px;margin:11px 0 5px}.stat-row b{font-size:12px}.stat-row span{font-size:12px;color:var(--muted)}.legend{display:flex;flex-wrap:wrap;gap:12px;margin-top:12px;color:var(--muted);font-size:11px}.dot{display:inline-block;width:9px;height:9px;border-radius:3px;margin-right:6px;background:#65ddb7}.dot.blue{background:#77aafa}.dot.orange{background:#ffc17f}.notice{background:#14293a;border:1px solid #30546a;padding:12px 14px;border-radius:10px;font-size:12px;color:#c1d5e8;margin:12px 0}.notice.warn{background:#392a20;border-color:#795f45;color:#ffe0b4}.empty{padding:25px;text-align:center;color:var(--muted)}.table-wrap{overflow:auto;border:1px solid var(--border);border-radius:10px}table{width:100%;border-collapse:collapse;text-align:left;white-space:nowrap;font-size:12px}th{background:#16243b;color:#a8bed7;letter-spacing:.45px;font-size:10px;text-transform:uppercase}th,td{padding:11px 12px;border-bottom:1px solid #26364b}tbody tr:last-child td{border-bottom:0}tbody tr:hover{background:#1a2d44}td b{font-size:12px}.status{padding:4px 8px;border-radius:8px;background:#244937;color:#aaf6d4;font-size:10px;font-weight:800}.status.out{background:#253149;color:#a2b3ce}.two{display:grid;grid-template-columns:1fr 1fr;gap:15px}.slider-row{border-bottom:1px solid #26364b;padding:12px 0}.slider-row:last-child{border:0}.slider-top{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:7px}.slider-top b{font-size:12px}.slider-top span{font-size:12px;color:var(--accent);font-weight:800}.mini-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.metric-mini{padding:14px;border-radius:10px;background:#162840;border:1px solid #2a405a}.metric-mini strong{display:block;font-size:19px}.metric-mini span{font-size:10px;color:#a6bbd1}.select-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px}.scenario-btn{border-radius:10px;background:#102139;border:1px solid #34506a;color:#b6cee7;padding:13px;text-align:left}.scenario-btn strong{display:block;font-size:12px;color:#f3f8fc}.scenario-btn small{display:block;color:#8babc9;margin-top:4px}.scenario-btn.selected{border-color:#62ddb9;background:#1a3e3e}.bar-chart{display:grid;gap:12px}.barline{display:grid;grid-template-columns:minmax(120px,1.3fr) minmax(120px,2fr) 64px;gap:11px;align-items:center;font-size:11px}.bar-track{height:12px;border-radius:20px;background:#20334b;overflow:hidden}.bar-track div{height:100%;border-radius:20px;background:linear-gradient(90deg,#3c987f,#79eac8)}.bar-track .dim{background:#567194}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.hide{display:none!important}.page{display:none}.page.active{display:block}.divider{height:1px;background:var(--border);margin:17px 0}.download-actions{display:flex;gap:9px;flex-wrap:wrap}.summary{display:flex;gap:16px;align-items:center;flex-wrap:wrap}.summary strong{font-size:23px}.info-tile{border:1px solid #2e425e;border-radius:10px;padding:14px;background:#14253a}.flow-note{margin-top:12px;font-size:12px;color:#8eaac0}.switch{display:flex;align-items:center;gap:8px;font-size:11px}.switch input{accent-color:var(--accent)}.toast{position:fixed;right:22px;bottom:20px;background:#193a34;border:1px solid #57b99d;padding:12px 17px;border-radius:11px;color:#e7fff7;box-shadow:0 10px 30px #0009;z-index:100;font-size:12px;max-width:min(400px,90vw)}.footer{margin:30px 0 0;text-align:center;color:#71869f;font-size:11px}.scatter{width:100%;height:auto;max-height:340px;display:block}.scroll-tabs{display:none}button:focus-visible{outline:2px solid #86f7d9;outline-offset:3px}.help{color:#8db0cd;font-size:11px;line-height:1.6}.number-edit{max-width:78px;padding:6px!important}#importFile{max-width:100%;font-size:12px;color:#d4e4f4}#importFile::file-selector-button{padding:7px;border-radius:8px;margin-right:12px;border:1px solid #54728c;background:#16364b;color:#b7dff1;cursor:pointer} |
| @media(max-width:1150px){.app{grid-template-columns:205px minmax(0,1fr)}.side{padding:20px 10px}.split{grid-template-columns:1fr}.kpis{grid-template-columns:repeat(2,1fr)}} |
| @media(max-width:760px){.app{display:block}.side{position:static;height:auto;padding:13px 15px;border-right:0;border-bottom:1px solid var(--border)}.brand{padding:0}.section-label,.side-bottom,.side .nav{display:none}.main{padding:18px 15px 34px}.topbar{margin-bottom:18px}.topright .chip.secondary{display:none}.headline{display:block}.title-actions{margin-top:15px;justify-content:start}.kpis{grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;margin:17px 0}.kpi{padding:14px}.kpi .num{font-size:23px}.grid{gap:11px}.cardpad{padding:16px}.split,.two,.cols3,.select-grid{grid-template-columns:1fr}.mini-metrics{grid-template-columns:repeat(2,1fr)}.scroll-tabs{display:flex;overflow:auto;gap:7px;padding:10px 0 3px;scrollbar-width:thin}.scroll-tabs button{flex:0 0 auto;background:#142840;border:1px solid #33516d;border-radius:9px;padding:9px 13px;color:#c7d7e7;font-size:11px}.scroll-tabs button.active{border-color:#62ddb9;color:#77f4ce;background:#1a3b3b}.barline{grid-template-columns:minmax(90px,1fr) minmax(100px,1fr) 50px;gap:6px}.lead{margin-bottom:16px}.headline h1{font-size:29px}} |
| </style> |
| </head><body> |
| <div class="app"> |
| <aside class="side"><div class="brand"><div class="logo">◈</div><div><strong>BudgetForge</strong><small>Decision Intelligence Studio</small></div></div><div class="section-label">Workspace</div><nav class="nav" id="nav"></nav><div class="side-bottom"><b>LOCAL-FIRST ANALYSIS</b><br>All calculations run in your browser. No server or account required.<br><br>Version 1.0 · Synthetic demonstrations</div></aside> |
| <main class="main"><div class="topbar"><div class="crumb">Research toolkit <span style="color:#66859e">/</span> Project #72 <span style="color:#66859e">/</span> BudgetForge</div><div class="topright"><span class="chip secondary">● Reproducible models</span><span class="chip">● Offline ready</span></div></div> |
| <div class="scroll-tabs" id="mobileNav" aria-label="Modules"></div> |
| <header class="headline"><div><div class="eyebrow">PUBLIC FINANCE · RESOURCE ALLOCATION · DECISION SCIENCE</div><h1 id="pageTitle">Budget Intelligence</h1><p class="lead" id="pageDesc">Optimize a portfolio of projects under a fixed budget, equity commitments and transparent scoring rules.</p></div><div class="title-actions"><button class="btn alt small" id="resetBtn">↺ Reset scenario</button><button class="btn small" id="downloadReport">↓ Download report</button></div></header> |
| <div class="controls" style="margin:0 0 12px"><div class="formfield" style="min-width:210px"><label for="scenarioSelect">DEMONSTRATION DATASET</label><select id="scenarioSelect"></select></div><div class="formfield" style="min-width:126px"><label for="budgetInput">BUDGET (THOUSANDS USD)</label><input type="number" id="budgetInput" min="0" max="10000" step="1"></div><div class="formfield" style="min-width:140px"><label for="quotaInput">PRIORITY AREA MIN. (%)</label><input type="number" id="quotaInput" min="0" max="100" step="5"></div></div> |
| <div class="page active" id="view-overview"></div><div class="page" id="view-portfolio"></div><div class="page" id="view-allocation"></div><div class="page" id="view-equity"></div><div class="page" id="view-sensitivity"></div><div class="page" id="view-data"></div> |
| <div class="footer">BUDGETFORGE v1.0 · PROJECT #72 · MIT · EDUCATIONAL, SYNTHETIC DATA ONLY · NOT FISCAL OR LEGAL ADVICE</div></main></div> |
| <div role="status" aria-live="polite" id="toast" class="toast hide"></div> |
| <script id="engine"> |
| |
| const Engine=(()=>{ |
| const clamp=(x,lo,hi)=>Math.max(lo,Math.min(hi,x)); |
| const round=(v,n=2)=>Math.round((v+Number.EPSILON)*10**n)/10**n; |
| const allowed=['Priority','General']; |
| const raw=[ |
| {key:'municipal',name:'Municipal Services',subtitle:'Community development & infrastructure',budget:145,quota:25,programs:[ |
| ['M01','Primary care outreach','Priority',35,94,85,24,91,3100],['M02','Public transport links','General',40,88,73,35,87,4500],['M03','Rural water repairs','Priority',28,95,94,27,79,2600],['M04','Neighborhood digital hubs','Priority',25,82,72,20,83,2100],['M05','City data portal','General',20,76,68,18,96,900],['M06','School facility upgrades','General',34,91,82,25,86,3900],['M07','Accessible bus stops','Priority',22,83,80,18,92,1700],['M08','Park maintenance','General',19,65,55,19,92,1500],['M09','Emergency response kits','Priority',30,87,95,22,83,3000],['M10','Street lighting retrofit','General',26,79,77,14,94,2600]]}, |
| {key:'university',name:'University Research',subtitle:'Academic programs & capacity',budget:120,quota:30,programs:[ |
| ['U01','Student writing lab','Priority',25,91,87,19,91,1800],['U02','Open research data','General',20,83,70,22,88,1250],['U03','Rural student grants','Priority',31,97,95,15,89,1650],['U04','AI teaching training','General',24,81,73,32,91,2150],['U05','Digital library access','General',34,90,82,17,94,2850],['U06','Academic integrity centre','General',23,83,86,21,85,1700],['U07','Community field methods','Priority',21,89,90,31,81,950],['U08','Research equipment','General',30,86,74,42,71,1400],['U09','Inclusive tutoring','Priority',19,88,91,16,94,1600],['U10','International seminars','General',18,67,43,29,79,620]]}, |
| {key:'climate',name:'Climate Resilience',subtitle:'Illustrative local adaptation initiatives',budget:185,quota:30,programs:[ |
| ['C01','Flood early warnings','Priority',45,94,94,33,83,4200],['C02','Urban tree corridors','General',26,80,61,20,91,2100],['C03','Watershed restoration','Priority',39,93,91,29,78,3600],['C04','Cool roofing pilot','General',29,79,77,18,92,2400],['C05','Drought assistance','Priority',40,96,94,35,79,3200],['C06','Resilient community centres','Priority',37,84,82,27,85,2600],['C07','Climate data dashboard','General',22,75,68,17,94,1200],['C08','Storm drain upgrades','General',49,92,87,33,82,4400],['C09','Heat awareness','Priority',18,78,74,15,98,1500],['C10','Community solar backup','General',35,84,82,28,84,2800]]} |
| ]; |
| const demos=raw.map(s=>({...s,programs:s.programs.map(a=>({id:a[0],name:a[1],area:a[2],cost:a[3],impact:a[4],urgency:a[5],risk:a[6],readiness:a[7],people:a[8]}))})); |
| const defaultWeights={impact:40,efficiency:25,readiness:20,risk:15}; |
| function validate(ps){if(!Array.isArray(ps)||ps.length<2||ps.length>14)throw Error('Provide between 2 and 14 projects');const ids=new Set();for(let i=0;i<ps.length;i++){const p=ps[i];if(!p.id||String(p.id).length>48||ids.has(p.id))throw Error('Project IDs must be unique and at most 48 characters');ids.add(p.id);if(!p.name||String(p.name).length>120)throw Error('Project names must have 1 to 120 characters');if(!allowed.includes(p.area))throw Error('Area must be Priority or General');for(const k of ['cost','impact','urgency','risk','readiness','people']){if(!Number.isFinite(p[k]))throw Error('Invalid number for '+k+' on row '+(i+2));}if(p.cost<=0||p.cost>10000||p.people<0||p.people>10000000||['impact','urgency','risk','readiness'].some(k=>p[k]<0||p[k]>100))throw Error('Out-of-range values on row '+(i+2));}return true;} |
| function normalized(w){const keys=['impact','efficiency','readiness','risk'];const vals=keys.map(k=>Number.isFinite(w[k])?Math.max(0,w[k]):0);let den=vals.reduce((a,b)=>a+b,0);if(!den)den=1;return Object.fromEntries(keys.map((k,i)=>[k,vals[i]/den]));} |
| function scored(ps,w){const n=normalized(w),eff=ps.map(p=>p.impact/p.cost),max=Math.max(...eff);return ps.map((p,i)=>({...p,score:round(100*(n.impact*p.impact/100+n.efficiency*(max?eff[i]/max:0)+n.readiness*p.readiness/100+n.risk*(100-p.risk)/100),6)}));} |
| function optimize(ps,budget,quota,w){validate(ps);if(!Number.isFinite(budget)||budget<0||budget>10000||!Number.isFinite(quota)||quota<0||quota>100)return {feasible:false,reason:'Invalid budget or equity target'}; |
| const arr=scored(ps,w), N=arr.length, n=1<<N;let best=null;for(let mask=1;mask<n;mask++){let cost=0,priority=0,value=0,people=0,count=0;for(let i=0;i<N;i++)if(mask&(1<<i)){const p=arr[i];cost+=p.cost;if(cost>budget+1e-8)break;if(p.area==='Priority')priority+=p.cost;value+=p.score;people+=p.people;count++;}if(cost>budget+1e-8||!count||priority+1e-9<quota/100*cost)continue;const sol={feasible:true,mask,cost:round(cost,6),priorityCost:round(priority,6),value:round(value,6),people,selected:count,equityShare:round(100*priority/cost,4)};if(!best||value>best.value+1e-6||(Math.abs(value-best.value)<=1e-6&&(people>best.people||(people===best.people&&cost<best.cost))))best=sol;} |
| if(!best)return {feasible:false,reason:'No project portfolio meets the budget and priority-area share. Adjust the budget or rules.'};return {...best,ids:arr.filter((p,i)=>best.mask&(1<<i)).map(p=>p.id),items:arr.filter((p,i)=>best.mask&(1<<i)),scores:arr};} |
| function greedy(ps,budget,quota,w){const arr=scored(ps,w).sort((a,b)=>b.score/b.cost-a.score/a.cost||a.id.localeCompare(b.id));let cost=0,priority=0,value=0,people=0,ids=[];for(let p of arr){if(cost+p.cost<=budget+1e-8){cost+=p.cost;if(p.area==='Priority')priority+=p.cost;value+=p.score;people+=p.people;ids.push(p.id)}}return {cost,priorityCost:priority, value:round(value,6),people,ids,feasible:cost>0&&priority+1e-9>=quota/100*cost, equityShare:cost?round(100*priority/cost,4):0};} |
| function seeded(seed){let s=seed>>>0;return()=>{s=(Math.imul(1664525,s)+1013904223)>>>0;return s/4294967296;};} |
| function sensitivity(ps,budget,quota,w,trials=120,seed=72026){validate(ps);const rand=seeded(seed),base=normalized(w);const tally=Object.fromEntries(ps.map(p=>[p.id,0]));let valid=0,totalValue=0;for(let t=0;t<trials;t++){const rw={};for(const key of Object.keys(base))rw[key]=Math.max(.01,base[key]*(.58+.84*rand()));let sol=optimize(ps,budget,quota,rw);if(sol.feasible){valid++;totalValue+=sol.value;for(const id of sol.ids)tally[id]++;}} |
| return {trials,valid,avgValue:valid?round(totalValue/valid,2):null,rows:ps.map(p=>({...p,count:tally[p.id],pct:round(100*tally[p.id]/trials,1)})).sort((a,b)=>b.pct-a.pct||a.id.localeCompare(b.id))};} |
| function tokenizeCsv(t){t=String(t).replace(/^\ufeff/,'');if(t.length>2000000)throw Error('CSV must be smaller than 2 MB');let rows=[],cell='',row=[],q=false,atEnd=false;for(let i=0;i<t.length;i++){const c=t[i];if(q){if(c==='"'&&t[i+1]==='"'){cell+='"';i++;}else if(c==='"'){q=false;atEnd=true;}else cell+=c;}else if(atEnd){if(c===','){row.push(cell);cell='';atEnd=false;}else if(c==='\n'||c==='\r'){row.push(cell);rows.push(row);row=[];cell='';atEnd=false;if(c==='\r'&&t[i+1]==='\n')i++;}else throw Error('Unexpected characters after a closing quote');}else if(c==='"'){if(cell!=='')throw Error('Unexpected quote in CSV');q=true;}else if(c===','){row.push(cell);cell='';}else if(c==='\n'||c==='\r'){row.push(cell);rows.push(row);row=[];cell='';if(c==='\r'&&t[i+1]==='\n')i++;}else cell+=c;}if(q)throw Error('Unclosed quote in CSV');if(cell!==''||row.length||atEnd){row.push(cell);rows.push(row);}return rows.filter(r=>r.some(x=>x.trim()!==''));} |
| const required=['id','name','area','cost','impact','urgency','risk','readiness','people']; |
| function importProjects(text){const rows=tokenizeCsv(text);if(rows.length<3)throw Error('CSV requires a header and at least two projects');const hdr=rows[0].map(x=>x.trim().toLowerCase());if(hdr.length!==required.length||required.some((k,i)=>hdr[i]!==k))throw Error('CSV headers must be exactly: '+required.join(','));const ps=rows.slice(1).map((r,i)=>{if(r.length!==required.length)throw Error('Wrong column count at row '+(i+2));const ob={};required.forEach((k,j)=>{if(['id','name','area'].includes(k))ob[k]=r[j].trim();else {if(!r[j].trim()||!/^(?:\d+\.?\d*|\.\d+)$/.test(r[j].trim()))throw Error('Invalid nonnegative number at row '+(i+2));ob[k]=Number(r[j]);}});return ob;});validate(ps);return ps;} |
| const safe=(v)=>{const s=String(v??'');return /^[=+\-@\t\r]/.test(s)?"'"+s:s}; |
| const csv=(rows)=>rows.map(row=>row.map(v=>{const s=safe(v);return '"'+s.replace(/"/g,'""')+'"';}).join(',')).join('\r\n')+'\r\n'; |
| function exportProjects(ps){return csv([required,...ps.map(p=>required.map(k=>p[k]))]);} |
| return {demos,defaultWeights,clamp,round,validate,normalized,scored,optimize,greedy,sensitivity,seeded,tokenizeCsv,importProjects,exportProjects,csv,required}; |
| })(); |
| </script> |
| <script id="ui"> |
| (()=>{ |
| const pages=[['overview','◫','Command Center','Budget Intelligence','Optimize a portfolio of projects under a fixed budget, equity commitments and transparent scoring rules.'],['portfolio','▦','Program Portfolio','Portfolio Explorer','Inspect and edit program-level costs, impact scores and project readiness.'],['allocation','◇','Allocation Studio','Allocation Studio','Tune your priorities, compare the exact optimum against a simple greedy baseline and explain every selection.'],['equity','◉','Equity & Coverage','Equity & Coverage','Explore geographic allocation, priority-area targets and project-level reach.'],['sensitivity','〽','Sensitivity Lab','Decision Robustness','Recompute optimized portfolios under 120 reproducible perturbations of the decision weights.'],['data','▤','Data & Audit','Data & Audit','Import validated project data and export a complete, reproducible decision audit.']]; |
| const $=id=>document.getElementById(id); |
| const esc=(v)=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); |
| const fmt=(n,d=0)=>Number(n).toLocaleString('en-US',{maximumFractionDigits:d,minimumFractionDigits:d}); |
| const usd=n=>'$'+fmt(n,0)+'k'; |
| const initial=i=>{let d=Engine.demos[i];return {key:d.key,title:d.name,programs:d.programs.map(p=>({...p})),budget:d.budget,quota:d.quota,weights:{...Engine.defaultWeights},source:'Synthetic demonstration',edited:false};}; |
| let state=initial(0),active='overview',cached=null,review=''; |
| const score=()=>{cached=Engine.optimize(state.programs,state.budget,state.quota,state.weights);return cached}; |
| const alerts=s=>{const notes=[];if(!s.feasible)notes.push(s.reason);else if(s.budget-s.cost>40)notes.push('Unallocated funds remain because the model selects only whole projects.');return notes;}; |
| function notify(text,error=false){const el=$('toast');el.textContent=text;el.style.background=error?'#422636':'#193a34';el.classList.remove('hide');clearTimeout(notify.timer);notify.timer=setTimeout(()=>el.classList.add('hide'),3300)} |
| function renderNav(){for(let target of ['nav','mobileNav'])$(target).innerHTML=pages.map(([key,icon,title])=>`<button type="button" data-page="${key}" class="${active===key?'active':''}" aria-current="${active===key?'page':'false'}"><span class="nicon">${icon}</span>${title}</button>`).join('');document.querySelectorAll('[data-page]').forEach(b=>b.onclick=()=>goto(b.dataset.page));} |
| function goto(key){active=key;document.querySelectorAll('.page').forEach(p=>p.classList.toggle('active',p.id==='view-'+key));const d=pages.find(p=>p[0]===key);$('pageTitle').textContent=d[3];$('pageDesc').textContent=d[4];renderNav();render();} |
| function resetConfirm(){if(state.edited&&!confirm('Reset this dataset and discard unsaved changes? Download a snapshot first if needed.'))return;const i=Math.max(0,Engine.demos.findIndex(s=>s.key===state.key));state=initial(i);render();notify('Scenario restored to original synthetic data.');} |
| function chartBar(label,value,max,selected=true){return `<div class="barline"><span title="${esc(label)}">${esc(label)}</span><div class="bar-track"><div class="${selected?'':'dim'}" style="width:${Math.max(0,Math.min(100,value/max*100))}%"></div></div><b style="text-align:right">${fmt(value,1)}</b></div>`} |
| function kpis(s){return `<section class="grid kpis"><div class="card kpi"><div class="label">Budget committed</div><div class="num">${s.feasible?usd(s.cost):'N/A'}</div><div class="detail">of ${usd(state.budget)} authorized</div></div><div class="card kpi"><div class="label">Projects selected</div><div class="num">${s.feasible?s.selected:'0'} <span style="font-size:16px;color:#8da2bd">/ ${state.programs.length}</span></div><div class="detail">Binary project approvals</div></div><div class="card kpi"><div class="label">Priority-area allocation</div><div class="num" style="color:${s.feasible&&s.equityShare>=state.quota?'#75edc4':'#ffc47e'}">${s.feasible?fmt(s.equityShare,1)+'%':'N/A'}</div><div class="detail">minimum required ${fmt(state.quota)}%</div></div><div class="card kpi"><div class="label">People reached (planned)</div><div class="num">${s.feasible?fmt(s.people):'N/A'}</div><div class="detail">Sum of synthetic beneficiaries</div></div></section>`;} |
| function overview(s){let all=Engine.scored(state.programs,state.weights),shown=s.feasible?s.items:[],top=shown.slice().sort((a,b)=>b.score-a.score),greedy=Engine.greedy(state.programs,state.budget,state.quota,state.weights);$('view-overview').innerHTML=kpis(s)+`<div class="grid split"><section class="card cardpad"><div class="cardheader"><div><div class="eyebrow">ALLOCATION PORTFOLIO</div><h2>Selected programs</h2><div class="small-note">Exact search over all feasible binary portfolios</div></div><span class="pill ${s.feasible?'good':'bad'}">${s.feasible?'✓ Feasible':'No feasible selection'}</span></div>${s.feasible?`<div class="bar-chart">${top.map(p=>chartBar(p.name,p.score,100,true)).join('')}</div>`:`<div class="empty">${esc(s.reason)}</div>`}<div class="legend"><span><span class="dot"></span>Decision-weighted program value</span></div></section><section class="card cardpad"><div class="eyebrow">BUDGET STRUCTURE</div><h2>Allocation breakdown</h2><div class="small-note">All amounts in thousands of illustrative USD</div><div class="stat-row"><b>Selected programs</b><span>${s.feasible?usd(s.cost):'N/A'}</span></div><div class="progress"><div style="width:${s.feasible&&state.budget?Math.min(100,s.cost/state.budget*100):0}%"></div></div><div class="stat-row"><b>Priority-area projects</b><span>${s.feasible?usd(s.priorityCost):'N/A'}</span></div><div class="progress"><div style="width:${s.feasible?Math.min(100,s.equityShare):0}%"></div></div><div class="stat-row"><b>Uncommitted budget</b><span>${s.feasible?usd(Math.max(0,state.budget-s.cost)):usd(state.budget)}</span></div><div class="divider"></div><div class="mini-metrics"><div class="metric-mini"><strong>${s.feasible?fmt(s.value,1):'N/A'}</strong><span>Decision score (sum)</span></div><div class="metric-mini"><strong>${s.feasible?fmt(s.equityShare,1)+'%':'N/A'}</strong><span>Priority-area share</span></div><div class="metric-mini"><strong>${fmt(state.programs.length)}</strong><span>Candidate projects</span></div></div></section></div><div class="grid split" style="margin-top:16px"><section class="card cardpad"><div class="cardheader"><div><div class="eyebrow">METHOD COMPARISON</div><h2>Exact allocation vs. greedy baseline</h2></div><span class="pill">Deterministic</span></div><div class="two"><div class="info-tile"><span class="muted small-note">EXACT SOLUTION</span><h2 style="font-size:26px">${s.feasible?fmt(s.value,1):'N/A'}</h2><div class="small-note">${s.feasible?s.selected+' projects, '+usd(s.cost):'No solution'}</div></div><div class="info-tile"><span class="muted small-note">GREEDY REFERENCE</span><h2 style="font-size:26px">${fmt(greedy.value,1)}</h2><div class="small-note">${greedy.ids.length} projects · ${greedy.feasible?'Equity feasible':'Equity rule not met'}</div></div></div><div class="flow-note">The greedy baseline sorts projects by value per cost. It is not guaranteed to satisfy the equity rule or maximize value.</div></section><section class="card cardpad"><div class="eyebrow">INTERPRETATION</div><h2>What the optimizer actually does</h2><p class="muted" style="font-size:12px">It evaluates every possible subset of ${state.programs.length} projects, rejects portfolios over budget or below the priority-area spending target, and maximizes the sum of weighted project scores.</p><div class="notice">All example budgets, scores, benefits and reach estimates are synthetic. This is a transparent demonstration, not an official fiscal assessment.</div>${alerts(s).map(x=>`<div class="notice warn">${esc(x)}</div>`).join('')}</section></div>`;} |
| function portfolio(s){const all=Engine.scored(state.programs,state.weights),sel=new Set(s.ids||[]);$('view-portfolio').innerHTML=`<div class="grid split"><div class="card cardpad"><div class="cardheader"><div><div class="eyebrow">INPUT DATA</div><h2>Project register</h2><div class="small-note">Edit costs, impact or readiness. Recalculate instantly.</div></div><div style="min-width:190px"><input type="search" id="programSearch" placeholder="Find a program..." aria-label="Search programs"></div></div><div class="table-wrap"><table><thead><tr><th>Project</th><th>Area</th><th>Cost $k</th><th>Impact</th><th>Readiness</th><th>Score</th><th>Chosen</th></tr></thead><tbody id="portfolioRows"></tbody></table></div><p class="flow-note">Editable values affect the algorithm immediately. Export your work before reloading the page.</p></div><div class="card cardpad"><div class="eyebrow">MODEL INPUTS</div><h2>How projects are rated</h2><p class="muted">Scores are normalized across the current portfolio and use four dimensions: estimated impact, impact-to-cost efficiency, readiness and inverse risk.</p><div class="divider"></div>${all.sort((a,b)=>b.score-a.score).slice(0,6).map(p=>chartBar(p.name,p.score,100,sel.has(p.id))).join('')}<div class="notice">Urgency and beneficiaries are shown for context. Urgency does not enter the objective; beneficiary count is used only as a tie-breaker.</div></div></div>`; |
| const rows=()=>{const key=($('programSearch')?.value||'').toLowerCase(),sel=new Set(score().ids||[]);$('portfolioRows').innerHTML=Engine.scored(state.programs,state.weights).filter(p=>(p.name+' '+p.id+' '+p.area).toLowerCase().includes(key)).map(p=>`<tr><td><b>${esc(p.name)}</b><br><span class="small-note mono">${esc(p.id)}</span></td><td>${esc(p.area)}</td><td><input aria-label="Cost for ${esc(p.name)}" data-edit="cost" data-id="${esc(p.id)}" type="number" class="number-edit" min=".1" max="10000" step="1" value="${p.cost}"></td><td><input aria-label="Impact for ${esc(p.name)}" data-edit="impact" data-id="${esc(p.id)}" type="number" class="number-edit" min="0" max="100" value="${p.impact}"></td><td><input aria-label="Readiness for ${esc(p.name)}" data-edit="readiness" data-id="${esc(p.id)}" type="number" class="number-edit" min="0" max="100" value="${p.readiness}"></td><td>${fmt(p.score,1)}</td><td><span class="status ${sel.has(p.id)?'':'out'}">${sel.has(p.id)?'SELECTED':'NOT SELECTED'}</span></td></tr>`).join('');document.querySelectorAll('[data-edit]').forEach(el=>el.onchange=()=>{const p=state.programs.find(p=>p.id===el.dataset.id);const v=Number(el.value),k=el.dataset.edit;if(!el.value.trim()||!Number.isFinite(v)||(k==='cost'&&(v<=0||v>10000))||(k!=='cost'&&(v<0||v>100))){notify('Invalid value. Cost must be positive; scores 0–100.',true);el.value=p[k];return;}p[k]=v;state.edited=true;render();notify('Project edited; allocation recalculated.')})};rows();$('programSearch').oninput=rows;} |
| function allocation(s){const n=Engine.normalized(state.weights),g=Engine.greedy(state.programs,state.budget,state.quota,state.weights),sorted=Engine.scored(state.programs,state.weights).sort((a,b)=>b.score-a.score);$('view-allocation').innerHTML=kpis(s)+`<div class="grid split"><section class="card cardpad"><div class="eyebrow">DECISION PRIORITIES</div><h2>Weighting studio</h2><p class="small-note">Change any weight. Values are normalized to 100% automatically.</p>${[['impact','Impact quality'],['efficiency','Impact-per-dollar efficiency'],['readiness','Implementation readiness'],['risk','Low-risk preference']].map(([k,t])=>`<div class="slider-row"><div class="slider-top"><b>${t}</b><span id="${k}WeightText">${fmt(n[k]*100,0)}% normalized</span></div><input type="range" aria-label="${t}" min="0" max="100" step="1" value="${state.weights[k]}" data-weight="${k}"></div>`).join('')}<div class="notice">Weights affect project scores, not eligibility. The equity target is a separate hard spending constraint.</div></section><section class="card cardpad"><div class="eyebrow">OPTIMAL PORTFOLIO</div><h2>Recommended commitments</h2><div class="table-wrap"><table><thead><tr><th>Program</th><th>Amount</th><th>Score</th></tr></thead><tbody>${s.feasible?s.items.sort((a,b)=>b.score-a.score).map(p=>`<tr><td>${esc(p.name)}</td><td>${usd(p.cost)}</td><td>${fmt(p.score,1)}</td></tr>`).join(''):`<tr><td colspan="3">${esc(s.reason)}</td></tr>`}</tbody></table></div><div class="divider"></div><div class="two"><div class="metric-mini"><span>Exact objective</span><strong>${s.feasible?fmt(s.value,1):'N/A'}</strong></div><div class="metric-mini"><span>Greedy objective</span><strong>${fmt(g.value,1)}</strong></div></div><p class="small-note" style="margin-top:12px">${g.feasible?'Both approaches meet the constraints.':'Greedy is not equity-feasible, so its score is not a directly comparable feasible solution.'}</p></section></div><div class="card cardpad" style="margin-top:15px"><div class="eyebrow">EXPLAINABLE RANKING</div><h2>All candidate scores</h2><div class="bar-chart">${sorted.map(p=>chartBar(p.name,p.score,100,(s.ids||[]).includes(p.id))).join('')}</div></div>`;document.querySelectorAll('[data-weight]').forEach(el=>el.oninput=()=>{state.weights[el.dataset.weight]=Number(el.value);state.edited=true;render();});} |
| function equity(s){const sel=s.feasible?s.items:[],pri=sel.filter(p=>p.area==='Priority'),gen=sel.filter(p=>p.area==='General'),pall=state.programs.filter(p=>p.area==='Priority'),gall=state.programs.filter(p=>p.area==='General');$('view-equity').innerHTML=kpis(s)+`<div class="grid split"><div class="card cardpad"><div class="eyebrow">EQUITY RULE</div><h2>Priority-area investment</h2><p class="muted">A minimum of ${fmt(state.quota)}% of committed funds must flow to programs tagged as a priority-area investment.</p><div class="stat-row"><b>Current priority-area share</b><span>${s.feasible?fmt(s.equityShare,1)+'%':'N/A'}</span></div><div class="progress"><div style="width:${s.feasible?s.equityShare:0}%"></div></div><div class="stat-row"><b>Required threshold</b><span>${fmt(state.quota)}%</span></div><div class="progress"><div style="width:${state.quota}%;background:#ffa95c"></div></div><div class="divider"></div><div class="two"><div class="metric-mini"><strong>${usd(pri.reduce((a,p)=>a+p.cost,0))}</strong><span>Priority spending</span></div><div class="metric-mini"><strong>${usd(gen.reduce((a,p)=>a+p.cost,0))}</strong><span>General spending</span></div></div><div class="notice">Equity here measures spending share, not actual fairness, geographic effectiveness or distribution of outcomes. These require external evidence.</div></div><div class="card cardpad"><div class="eyebrow">COVERAGE PROFILE</div><h2>Selected reach & projects</h2>${[['Priority area',pri,pall],['General area',gen,gall]].map(([label,items,all])=>`<div class="stat-row"><b>${label}</b><span>${items.length}/${all.length} projects</span></div><div class="progress"><div style="width:${all.length?items.length/all.length*100:0}%"></div></div><div class="stat-row"><b>Indicative people reached</b><span>${fmt(items.reduce((a,p)=>a+p.people,0))}</span></div>`).join('<div class="divider"></div>')}</div></div><div class="card cardpad" style="margin-top:15px"><div class="eyebrow">EQUITY TRADE-OFFS</div><h2>What changes without the spending requirement?</h2>${(()=>{let u=Engine.optimize(state.programs,state.budget,0,state.weights);return `<div class="two"><div class="info-tile"><span class="small-note">WITH THE EQUITY RULE</span><h2>${s.feasible?fmt(s.value,1):'N/A'}</h2><div class="small-note">${s.feasible?fmt(s.equityShare,1)+'% priority share':'No feasible set'}</div></div><div class="info-tile"><span class="small-note">WITHOUT THE RULE</span><h2>${u.feasible?fmt(u.value,1):'N/A'}</h2><div class="small-note">${u.feasible?fmt(u.equityShare,1)+'% priority share':'No feasible set'}</div></div></div>`})()}<p class="help">This comparison shows the modeled cost of a hard equity constraint in decision-score units. Scores are not monetary benefits.</p></div>`;} |
| function sensitivityView(s){let t=Engine.sensitivity(state.programs,state.budget,state.quota,state.weights,120,72026);$('view-sensitivity').innerHTML=`<div class="grid kpis"><div class="card kpi"><div class="label">Weight perturbations</div><div class="num">120</div><div class="detail">Fixed seed 72026</div></div><div class="card kpi"><div class="label">Feasible runs</div><div class="num">${t.valid}</div><div class="detail">Same budget and quota</div></div><div class="card kpi"><div class="label">Mean score</div><div class="num">${t.avgValue===null?'N/A':fmt(t.avgValue,1)}</div><div class="detail">Scores across reweighted models</div></div><div class="card kpi"><div class="label">Programs tested</div><div class="num">${state.programs.length}</div><div class="detail">Exact subset search per run</div></div></div><div class="grid split"><section class="card cardpad"><div class="eyebrow">SELECTION FREQUENCY</div><h2>Which programs survive preference changes?</h2><div class="bar-chart">${t.rows.map(p=>chartBar(p.name,p.pct,100,true)).join('')}</div></section><section class="card cardpad"><div class="eyebrow">UNCERTAINTY MODEL</div><h2>Reproducible preference stress test</h2><p class="muted">Each run multiplies the four decision weights by independently generated factors between 0.58 and 1.42. Weights are normalized and the exact allocation is solved again.</p><div class="notice">This is a sensitivity test to assumptions about preferences, not a confidence interval or forecast of real-world impact.</div><h3>Interpretation</h3><p class="help">A high selection frequency indicates stability under the specified perturbation distribution, not proof of value, fairness or policy effectiveness.</p><button class="btn alt small" id="exportSensitivity">↓ Download sensitivity CSV</button></section></div>`;$('exportSensitivity').onclick=()=>download('sensitivity');} |
| function data(s){$('view-data').innerHTML=`<div class="grid split"><div class="card cardpad"><div class="eyebrow">IMPORT DATA</div><h2>Upload your project portfolio</h2><p class="muted">Use the downloadable CSV template and keep the exact column names. Files are processed only in the browser.</p><div class="notice">Allowed: 2–14 projects; IDs must be unique; area must be <b>Priority</b> or <b>General</b>. Costs and people must be nonnegative. Scores must be between 0 and 100.</div><input id="importFile" type="file" accept=".csv,text/csv" aria-label="Import CSV file"><div class="download-actions" style="margin-top:16px"><button class="btn small" id="doImport">↑ Import CSV</button><button class="btn alt small" id="downloadTemplate">↓ CSV template</button></div><div class="divider"></div><span class="small-note">Current dataset: <b>${esc(state.title)}</b> · ${state.programs.length} projects · ${esc(state.source)}</span><p class="help">Unsaved changes are lost on reload. Export a snapshot to preserve your work.</p></div><div class="card cardpad"><div class="eyebrow">EXPORT AUDIT</div><h2>Download reproducible outputs</h2><div class="download-actions">${[['report','Research report (Markdown)'],['snapshot','Full project snapshot (JSON)'],['selected','Selected projects (CSV)'],['portfolio','Full portfolio (CSV)'],['audit','Decision audit (CSV)'],['sensitivity','Sensitivity results (CSV)'],['template','CSV import template']].map(([k,v])=>`<button class="btn alt small" data-export="${k}">↓ ${v}</button>`).join('')}</div><div class="notice">CSV text beginning with spreadsheet formula characters is escaped to reduce accidental formula execution.</div></div></div><div class="card cardpad" style="margin-top:15px"><div class="eyebrow">TRANSPARENT MODEL SPECIFICATION</div><h2>Methodology & limitations</h2><div class="two"><div><h3>Exact binary portfolio optimization</h3><p class="muted">Enumerates up to 2¹⁴ subsets. Each program is either fully funded or not funded. A portfolio is feasible when its total cost is at most the budget and its priority-area expenditure is at least the required share of committed funds.</p><h3>Objective</h3><p class="muted">Maximizes the sum of normalized program scores. Criteria include impact, impact-per-dollar efficiency (relative to this dataset), readiness and low-risk preference. Estimated beneficiaries break ties.</p></div><div><h3>Scope limits</h3><p class="muted">The examples are synthetic. The model does not account for partial funding, program dependencies, legal procurement constraints, displacement, double-counting of beneficiaries or distributional outcomes.</p><h3>Decision protocol</h3><p class="muted">For any real allocation, validate cost estimates, eligibility, equity definitions, conflicts of interest, public consultation and implementation feasibility before using the result.</p></div></div></div>`;document.querySelectorAll('[data-export]').forEach(b=>b.onclick=()=>download(b.dataset.export));$('downloadTemplate').onclick=()=>download('template');$('doImport').onclick=async()=>{const f=$('importFile').files[0];if(!f){notify('Choose a CSV file before importing.',true);return;}if(f.size>2000000){notify('CSV file exceeds 2 MB.',true);return;}try{const contents=await f.text(),ps=Engine.importProjects(contents);const st={...state,key:'custom',programs:ps,title:'Imported portfolio',source:'User CSV',edited:true};Engine.optimize(st.programs,st.budget,st.quota,st.weights);if(state.edited&&!confirm('Replace the current portfolio with this valid import?'))return;state=st;render();notify(ps.length+' projects imported successfully.');}catch(e){notify('Import rejected: '+e.message,true);}};} |
| function makeDownload(name,content,type){const blob=new Blob([content],{type}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);} |
| function report(s){return `# BudgetForge Decision Audit\n\nGenerated from local browser state. All supplied demonstrations are synthetic.\n\nDataset: ${state.title}\nSource: ${state.source}\nBudget (thousands USD): ${state.budget}\nPriority spending share required: ${state.quota}%\nWeights: ${JSON.stringify(Engine.normalized(state.weights))}\nFeasible: ${Boolean(s.feasible)}\n${s.feasible?`Committed: ${s.cost}\nPriority spending: ${s.priorityCost}\nActual share: ${s.equityShare}%\nWeighted decision objective (dimensionless): ${s.value}\nBeneficiaries (synthetic; not deduplicated): ${s.people}\nSelected: ${s.ids.join(', ')}`:'Reason: '+s.reason}\n\n## Selected programs\n\n${s.feasible?s.items.map(p=>'- '+p.id+' | '+p.name+' | cost $'+p.cost+'k | score '+Engine.round(p.score,2)).join('\n'):'None'}\n\n## Method\n\nExact exhaustive search over binary program subsets subject to a fixed budget and minimum priority-area expenditure share. Scores are based on normalized criteria: impact, impact/cost efficiency, readiness and inverse risk. The weights are user-configurable. People are used only as a tie-breaker.\n\n## Limitations\n\nNo evidence of causal effects, actual beneficiary counts, distributive justice, true market prices or statutory compliance is provided. Real-world decisions require expert review.\n`;} |
| function download(kind){let s=score(),date='BudgetForge_72_'+kind,body='',type='text/plain;charset=utf-8',ext='txt';const header=['id','name','area','cost_kusd','impact','urgency','risk','readiness','people','score','selected'];switch(kind){case'report':body=report(s);ext='md';type='text/markdown;charset=utf-8';break;case'snapshot':body=JSON.stringify({project:'BudgetForge',version:'1.0',metadata:{title:state.title,source:state.source,synthetic:state.source!=='User CSV'},budget:state.budget,quota:state.quota,weights:state.weights,normalizedWeights:Engine.normalized(state.weights),programs:state.programs,solution:{...s,items:undefined,scores:undefined}},null,2);ext='json';type='application/json;charset=utf-8';break;case'portfolio':body=Engine.exportProjects(state.programs);ext='csv';type='text/csv;charset=utf-8';break;case'template':body=Engine.exportProjects(Engine.demos[0].programs.slice(0,3));ext='csv';type='text/csv;charset=utf-8';break;case'selected':case'audit':{let arr=Engine.scored(state.programs,state.weights),sel=new Set(s.ids||[]);if(kind==='selected')arr=arr.filter(p=>sel.has(p.id));body=Engine.csv([header,...arr.map(p=>[p.id,p.name,p.area,p.cost,p.impact,p.urgency,p.risk,p.readiness,p.people,Engine.round(p.score,3),sel.has(p.id)?'yes':'no'])]);ext='csv';type='text/csv;charset=utf-8';break;}case'sensitivity':{const t=Engine.sensitivity(state.programs,state.budget,state.quota,state.weights,120,72026);body=Engine.csv([['id','name','selected_runs','total_runs','selection_percent'],...t.rows.map(p=>[p.id,p.name,p.count,t.trials,p.pct])]);ext='csv';type='text/csv;charset=utf-8';break;}default:return;}makeDownload(date+'.'+ext,body,type);notify(kind+' exported.');} |
| function render(){const s=score();$('scenarioSelect').innerHTML=Engine.demos.map(p=>`<option value="${p.key}">${esc(p.name)} · synthetic</option>`).join('')+(Engine.demos.some(x=>x.key===state.key)?'':`<option value="custom">Imported portfolio</option>`);$('scenarioSelect').value=state.key;$('budgetInput').value=state.budget;$('quotaInput').value=state.quota; |
| if(active==='overview')overview(s);if(active==='portfolio')portfolio(s);if(active==='allocation')allocation(s);if(active==='equity')equity(s);if(active==='sensitivity')sensitivityView(s);if(active==='data')data(s); |
| } |
| $('scenarioSelect').onchange=e=>{if(state.edited&&!confirm('Switching datasets discards unsaved changes. Continue?')){render();return;}const i=Engine.demos.findIndex(x=>x.key===e.target.value);if(i<0){render();return;}state=initial(i);render();notify('Loaded '+state.title+'.');}; |
| const numberChange=(id,k,lo,hi)=>$(id).onchange=e=>{const raw=e.target.value,v=Number(raw);if(!raw.trim()||!Number.isFinite(v)||v<lo||v>hi){notify('Enter a number from '+lo+' to '+hi+'.',true);render();return;}state[k]=v;state.edited=true;render();};numberChange('budgetInput','budget',0,10000);numberChange('quotaInput','quota',0,100); |
| $('resetBtn').onclick=resetConfirm;$('downloadReport').onclick=()=>download('report');renderNav();render(); |
| })(); |
| </script> |
| </body></html> |
|
|