/* ImpactLab LATAM browser UI. The analytics engine is dependency-free. */ (function(){ 'use strict'; const E=window.ImpactEngine,D=window.ImpactDemo; const colors=['#189784','#638fd7','#d18b42','#9c6dc1','#bc5960','#4f8ea0']; const $=id=>document.getElementById(id); const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const fmt=(x,dp=2)=>E.finite(x)?new Intl.NumberFormat('en-US',{maximumFractionDigits:dp,minimumFractionDigits:dp}).format(x):'—'; const signed=(x,dp=2)=>E.finite(x)?(x>0?'+':'')+fmt(x,dp):'—'; const countryName=c=>E.countries[c]||c; const optsDefault={country:'CHL',controls:['ARG','PER','URY'],indicator:'SP.DYN.LE00.IN',start:2003,end:2024,breakYear:2016}; const state={rows:D.records.slice(),source:'demo',indicatorLoaded:null,opts:{...optsDefault,controls:[...optsDefault.controls]},activeTab:'overview',did:null,synth:null,notes:'',loading:false,updated:'',message:'',revision:0}; window.__impactState=state; function nameShort(ind){return E.indicators[ind]?.name||ind;} function optionMarkup(obj,sel){return Object.keys(obj).map(key=>``).join('');} function setDefaultForm(){ $('indicator').innerHTML=optionMarkup(E.indicators,state.opts.indicator); $('treated').innerHTML=optionMarkup(E.countries,state.opts.country); $('startYear').value=state.opts.start;$('endYear').value=state.opts.end;$('breakYear').value=state.opts.breakYear; $('policyName').value='';$('policyUrl').value='';renderDonors(); } function renderDonors(){ const treated=$('treated').value; $('controls').innerHTML=Object.entries(E.countries).map(([code,name])=>``).join(''); } function optionsFromForm(){ const controls=Array.from($('controls').querySelectorAll('input:checked')).map(el=>el.value); return {country:$('treated').value,controls,indicator:$('indicator').value,start:Number($('startYear').value),end:Number($('endYear').value),breakYear:Number($('breakYear').value)}; } function validatedPolicyUrl(){const input=$('policyUrl').value.trim();if(!input)return '';try{const u=new URL(input);return ['http:','https:'].includes(u.protocol)?u.href:'';}catch(e){return '';}} function error(msg){$('errorPanel').textContent=msg||'';$('errorPanel').hidden=!msg;} function statusMessage(){ const live=state.source==='worldbank',imported=state.source==='imported';const el=$('sourceBanner');el.dataset.live=String(live);el.dataset.imported=String(imported); el.querySelector('.source-chip').textContent=live?'WORLD BANK LIVE':imported?'IMPORTED DATA':'SYNTHETIC DEMO'; $('sourceMessage').textContent=live?`Live World Bank records for ${nameShort(state.indicatorLoaded)}. Reporting years may lag.`:imported?'User-imported data. Provenance and completeness must be independently verified.':'All displayed values are fictional synthetic examples, not official country statistics.'; $('rowCount').textContent=`${new Intl.NumberFormat('en-US').format(state.rows.length)} records`; } function refresh(){ try{state.opts=optionsFromForm();E.checkOptions(state.opts);if(state.source==='worldbank'&&state.opts.indicator!==state.indicatorLoaded){throw Error('Select “Load World Bank data” to fetch the newly selected indicator.');} state.did=E.estimateDid(state.rows,state.opts);state.synth=E.syntheticControl(state.rows,state.opts);error(state.did.ok?'':state.did.error);state.revision++; }catch(ex){state.did=null;state.synth=null;error(ex.message);} statusMessage();render(); } function heading(num,title,subtitle,tag='RESEARCH MODULE'){return `
${num} · ${esc(tag)}

${esc(title)}

${esc(subtitle)}

${state.source==='demo'?'DEMO DATA':state.source==='worldbank'?'OFFICIAL SOURCE':'IMPORTED DATA'}
`;} function metric(label,value,detail,kind=''){return `
${esc(label)}
${esc(value)}
${esc(detail)}
`;} function box(title,subtitle,body,extra=''){return `

${esc(title)}

${subtitle?`
${esc(subtitle)}
`:''}${body}
`;} function notice(text,warn=false){return `
${esc(text)}
`;} function noData(e='Not enough comparable observations for this method.'){ return `

Not enough comparable data

${esc(e)}

Try a different outcome, more controls, a longer period, or synthetic demo.

`; } function chartLine(lines,yearBreak,opt={}){ const data=lines.flatMap(line=>line.points.filter(p=>E.finite(p.v)).map(p=>p.v)); const years=lines.flatMap(line=>line.points.map(p=>p.year)); if(!data.length||!years.length)return `
No chartable series
`; let lo=Math.min(...data),hi=Math.max(...data);if(lo===hi){lo-=1;hi+=1;} const spread=hi-lo;lo-=spread*.12;hi+=spread*.15; const minY=Math.min(...years),maxY=Math.max(...years),width=800,height=312,L=57,R=16,T=16,B=39; const x=year=>L+(year-minY)/(maxY-minY||1)*(width-L-R); const y=v=>T+(hi-v)/(hi-lo)*(height-T-B); let svg=``; for(let j=0;j<=4;j++){const value=lo+(hi-lo)*j/4;svg+=`${esc(fmt(value,Math.abs(value)>1000?0:1))}`;} for(let j=0;j<=5;j++){const year=Math.round(minY+(maxY-minY)*j/5);svg+=`${year}`;} if(E.finite(yearBreak)&&yearBreak>=minY&&yearBreak<=maxY){const bx=x(yearBreak);svg+=`break ${yearBreak}`;} lines.forEach((line,i)=>{ const col=line.color||colors[i%colors.length];let points=[]; line.points.forEach((p,j)=>{ if(E.finite(p.v)){points.push([x(p.year),y(p.v)]);}else{if(points.length){svg+=makePath(points,col);points=[];}} });if(points.length)svg+=makePath(points,col); for(const p of line.points.filter(p=>E.finite(p.v))){svg+=`${esc(line.name)} ${p.year}: ${esc(fmt(p.v,3))}`;} }); svg+=''; svg+=`
${lines.map((line,i)=>`${esc(line.name)}`).join('')}
`; return svg; } function makePath(points,color){return ``;} function lineFromPoints(points,key,name,color){return {name,color,points:points.map(p=>({year:p.year,v:p[key]}))};} function barList(entries,scale){ return `
${entries.map(([name,value],i)=>`
${esc(name)}
${esc(fmt(value,2))}
`).join('')}
`; } function valueUnit(){return E.indicators[state.opts.indicator]?.unit||'outcome units';} function pairTable(points,columns){return `
${columns.map(c=>``).join('')}${points.map(p=>`${columns.map(c=>``).join('')}`).join('')}
${esc(c.label)}
${esc(c.number?fmt(p[c.key],c.decimals??2):p[c.key])}
`;} function overview(){ const did=state.did,syn=state.synth;let html=heading('01','The research overview','Separate an observational pattern from a defensible policy claim.','DESIGN SNAPSHOT'); if(!did?.ok)return html+noData(did?.error); const trend=E.pretrend(did); html+=`
${metric('DiD contrast',signed(did.effect),valueUnit(),'teal')}${metric('Temporal observations',String(did.preYears+did.postYears),`${did.preYears} pre + ${did.postYears} post`)}${metric('Pre-trend slope gap',signed(trend.slopeDifference,3),`${valueUnit()} per year`)}${metric('Synthetic model',syn?.ok?'Available':'Unavailable',syn?.ok?`${syn.weights.length} donor weights`:'Check data coverage',syn?.ok?'teal':'amber')}
`; html+=`
${box('Outcome trajectory','Treated country vs. equal-weight mean of comparison economies',chartLine([lineFromPoints(did.points,'treated',countryName(state.opts.country),colors[0]),lineFromPoints(did.points,'controls','Comparison mean',colors[1])],state.opts.breakYear))}${box('Research interpretation','Methodology visible by design',`
● ${esc(countryName(state.opts.country))} compared with ${did.preYears+did.postYears} matched years.
● ${state.opts.controls.length} comparison countries selected.
● Pre-period slopes: ${esc(trend.ok?'calculated':'insufficient data')}.
● Convex synthetic comparison: ${esc(syn?.ok?'estimated':'unavailable')}.

Interpretation: Neither a DiD estimate nor a synthetic comparison alone establishes a causal effect. Identification needs a real, independently documented intervention and scrutiny of confounders.

`)}
`; html+=notice(`${state.source==='demo'?'DEMONSTRATION ONLY. ':'Observational study. '}The selected break year is a user-defined analytical assumption, not an independently validated policy event.`,true); return html; } function trends(){ const d=state.did;if(!d?.ok)return heading('02','Outcome time series','Inspect levels, variation and reporting gaps.')+noData(d?.error); return heading('02','Time-series explorer','Inspect trends before and after the proposed policy break.')+ box('Observed outcome','Only years with reported data are plotted; gaps are not silently imputed.',chartLine([lineFromPoints(d.points,'treated',countryName(state.opts.country),colors[0]),lineFromPoints(d.points,'controls','Control mean',colors[1])],state.opts.breakYear))+ `
${box('Year-by-year contrast','The control mean is the equal-weight average of all chosen donors',pairTable(d.points.map(p=>({...p,difference:E.finite(p.treated)&&E.finite(p.controls)?p.treated-p.controls:null})),[{key:'year',label:'Year'},{key:'treated',label:'Treated',numeric:true,number:true},{key:'controls',label:'Control mean',numeric:true,number:true},{key:'difference',label:'Difference',numeric:true,number:true}]))}${box('Series notes','Good comparative inference requires more than a visible chart.',``)}
`; } function didView(){ const d=state.did;if(!d?.ok)return heading('03','Difference-in-Differences','A transparent group-level pre/post contrast.')+noData(d?.error); const p=E.pretrend(d),boot=E.bootstrapDid(d,400,20261008); let out=heading('03','Difference-in-Differences','Two-group, two-period contrast, with paired-year averages and descriptive diagnostics.'); out+=`
${metric('DiD contrast',signed(d.effect),valueUnit(),'teal')}${metric('Treated change',signed(d.treatedChange),`post minus pre`)}${metric('Control change',signed(d.controlChange),'post minus pre')}${metric('Year-resampling range',`${signed(boot.interval[0],1)} to ${signed(boot.interval[1],1)}`,'2.5–97.5% of 400 draws')}
`; out+=`
${box('Comparison over time','Both series use exactly the same observed years.',chartLine([lineFromPoints(d.points,'treated',countryName(state.opts.country),colors[0]),lineFromPoints(d.points,'controls','Comparison mean',colors[1])],state.opts.breakYear))}${box('Pre-trend diagnostic','A slope gap is a diagnostic, not a formal parallel-trends guarantee.',`
${barList([['Treated slope',p.treatedSlope||0],['Control slope',p.controlSlope||0]],Math.max(.001,Math.abs(p.treatedSlope||0),Math.abs(p.controlSlope||0)))}

Pre-treatment slope difference: ${esc(signed(p.slopeDifference,3))} ${esc(valueUnit())}/year.

The interval resamples observed years. It is a descriptive stability check, not a cluster-robust statistical confidence interval and not proof of causal identification.

`)}
`; out+=box('Estimator definition','No black-box treatment-effect claim',`
DiD = (Treated_after − Treated_before) − (Controls_after − Controls_before)\n\n${esc(`${fmt(d.treatedPost)} − ${fmt(d.treatedPre)} − (${fmt(d.controlPost)} − ${fmt(d.controlPre)}) = ${signed(d.effect)} ${valueUnit()}`)}
`); out+=notice('Parallel trends, contemporaneous confounding, interference, treatment timing and measurement changes must be assessed outside this dashboard. This is an exploratory contrast.',true); return out; } function synthView(){ const d=state.synth;let out=heading('04','Synthetic control','A convex mixture of donors fitted only using pre-break observations.');if(!d?.ok)return out+noData(d?.error); out+=`
${metric('Average post-break gap',signed(d.postMeanGap),valueUnit(),'teal')}${metric('Pre-break RMSE',fmt(d.preRmse),valueUnit())}${metric('Post-break RMSE',fmt(d.postRmse),valueUnit())}${metric('RMSE ratio',fmt(d.fitRatio), 'post / pre; interpret cautiously')}
`; out+=`
${box('Observed vs synthetic','Donor weights are nonnegative, sum to one, and use pre-break years only.',chartLine([lineFromPoints(d.points,'treated',countryName(state.opts.country),colors[0]),lineFromPoints(d.points,'synthetic','Synthetic control',colors[2])],state.opts.breakYear))}${box('Donor weights','Optimized using constrained least squares with mild regularization',barList(d.weights.map(w=>[countryName(w.country),w.weight]),1)+`

Weights represent pre-break statistical fit, not country similarity or causal validity. Sum = ${fmt(E.sum(d.weights.map(w=>w.weight)),4)}.

`)}
`; out+=box('Gap series','Treated outcome minus weighted synthetic comparison',chartLine([lineFromPoints(d.points,'gap','Gap',colors[0])],state.opts.breakYear)); out+=notice('A low pre-break RMSE is necessary but not sufficient. The result is sensitive to the chosen donor pool and to unobserved changes.',true);return out; } function placebos(){ const d=state.did;if(!d?.ok)return heading('05','Placebo diagnostics','Explore alternative fake assignments and break years.')+noData(d?.error); const units=E.unitPlacebos(state.rows,state.opts),times=E.timePlacebos(state.rows,state.opts); let out=heading('05','Placebo diagnostics','Two diagnostics that help reveal how distinctive the observed contrast is.'); out+=`
${metric('Unit assignments',String(units.length),'countries re-treated in turn')}${metric('Earlier break dates',String(times.length),'fake interventions')}${metric('Observed DiD',signed(d.effect),valueUnit(),'teal')}${metric('Placebo inference','Descriptive','no causal p-values','amber')}
`; out+=`
${box('In-space unit placebos','Each selected country is treated in turn; others become donors',units.length?pairTable(units.map(u=>({...u,country:countryName(u.country),case:u.treated?'Observed':'Placebo'})),[{key:'country',label:'Country'},{key:'case',label:'Role'},{key:'postMeanGap',label:'Post gap',numeric:true,number:true},{key:'preRmse',label:'Pre RMSE',numeric:true,number:true}]):'

Choose more donor countries with complete coverage to enable this diagnostic.')}${box('Earlier-break DiD placebos','All fake breaks use observations before the actual break year',times.length?pairTable(times,[{key:'year',label:'Fake break'},{key:'effect',label:'DiD contrast',numeric:true,number:true}]):'

A longer pre-period is needed for fake breaks with three-year segments.')}

`; out+=notice('These finite sets of placebos are sensitivity exercises, not valid randomization inference. Never interpret them as established statistical significance.',true);return out; } function quality(){ const o=state.opts,c=E.coverage(state.rows,o),d=state.did,s=state.synth; const present=E.sum(c.map(x=>x.present)),expected=E.sum(c.map(x=>x.expected)); let out=heading('06','Data coverage & integrity','Missing observations remain missing; source and year lag stay visible.'); out+=`
${metric('Observed coverage',`${fmt(100*present/Math.max(1,expected),1)}%`,'selected study panel','teal')}${metric('Expected observations',String(expected),'years × economies')}${metric('Available observations',String(present),'non-null values')}${metric('Methods usable',String(Number(!!d?.ok)+Number(!!s?.ok))+'/2','DiD and synthetic control')}
`; out+=box('Panel completeness','Coverage uses selected outcome, countries and calendar years.',pairTable(c.map(r=>({...r,country:countryName(r.country),coverage:100*r.present/r.expected})),[{key:'country',label:'Economy'},{key:'present',label:'Present'},{key:'expected',label:'Expected'},{key:'coverage',label:'Coverage %',numeric:true,number:true,decimals:1},{key:'latest',label:'Latest year'}])); out+=`
${box('Source boundaries','How to read the status at the top of the page',``)}${box('Quality gates','Deterministic method checks',`
● At least three matched years before and after for DiD
● At least five shared pre and two post years for synthetic control
● At least two donor economies
● No interpolation of missing observations
`)}
`; return out; } function protocol(){ const o=state.opts,d=state.did,p=d?.ok?E.pretrend(d):null; let out=heading('07','Research protocol','Record intervention provenance and assumptions before interpreting results.'); const policy=$('policyName').value.trim(),url=validatedPolicyUrl(); out+=`
${box('Design register','User-defined selections; no automatic policy-event verification',`
Treated economy${esc(countryName(o.country))}
Comparator set${esc(o.controls.map(countryName).join(', '))}
Outcome${esc(nameShort(o.indicator))}
Study window${o.start}–${o.end}
Break year${o.breakYear}
Policy description${esc(policy||'Not documented')}
External evidence URL${url?`Open source ↗`:'Not provided'}
Data source${esc(state.source)}
`)}${box('Identification assumptions','These are not automatically established',`` )}
`; out+=box('Researcher notes','Editable notes are kept in browser memory and included in exported sessions.',`
Notes are not uploaded to our server; export a JSON session to save them before closing the page.
`); out+=notice('Model output is never equivalent to a validated policy evaluation. Distinguish exploratory comparisons from causal identification.',true);return out; } function createReport(){ const o=state.opts,d=state.did,s=state.synth,trend=d?.ok?E.pretrend(d):null; const dataLabel=state.source==='demo'?'SYNTHETIC DEMO, NOT REAL COUNTRY DATA':state.source==='worldbank'?'WORLD BANK INDICATORS API, direct browser fetch':'IMPORTED UNVERIFIED DATA'; const pre=d?.ok?d.preYears:'N/A',post=d?.ok?d.postYears:'N/A'; const url=validatedPolicyUrl(); return `# ImpactLab LATAM — Exploratory research record\n\nGenerated: ${new Date().toISOString()}\nData provenance: ${dataLabel}\nIndicator: ${nameShort(o.indicator)} (${o.indicator}, ${valueUnit()})\nTreated economy: ${countryName(o.country)} (${o.country})\nDonors: ${o.controls.map(x=>countryName(x)).join(', ')}\nYears: ${o.start}–${o.end}; hypothetical break: ${o.breakYear}\nIntervention description: ${$('policyName').value.trim()||'Not documented'}\nIntervention external source: ${url||'Not provided'}\n\n## Difference-in-Differences (descriptive)\n\nEstimate: ${d?.ok?fmt(d.effect,4):'Not estimable'} ${valueUnit()}\nMatched years: ${pre} before; ${post} after.\nPretrend slope difference: ${trend?.ok?fmt(trend.slopeDifference,4):'Unavailable'} ${valueUnit()}/year.\nYear-resampling descriptive interval: ${d?.ok?E.bootstrapDid(d,400,20261008).interval.map(x=>fmt(x,4)).join(' to '):'Unavailable'}\n\n## Synthetic control (descriptive)\n\nMean post-break gap: ${s?.ok?fmt(s.postMeanGap,4):'Not estimable'} ${valueUnit()}\nPre-break RMSE: ${s?.ok?fmt(s.preRmse,4):'Unavailable'}\nDonor weights: ${s?.ok?s.weights.map(x=>x.country+'='+fmt(x.weight,4)).join('; '):'Unavailable'}\n\n## Notes\n\n${state.notes||'No notes provided.'}\n\n## Required interpretation caveats\n\nEstimates are exploratory associations, not identified causal effects. Difference-in-Differences requires credible parallel trends, controls for contemporaneous shocks and justified policy assignment. Synthetic controls depend on donor selection and pre-break fit. Placebo exercises are descriptive, not formal p-values. Missing data are not imputed. The year-resampling interval is not a valid causal confidence interval.\n\n## Technical references\n- World Bank Indicators API v2: https://datahelpdesk.worldbank.org/knowledgebase/articles/889392\n- Study protocol and algorithms: METHODOLOGY.md (in source repository)\n- Open-source code: engine.js\n`; } function downloadText(name,text,type){ const blob=new Blob([text],{type});const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000); } function exportRows(){ const o=state.opts;return state.rows.filter(r=>r.indicator===o.indicator&&[o.country,...o.controls].includes(r.country)&&r.year>=o.start&&r.year<=o.end); } function csvExport(){downloadText('impactlab_selected_panel.csv',E.toCSV(exportRows(),['country','year','indicator','value','source']),'text/csv;charset=utf-8');} function sessionJson(){return JSON.stringify({version:'1.0.0',source:state.source,indicatorLoaded:state.indicatorLoaded,opts:state.opts,rows:state.rows,policyName:$('policyName').value,policyUrl:validatedPolicyUrl(),notes:state.notes,exportedAt:new Date().toISOString(),methodology:'Descriptive comparisons, no causal identification.'},null,2);} function exportView(){ let out=heading('08','Export & reproducibility','Download data, the analysis protocol, and a full restorable research session.'); out+=`
${metric('Export formats','3','CSV · Markdown · JSON')}${metric('Panel rows',String(exportRows().length),'selected outcome and economies')}${metric('Session records',String(state.rows.length),'restore all loaded observations')}${metric('License','MIT','Source code only')}
`; out+=box('Download your study','Source-aware exports; synthetic data remain labeled.',`

Full JSON sessions include the selected outcome data, settings and research notes. Review contents before sharing publicly.

`); out+=`
${box('Restore a prior session','Import only files that you trust. Imported data are never automatically treated as verified official statistics.',`

A session must include a records array and a valid research design. Uploading does not communicate files to a server.

`)}${box('Method disclosure','Transparency is part of the research output',``)}
`; out+=box('Report preview','This Markdown file records the key assumptions and limitations.',`
${esc(createReport())}
`);return out; } const renderers={overview,trends,did:didView,synthetic:synthView,placebos,quality,protocol,export:exportView}; function render(){ document.querySelectorAll('.tab').forEach(btn=>btn.classList.toggle('active',btn.dataset.tab===state.activeTab)); $('view').innerHTML=renderers[state.activeTab](); if(state.activeTab==='protocol')$('researchNotes').addEventListener('input',ev=>state.notes=ev.target.value); if(state.activeTab==='export'){ $('exportCsv').onclick=csvExport;$('exportMd').onclick=()=>downloadText('impactlab_research_report.md',createReport(),'text/markdown;charset=utf-8'); $('exportJson').onclick=()=>downloadText('impactlab_reproducible_session.json',sessionJson(),'application/json;charset=utf-8'); $('importJson').onchange=importSession; } } async function importSession(ev){ const f=ev.target.files?.[0];if(!f)return; try{if(f.size>5_000_000)throw Error('JSON file exceeds 5 MB.'); const json=JSON.parse(await f.text());if(!json||!Array.isArray(json.rows)||!json.opts)throw Error('Not an ImpactLab session.'); const rows=E.validateRows(json.rows);if(!rows.length)throw Error('No usable data rows in this file.'); const opts=json.opts;E.checkOptions(opts); if(!E.indicators[opts.indicator]||!E.countries[opts.country]||opts.controls.some(x=>!E.countries[x]))throw Error('Session contains an unsupported country or indicator.'); if(rows.length>15000)throw Error('Session exceeds 15,000 usable rows.'); state.rows=rows;state.source='imported';state.indicatorLoaded=null;state.opts={...opts,controls:[...opts.controls]};state.notes=String(json.notes||'').slice(0,5000); setDefaultForm();$('policyName').value=String(json.policyName||'').slice(0,140);$('policyUrl').value=String(json.policyUrl||'').slice(0,200); state.activeTab='overview';refresh(); }catch(e){error('Could not restore session: '+e.message);} } async function loadLive(){ if(state.loading)return; const o=optionsFromForm();try{E.checkOptions(o);}catch(ex){error(ex.message);return;} state.loading=true;$('liveBtn').textContent='Loading World Bank API…';$('liveBtn').disabled=true;error(''); const countries=Object.keys(E.countries).join(';'),years='1998:2025'; const endpoint=`https://api.worldbank.org/v2/country/${countries}/indicator/${encodeURIComponent(o.indicator)}?format=json&per_page=2500&date=${years}`; const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),18000); try{ const resp=await fetch(endpoint,{signal:controller.signal,headers:{Accept:'application/json'}}); if(!resp.ok)throw Error(`World Bank API HTTP ${resp.status}`); const body=await resp.json(),rows=E.validateRows(E.parseWorldBank(body,o.indicator)); if(rows.filter(r=>E.finite(r.value)).length<20)throw Error('Too few official observations. The API may be unavailable for this indicator.'); state.rows=rows;state.indicatorLoaded=o.indicator;state.source='worldbank';state.activeTab='overview';refresh(); }catch(ex){error('Live data could not be loaded. Previous data were preserved. '+(ex.name==='AbortError'?'Request timed out.':ex.message));} finally{clearTimeout(timer);state.loading=false;$('liveBtn').disabled=false;$('liveBtn').textContent='↻ Load World Bank data';} } function resetDemo(){state.source='demo';state.rows=D.records.slice();state.indicatorLoaded=null;state.opts={...optsDefault,controls:[...optsDefault.controls]};state.activeTab='overview';state.notes='';setDefaultForm();refresh();} $('tabs').addEventListener('click',ev=>{const b=ev.target.closest('button[data-tab]');if(!b)return;state.activeTab=b.dataset.tab;render();}); $('treated').addEventListener('change',()=>{state.opts.controls=Array.from($('controls').querySelectorAll('input:checked')).map(x=>x.value).filter(x=>x!==$('treated').value);renderDonors();}); $('controls').addEventListener('change',()=>{state.opts.controls=Array.from($('controls').querySelectorAll('input:checked')).map(x=>x.value);}); $('runBtn').addEventListener('click',refresh); $('liveBtn').addEventListener('click',loadLive); $('demoBtn').addEventListener('click',resetDemo); setDefaultForm();refresh(); })();