import {COUNTRIES,INDICATORS,IDS,COUNTRY_IDS,finite,escapeHTML,formatValue,latest,subset,coverage,status,timeline,compareCountries,crossCountryGap,paired,pearson,change,toCSV,validateSession,normalizeRows} from './core.js'; import {makeWorldBankURL,requestWorldBank} from './worldbank.js'; import {createDemo} from './demo-data.js'; const $=id=>document.getElementById(id); const COLOR=['#007e77','#3562af','#df8c43','#8b6cc2','#d75872','#4a9d9c','#b78d27','#3f8294']; const STATE={rows:createDemo(),mode:'demo',url:null,loadedAt:null,loadedCountries:[],activeView:'overview'}; const saved=['ARG','BOL','BRA','CHL','COL','PRY']; function populate(){ $('countries').innerHTML=COUNTRY_IDS.map(c=>``).join(''); $('indicator').innerHTML=IDS.map(id=>``).join(''); $('compare-indicator').innerHTML=IDS.map(id=>``).join(''); $('gap-year').value='2020'; } function chosen(){return [...$('countries').selectedOptions].map(o=>o.value);} function filters(){const countries=chosen(),start=Number($('start').value),end=Number($('end').value),ind=$('indicator').value,other=$('compare-indicator').value; if(!countries.length)throw new Error('Choose at least one country.');if(countries.length>8)throw new Error('Choose at most eight countries.');if(!Number.isInteger(start)||!Number.isInteger(end)||start<1990||end>2026||start>end)throw new Error('Choose a valid year range between 1990 and 2026.'); return {countries,start,end,ind,other};} function setAlert(text,type='demo'){const box=$('alert');box.textContent=text;box.className='alert '+type;} function withFilters(fn){try{return fn(filters());}catch(e){setAlert(e.message,'error');return null;}} function esc(n){return escapeHTML(n);} function num(n,d=1){return n===null||n===undefined?'N/A':Number(n).toLocaleString('en-US',{maximumFractionDigits:d});} function smallTitle(t){return esc(t);} function empty(msg){return `
${esc(msg)}
`;} function updateTop(f){ const visible=STATE.rows.filter(r=>f.countries.includes(r.iso3)&&r.year>=f.start&&r.year<=f.end); const st=status(visible,f.countries,IDS,f.start,f.end); $('metric-countries').textContent=f.countries.length; $('metric-obs').textContent=num(st.count,0); $('metric-coverage').textContent=(100*st.ratio).toFixed(1)+'%'; $('metric-lag').textContent=st.meanLag===null?'N/A':st.meanLag.toFixed(1)+'y'; $('mode-pill').className='mode-pill '+STATE.mode;$('mode-pill').textContent=STATE.mode==='live'?'WORLD BANK LIVE':STATE.mode==='imported'?'IMPORTED DATA':'SYNTHETIC DEMO'; $('status-title').textContent=STATE.mode==='live'?'Official data retrieved from World Bank API':STATE.mode==='imported'?'User-imported research session':'Synthetic research preview'; $('status-sub').textContent=STATE.mode==='live'?`Retrieved ${STATE.loadedAt||'this session'} · ${st.count} valid observations`:(STATE.mode==='imported'?'User-supplied values; original provenance not verified.':'Generated values for demonstrating application functionality.'); $('footer-source').textContent=STATE.mode==='live'?'World Bank WDI source 2 · last loaded '+STATE.loadedAt:STATE.mode==='imported'?'USER-IMPORTED OBSERVATIONS':'SYNTHETIC DEMO, NOT FOR CITATION'; if(STATE.mode==='demo')setAlert('SYNTHETIC DEMO: These figures are invented for interface testing. Click “Load World Bank data” to retrieve official observations.','demo'); else if(STATE.mode==='imported')setAlert('IMPORTED SESSION: User-provided data. Source and accuracy have not been independently verified.','imported'); else {const missing=f.countries.filter(c=>!STATE.loadedCountries.includes(c));setAlert(missing.length?'WORLD BANK DATA: New selected countries were not in the last request. Click “Load World Bank data” again.':`OFFICIAL WORLD BANK DATA: ${st.count} non-missing records. Each observation retains its original year. No imputation.`,missing.length?'error':'live');} } function renderOverview(f){ const rank=compareCountries(STATE.rows,f.countries,f.ind,f.start,f.end); $('overview-meta').textContent=INDICATORS[f.ind].short+' · '+f.start+'–'+f.end; const max=rank.length?Math.max(...rank.map(x=>Math.max(0,x.point.value)),0):0; $('rank-bars').innerHTML=rank.length?rank.map((x,i)=>`
${esc(COUNTRIES[x.country])}
${esc(formatValue(x.point.value,f.ind))}${x.point.year}
`).join(''):empty('No observations found. Try a different indicator or wider year range.'); $('rank-table').innerHTML=rank.length?`
${rank.map((x,i)=>``).join('')}
RankEconomyIndicatorReported valueYearLag
${i+1}${esc(COUNTRIES[x.country])}${esc(INDICATORS[f.ind].short)}${esc(formatValue(x.point.value,f.ind))}${x.point.year}${f.end-x.point.year}
`:empty('No comparable country observations in this interval.'); } function bounds(nums){let min=Math.min(...nums),max=Math.max(...nums);if(!Number.isFinite(min)||!Number.isFinite(max))return [0,1];if(max===min){const delta=Math.max(1,Math.abs(max)*.05);return[min-delta,max+delta];}const p=(max-min)*.08;return[min-p,max+p];} function renderTrends(f){ $('trend-title').textContent=INDICATORS[f.ind].label; const series=timeline(STATE.rows,f.countries,f.ind,f.start,f.end),vals=series.flatMap(s=>s.points.map(p=>p.value)); if(!vals.length){$('line-chart').innerHTML=empty('No observations to plot for this indicator.');$('legend').innerHTML='';$('trend-notes').innerHTML='';return;} const w=800,h=330,pad={l:74,r:30,t:25,b:45},[ymin,ymax]=bounds(vals),xx=yr=>pad.l+(yr-f.start)/Math.max(1,f.end-f.start)*(w-pad.l-pad.r),yy=v=>h-pad.b-(v-ymin)/(ymax-ymin)*(h-pad.t-pad.b); let svg=``; for(let i=0;i<6;i++){const v=ymin+i*(ymax-ymin)/5,y=yy(v);svg+=`${esc(num(v,1))}`;} const step=Math.max(1,Math.ceil((f.end-f.start)/7));for(let y=f.start;y<=f.end;y+=step)svg+=`${y}`; series.forEach((s,j)=>{const color=COLOR[j%COLOR.length];for(let i=1;i`; }for(const p of s.points)svg+=`${esc(COUNTRIES[p.iso3])} ${p.year}: ${esc(formatValue(p.value,f.ind))}`;});svg+=''; $('line-chart').innerHTML=svg;$('legend').innerHTML=series.map((s,i)=>`${esc(COUNTRIES[s.iso3])} (${s.points.length})`).join(''); const lines=series.map(s=>{if(s.points.length<2)return `${esc(COUNTRIES[s.iso3])}Insufficient years`;const ch=change(s.points[0],s.points[s.points.length-1]);return `${esc(COUNTRIES[s.iso3])}${s.points[0].year} → ${s.points[s.points.length-1].year}${ch?esc((ch.amount>0?'+':'')+num(ch.amount,2)): 'N/A'}${s.points.length}`;}); $('trend-notes').innerHTML=`

Observed change, without interpolation

${lines.join('')}
EconomyFirst → lastChange (indicator units)Reported years

${esc(INDICATORS[f.ind].tip)} Change uses earliest and latest available observations, not necessarily the requested boundary years.

`; } function renderGaps(f){ const selected=f.countries;$('gap-a').innerHTML=selected.map(c=>``).join('');$('gap-b').innerHTML=selected.map(c=>``).join(''); const priorA=$('gap-a').dataset.last,priorB=$('gap-b').dataset.last;$('gap-a').value=selected.includes(priorA)?priorA:selected[0];$('gap-b').value=selected.includes(priorB)?priorB:(selected[1]||selected[0]); const yr=Number($('gap-year').value);if(yrf.end){$('gap-year').value=String(f.end);} renderGapResult(f); const firsts=timeline(STATE.rows,selected,f.ind,f.start,f.end); $('progress-table').innerHTML=`

Progress diagnostics

First and last reported points; no missing years filled.
${firsts.map(s=>{if(!s.points.length)return ``;const a=s.points[0],b=s.points[s.points.length-1],d=change(a,b);return ``;}).join('')}
EconomyFirst reportedLast reportedChangeAvailable years
${esc(COUNTRIES[s.iso3])}Missing
${esc(COUNTRIES[s.iso3])}${esc(formatValue(a.value,f.ind))} (${a.year})${esc(formatValue(b.value,f.ind))} (${b.year})${d?(d.amount>0?'+':'')+esc(num(d.amount,2)):'N/A'}${s.points.length}
`; } function renderGapResult(f){const a=$('gap-a').value,b=$('gap-b').value,yr=Number($('gap-year').value);$('gap-a').dataset.last=a;$('gap-b').dataset.last=b; if(!a||!b||a===b||!Number.isInteger(yr)||yrf.end){$('gap-result').innerHTML=empty('Choose two different economies and a year within the selected range.');return;} const z=crossCountryGap(STATE.rows,a,b,f.ind,f.start,f.end,yr);$('gap-result').innerHTML=z?`
${z.delta>=0?'+':''}${esc(num(z.delta,2))} ${esc(INDICATORS[f.ind].unit)} · ${esc(COUNTRIES[a])} minus ${esc(COUNTRIES[b])} in ${yr}

${esc(COUNTRIES[a])}: ${esc(formatValue(z.a,f.ind))} · ${esc(COUNTRIES[b])}: ${esc(formatValue(z.b,f.ind))}. This is a descriptive cross-country difference, not an estimate of within-country equity.

`:empty('At least one of these countries has no observation for this exact year. Choose another year; values are never filled in.');} function renderRelations(f){const a=f.ind,b=f.other;if(a===b){$('scatter-chart').innerHTML=empty('Choose a different association variable.');$('relationship-meta').textContent='Comparing a measure with itself is not useful.';return;} const pairs=paired(STATE.rows,a,b,f.countries,f.start,f.end);if(pairs.length<3){$('scatter-chart').innerHTML=empty('Too few matched country-year observations. Try a wider period or a series with better coverage.');$('relationship-meta').textContent=`Only ${pairs.length} valid matched pairs. At least three are required.`;return;} const w=800,h=355,p={l:85,r:25,t:25,b:72},[xmin,xmax]=bounds(pairs.map(x=>x[a])),[ymin,ymax]=bounds(pairs.map(x=>x[b]));const xx=x=>p.l+(x-xmin)/(xmax-xmin)*(w-p.l-p.r),yy=y=>h-p.b-(y-ymin)/(ymax-ymin)*(h-p.t-p.b); let svg=``; for(let i=0;i<5;i++){const v=ymin+i*(ymax-ymin)/4,y=yy(v);svg+=`${esc(num(v,1))}`;} for(let i=0;i<5;i++){const v=xmin+i*(xmax-xmin)/4,x=xx(v);svg+=`${esc(num(v,1))}`;} svg+=`${esc(INDICATORS[a].short)} (${esc(INDICATORS[a].unit)})`; svg+=`${esc(INDICATORS[b].short)} (${esc(INDICATORS[b].unit)})`; for(const o of pairs){const ci=f.countries.indexOf(o.iso3);svg+=`${esc(COUNTRIES[o.iso3])} ${o.year}: ${esc(num(o[a],2))} / ${esc(num(o[b],2))}`;}svg+=''; const r=pearson(pairs,a,b);$('scatter-chart').innerHTML=svg; $('relationship-meta').innerHTML=`Paired observations: ${pairs.length} (country-year pairs) · Pearson r: ${r===null?'N/A':num(r,3)} · Selected countries: ${f.countries.length}.
This is an exploratory descriptive correlation. The same country appears in multiple years, observations are not independent, reporting may be uneven, and no statistical significance or causal claim is implied.`; } function renderQuality(f){const years=f.end-f.start+1;const sample=STATE.rows.filter(r=>f.countries.includes(r.iso3)&&r.year>=f.start&&r.year<=f.end),cols=IDS; const matrix=f.countries.map(c=>{const cells=cols.map(ind=>{const points=sample.filter(r=>r.iso3===c&&r.indicator===ind);const ratio=points.length/years,shade=ratio===0?'background:#fae8e5':`background:rgba(9,151,134,${(.09+.65*ratio).toFixed(2)})`;return `${num(ratio*100,0)}%`;}).join('');return `${esc(COUNTRIES[c])}${cells}`;}).join(''); $('coverage-matrix').innerHTML=`
${cols.map(id=>``).join('')}${matrix}
Economy${esc(INDICATORS[id].short)}

Color darkness approximates the share of selected years with a reported value. Coverage is not a quality certificate.

`; const st=status(sample,f.countries,IDS,f.start,f.end);$('source-audit').innerHTML=`

Audit trail and caveats

Data mode: ${STATE.mode==='live'?'World Bank WDI API, source 2':STATE.mode==='demo'?'SYNTHETIC DEMO (not official)':'User imported, source unverified'}.
Period: ${f.start}–${f.end}. Coverage: ${st.count}/${st.eligible} possible country-year-indicator observations. Reporting lag: ${st.meanLag===null?'N/A':st.meanLag.toFixed(1)+' years'} averaged over available series.

UNESCO UIS supplies many education series reproduced in World Development Indicators. Gross enrollment and primary completion definitions should not be interpreted as graduation rates, and gross ratios can exceed 100%. Missing values stay missing. Cross-country coverage and vintage differ. GDP per capita and Internet use are contextual, not direct school outcomes.

API: ${STATE.url?`Exact query link`:'Not queried live in this session.'} · World Bank API documentation · Indicator metadata glossary

`; } function makeBrief(f){const s=subset(STATE.rows,f.countries,f.ind,f.start,f.end),rank=compareCountries(STATE.rows,f.countries,f.ind,f.start,f.end),st=status(STATE.rows,f.countries,IDS,f.start,f.end),pairs=paired(STATE.rows,f.ind,f.other,f.countries,f.start,f.end);const r=f.ind===f.other?null:pearson(pairs,f.ind,f.other);const mode=STATE.mode==='live'?'OFFICIAL WORLD BANK WDI OBSERVATIONS':STATE.mode==='demo'?'SYNTHETIC DEMO, NOT CITABLE':'USER-IMPORTED DATA, SOURCE UNVERIFIED'; const lines=[`# Education Data Lab: Comparative Research Brief`,``,`**Data status:** ${mode}`,`**Period:** ${f.start}–${f.end}`,`**Countries:** ${f.countries.map(c=>COUNTRIES[c]).join(', ')}`,`**Primary measure:** ${INDICATORS[f.ind].label} (${INDICATORS[f.ind].unit})`,`**Association measure:** ${INDICATORS[f.other].label}`,`**Generated:** ${new Date().toISOString()}`,``,`## Observations`,`- ${s.length} non-missing observations for the primary measure; ${st.count}/${st.eligible} country-year-indicator cells observed across the 8 tracked series.`,`- Latest reported values may have different reporting years and must not be described as same-year comparisons.`]; if(rank.length){const top=rank[0];lines.push(`- Highest latest *reported* value within these selected economies: ${COUNTRIES[top.country]} (${formatValue(top.point.value,f.ind)}, ${top.point.year}). This is not a causal ranking or necessarily the latest calendar year.`);}else lines.push('- No latest reported observation is available for this measure in the selected window.'); for(const item of rank.slice(0,8))lines.push(`- ${COUNTRIES[item.country]}: ${formatValue(item.point.value,f.ind)} (${item.point.year})`); lines.push('','## Exploratory association');if(r!==null)lines.push(`- Pearson r for ${pairs.length} matched country-year pairs: ${r.toFixed(3)}. Descriptive only; repeated country-years are not independent and **no causal or significance inference** follows.`);else lines.push('- Insufficient or unsuitable paired observations for correlation.'); lines.push('','## Methodological safeguards','- Missing years are not interpolated, carried forward or imputed.','- Gross education indicators may exceed 100%; they are not net participation rates.','- Data vintages, coverage, definitions and reporting frequency differ across countries.','- This brief is an exploratory descriptive analysis, not an impact evaluation or recommendation for resource allocation.','',`## Provenance`,`- Mode: ${mode}`,`- Query: ${STATE.url||'No live query; generated demo or imported file.'}`,'- World Bank Indicators API v2, WDI source 2: https://datahelpdesk.worldbank.org/knowledgebase/articles/889392','- Definitions: https://databank.worldbank.org/metadataglossary/world-development-indicators/','');return lines.join('\n');} function renderBrief(f){const txt=makeBrief(f);$('brief-text').innerHTML=txt.split('\n').map(line=>{ if(line.startsWith('# '))return `

${esc(line.slice(2))}

`; if(line.startsWith('## '))return `

${esc(line.slice(3))}

`; if(line.startsWith('- '))return `

• ${esc(line.slice(2))}

`; return line?`

${esc(line)}

`:'';}).join('');} function render(){withFilters(f=>{updateTop(f);renderOverview(f);renderTrends(f);renderGaps(f);renderRelations(f);renderQuality(f);renderBrief(f);});} function rowsForSelection(){const f=filters();return STATE.rows.filter(r=>f.countries.includes(r.iso3)&&r.year>=f.start&&r.year<=f.end);} function download(text,name,type){const blob=new Blob([text],{type});const u=URL.createObjectURL(blob);const a=document.createElement('a');a.href=u;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(u),10000);} function sendExport(type){withFilters(f=>{const suf=STATE.mode==='demo'?'SYNTHETIC_DEMO':STATE.mode==='live'?'WORLD_BANK':'USER_IMPORTED';const pre='education-data-lab_'+suf+'_'+new Date().toISOString().slice(0,10); if(type==='csv')download(toCSV(rowsForSelection()),pre+'.csv','text/csv;charset=utf-8'); else if(type==='md')download(makeBrief(f),pre+'_research-brief.md','text/markdown;charset=utf-8'); else download(JSON.stringify({format:'edlab-session-v1',mode:STATE.mode,sourceURL:STATE.url,exportedAt:new Date().toISOString(),filters:f,rows:rowsForSelection(),disclaimer:'Demo values are synthetic; imported files are unverified; live source is World Bank WDI.'},null,2),pre+'.json','application/json');});} function bind(){ for(const id of ['countries','indicator','start','end','compare-indicator'])$(id).addEventListener('change',render); document.querySelectorAll('.tab').forEach(btn=>btn.addEventListener('click',()=>{document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active',t===btn));document.querySelectorAll('.view').forEach(v=>v.classList.toggle('active',v.id==='view-'+btn.dataset.view));STATE.activeView=btn.dataset.view;})); for(const id of ['gap-a','gap-b','gap-year'])$(id).addEventListener('change',()=>withFilters(renderGapResult)); $('load-demo').addEventListener('click',()=>{STATE.rows=createDemo();STATE.mode='demo';STATE.url=null;STATE.loadedAt=null;STATE.loadedCountries=[];render();}); $('load-live').addEventListener('click',async()=>{const f=withFilters(x=>x);if(!f)return;const btn=$('load-live');btn.disabled=true;btn.textContent='Loading official data…';setAlert('Fetching official observations from the World Bank. Please wait.','live'); try{const {rows,url}=await requestWorldBank(f.countries,IDS,f.start,f.end);STATE.rows=rows;STATE.mode='live';STATE.url=url;STATE.loadedAt=new Date().toLocaleString();STATE.loadedCountries=[...f.countries];render();} catch(e){setAlert('Live data could not be loaded: '+e.message+'. Previously shown data were not replaced. You can use the clearly labeled demo meanwhile.','error');} finally{btn.disabled=false;btn.textContent='↗ Load World Bank data';} }); $('download-csv').addEventListener('click',()=>sendExport('csv'));$('export-md').addEventListener('click',()=>sendExport('md'));$('export-json').addEventListener('click',()=>sendExport('json')); $('import-json').addEventListener('click',()=>$('import-file').click());$('import-file').addEventListener('change',async(e)=>{const file=e.target.files?.[0];if(!file)return;try{if(file.size>6e6)throw new Error('Maximum file size is 6 MB');const json=JSON.parse(await file.text());const parsed=validateSession(json);STATE.rows=parsed.rows;STATE.mode='imported';STATE.url=null;STATE.loadedCountries=[];STATE.loadedAt=new Date().toLocaleString();render();}catch(error){setAlert('Import failed: '+error.message,'error');}finally{e.target.value='';}}); } populate();bind();render();