1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<meta charset="UTF-8"/> 5<meta name="viewport" content="width=device-width,initial-scale=1"/> 6<title>PAL Network â ICAN ICAR Dashboard</title> 7<!-- Google tag (gtag.js) -->
vendor: 64 bytes, lines 7-8
7 8<script async src="https://www.googletagmanager.com/gtag/js?id=
8G-LPRWNRKRQV
vendor: 12 bytes, line 8
8"></script>
9<script> 10
vendor: 133 bytes, lines 10-13
10window.dataLayer = window.dataLayer || []; 11 function gtag(){dataLayer.push(arguments);} 12 gtag('js', new Date()); 13 gtag('config', '
13G-LPRWNRKRQV
vendor: 4 bytes, line 13
13');
14</script>
vendor: 1 bytes, line 14
14
15<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
vendor: 1 bytes, line 15
15
16<script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"></script>
16 17<style> 18*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} 19body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#F0F4F8;color:#1A202C;height:100vh;display:flex;flex-direction:column;overflow:hidden} 20 21/* ââ Navbar âââââââââââââââââââââââââââââââââââââââââââââ */ 22.navbar{background:#fff;color:#1A202C;padding:8px 24px;display:flex;align-items:center;gap:20px;position:sticky;top:0;z-index:100;box-shadow:0 1px 6px rgba(0,0,0,.12);border-bottom:1px solid #E2E8F0;flex-shrink:0} 23.navbar-logo{display:flex;align-items:center;gap:0;flex-shrink:0} 24.navbar-divider{width:1px;height:36px;background:#E2E8F0;margin:0 4px} 25.navbar-survey{display:flex;flex-direction:column} 26.navbar-survey-title{font-size:13px;font-weight:700;color:#014663;letter-spacing:.3px} 27.navbar-survey-sub{font-size:11px;color:#718096} 28.navbar-tag{background:#66B255;padding:3px 10px;border-radius:4px;font-size:11px;font-weight:700;letter-spacing:.8px;margin-left:auto;color:#fff} 29 30/* ââ Page tabs ââââââââââââââââââââââââââââââââââââââââââ */ 31.page-tabs{display:flex;background:#fff;border-bottom:2px solid #E2E8F0;padding:0 20px;flex-shrink:0} 32.page-tab{padding:10px 20px;font-size:13px;font-weight:600;color:#718096;border:none;background:none;cursor:pointer;border-bottom:3px solid transparent;margin-bottom:-2px;transition:all .15s} 33.page-tab.active{color:#014663;border-bottom-color:#014663} 34.page-tab:hover:not(.active){color:#2D3748;background:#F7FAFC} 35 36/* ââ Layout âââââââââââââââââââââââââââââââââââââââââââââ */ 37.layout{display:flex;flex:1;min-height:0;overflow:hidden} 38 39/* ââ Sidebar ââââââââââââââââââââââââââââââââââââââââââââ */ 40.sidebar{width:260px;min-width:260px;flex-shrink:0;background:#fff;border-right:1px solid #E2E8F0;overflow-y:auto;overflow-x:hidden;padding:12px} 41 42details{border:1px solid #E2E8F0;border-radius:8px;margin-bottom:6px;overflow:hidden} 43summary{list-style:none;padding:8px 12px;cursor:pointer;background:#F7FAFC;font-size:12px;font-weight:600;color:#4A5568;display:flex;justify-content:space-between;align-items:center;user-select:none} 44summary::-webkit-details-marker{display:none} 45summary::after{content:'â¶';font-size:9px;transition:transform .2s;color:#A0AEC0} 46details[open] summary::after{transform:rotate(90deg)} 47summary:hover{background:#EDF2F7} 48.filter-body{padding:10px 12px;background:#fff} 49 50.cb-list{display:flex;flex-direction:column;gap:2px;max-height:160px;overflow-y:auto} 51.cb-item{display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;padding:3px 4px;border-radius:4px} 52.cb-item:hover{background:#EDF2F7} 53.cb-item input[type=checkbox]{cursor:pointer;accent-color:#014663;flex-shrink:0} 54.cb-actions{display:flex;gap:4px;margin-top:5px} 55 56.pill-row{display:flex;gap:4px;flex-wrap:wrap} 57.pill{padding:4px 10px;border-radius:20px;font-size:11px;font-weight:600;border:1.5px solid #CBD5E0;background:#fff;cursor:pointer;transition:all .15s;color:#4A5568} 58.pill.active{background:#014663;border-color:#014663;color:#fff} 59.pill:hover:not(.active){background:#EDF2F7} 60 61.chip{font-size:11px;padding:2px 7px;border-radius:10px;background:#EDF2F7;border:1px solid #CBD5E0;cursor:pointer;color:#4A5568;transition:all .15s} 62.chip:hover{background:#014663;color:#fff;border-color:#014663} 63 64.filter-select{width:100%;padding:6px 8px;border-radius:6px;border:1.5px solid #CBD5E0;font-size:12px;background:#fff;color:#2D3748;cursor:pointer;outline:none} 65.filter-select:focus{border-color:#014663} 66 67.btn-row{display:flex;gap:6px;margin-top:8px} 68.btn{flex:1;padding:8px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:none;transition:all .15s} 69.btn-primary{background:#014663;color:#fff} 70.btn-primary:hover{background:#012f47} 71.btn-reset{background:#EDF2F7;color:#4A5568;border:1.5px solid #CBD5E0} 72.btn-reset:hover{background:#E2E8F0} 73.sidebar-n{font-size:12px;color:#1A202C;text-align:center;margin-top:4px} 74 75/* ââ Main area ââââââââââââââââââââââââââââââââââââââââââ */ 76.main{flex:1;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:14px} 77.page{display:none;flex-direction:column;gap:14px} 78.page.active{display:flex} 79 80/* Controls row */
81.controls-row{display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap} 82.ctrl-group{display:flex;flex-direction:column;gap:4px} 83.ctrl-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#718096} 84.mpl-tabs{display:flex;border-radius:8px;overflow:hidden;border:1.5px solid #CBD5E0} 85.mpl-tab{padding:7px 14px;font-size:12px;font-weight:600;background:#fff;color:#4A5568;border:none;cursor:pointer;border-right:1px solid #CBD5E0;transition:all .15s} 86.mpl-tab:last-child{border-right:none} 87.mpl-tab.active{background:#014663;color:#fff} 88.mpl-tab:hover:not(.active){background:#EDF2F7} 89 90/* Info note */ 91.info-note{background:#EBF8FF;border:1px solid #90CDF4;border-radius:8px;padding:10px 14px;font-size:12px;color:#014663;line-height:1.6} 92.info-note details summary{background:transparent;padding:0;font-size:12px;font-weight:600;color:#014663;border:none} 93.info-note details[open] summary{margin-bottom:8px} 94.info-note details .filter-body{padding:0;background:transparent;font-size:12px;color:#2C5282;line-height:1.6} 95 96/* Chart box */ 97.chart-box{background:#fff;border-radius:12px;padding:18px 20px;box-shadow:0 1px 4px rgba(0,0,0,.07)} 98.chart-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:4px;flex-wrap:wrap;gap:8px} 99.chart-title{font-size:13px;font-weight:700;color:#014663} 100.chart-sub{font-size:11px;color:#718096;margin-top:2px} 101.warn-note{font-size:11px;color:#C05621;background:#FFFAF0;border:1px solid #F6AD55;border-radius:6px;padding:3px 9px} 102.chart-wrap{position:relative;height:380px} 103 104/* All-MPL 2-panel grid (Math + Reading only) */ 105.chart-all-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px} 106.chart-panel{background:#F7FAFC;border-radius:10px;padding:14px 16px;border:1px solid #E2E8F0} 107.chart-panel-title{font-size:12px;font-weight:700;text-align:center;color:#014663;margin-bottom:10px;padding-bottom:6px;border-bottom:2px solid #66B255} 108 109/* Table */ 110.table-box{background:#fff;border-radius:12px;padding:16px 20px;box-shadow:0 1px 4px rgba(0,0,0,.07);overflow-x:auto} 111.table-title{font-size:13px;font-weight:700;margin-bottom:10px;color:#014663} 112table{width:100%;border-collapse:collapse;font-size:12px} 113thead th{background:#F7FAFC;padding:7px 12px;text-align:left;font-weight:700;color:#4A5568;border-bottom:2px solid #E2E8F0;font-size:11px;text-transform:uppercase;letter-spacing:.6px;white-space:nowrap} 114tbody tr:hover{background:#F7FAFC} 115tbody td{padding:7px 12px;border-bottom:1px solid #EDF2F7} 116.pct-bar-wrap{display:flex;align-items:center;gap:6px} 117.pct-mini-bar{height:6px;border-radius:3px;background:#014663;min-width:2px} 118.suppressed{color:#C05621;font-style:italic;font-size:11px} 119.n-col{color:#A0AEC0;font-size:11px} 120.warn-badge{display:inline-flex;align-items:center;gap:3px;background:#FFFAF0;border:1px solid #F6AD55;color:#C05621;font-size:10px;font-weight:600;padding:2px 6px;border-radius:10px} 121 122/* Access tab */ 123.access-section{display:flex;flex-direction:column;gap:10px} 124.section-hdr{font-size:13px;font-weight:700;color:#014663;padding:6px 0;border-bottom:2px solid #66B255;margin-bottom:4px} 125.access-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px} 126.access-card{background:#fff;border-radius:10px;padding:14px 16px;box-shadow:0 1px 4px rgba(0,0,0,.07)} 127.access-card-title{font-size:10px;font-weight:700;color:#014663;text-transform:uppercase;letter-spacing:.7px;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center} 128.btn-export{padding:4px 9px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;border:1.5px solid #CBD5E0;background:#fff;color:#4A5568;display:inline-flex;align-items:center;gap:3px;transition:all .15s;white-space:nowrap;flex-shrink:0} 129.btn-export:hover{background:#014663;color:#fff;border-color:#014663} 130.export-btns{display:flex;gap:6px;align-items:center;flex-shrink:0} 131.access-note{font-size:11px;color:#718096;background:#F7FAFC;border-radius:6px;padding:8px 12px;border:1px solid #E2E8F0} 132.acc-wrap{position:relative;height:300px} 133 134/* Loading */ 135.loading{display:none;position:fixed;inset:0;background:rgba(255,255,255,.8);z-index:200;align-items:center;justify-content:center;flex-direction:column;gap:10px} 136.loading.show{display:flex} 137.spinner{width:34px;height:34px;border:4px solid #E2E8F0;border-top-color:#014663;border-radius:50%;animation:spin .7s linear infinite} 138@keyframes spin{to{transform:rotate(360deg)}} 139
140/* Chatbot */ 141.chat-fab{position:fixed;bottom:24px;right:24px;width:54px;height:54px;border-radius:50%;background:#014663;color:#fff;font-size:22px;border:none;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.3);display:flex;align-items:center;justify-content:center;z-index:300;transition:transform .2s} 142.chat-fab:hover{transform:scale(1.08)} 143.chat-panel{position:fixed;bottom:90px;right:24px;width:370px;height:510px;background:#fff;border-radius:14px;box-shadow:0 8px 40px rgba(0,0,0,.2);display:none;flex-direction:column;z-index:300;overflow:hidden;border:1px solid #E2E8F0} 144.chat-panel.open{display:flex} 145.chat-hdr{background:#014663;color:#fff;padding:12px 16px;display:flex;justify-content:space-between;align-items:center} 146.chat-hdr-title{font-size:14px;font-weight:700} 147.chat-hdr-sub{font-size:11px;color:#90CDF4;margin-top:1px} 148.chat-close{background:none;border:none;color:#90CDF4;font-size:18px;cursor:pointer} 149.chat-msgs{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px;background:#F7FAFC} 150.msg{max-width:86%;display:flex;flex-direction:column;gap:2px} 151.msg.user{align-self:flex-end;align-items:flex-end} 152.msg.assistant{align-self:flex-start} 153.msg-bubble{padding:8px 12px;border-radius:12px;font-size:13px;line-height:1.5} 154.msg.user .msg-bubble{background:#014663;color:#fff;border-radius:12px 12px 4px 12px} 155.msg.assistant .msg-bubble{background:#fff;border:1px solid #E2E8F0;white-space:pre-wrap;color:#1A202C;border-radius:12px 12px 12px 4px} 156.msg-time{font-size:10px;color:#A0AEC0} 157.typing-dots{display:flex;gap:4px;padding:8px 12px} 158.typing-dots span{width:7px;height:7px;border-radius:50%;background:#CBD5E0;animation:bounce .9s infinite} 159.typing-dots span:nth-child(2){animation-delay:.15s} 160.typing-dots span:nth-child(3){animation-delay:.3s} 161@keyframes bounce{0%,80%,100%{transform:scale(.8)}40%{transform:scale(1.2)}} 162.chat-examples{padding:10px 14px;background:#EDF2F7;border-top:1px solid #E2E8F0} 163.chat-examples p{font-size:11px;color:#718096;margin-bottom:5px;font-weight:600} 164.ex-chips{display:flex;flex-wrap:wrap;gap:4px} 165.chat-input-row{padding:10px;border-top:1px solid #E2E8F0;display:flex;gap:6px;background:#fff} 166.chat-input{flex:1;padding:7px 12px;border-radius:18px;border:1.5px solid #CBD5E0;font-size:13px;outline:none;resize:none} 167.chat-input:focus{border-color:#014663} 168.chat-send{width:34px;height:34px;border-radius:50%;background:#014663;color:#fff;border:none;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0} 169.chat-send:hover{background:#012f47} 170.chat-send:disabled{background:#CBD5E0;cursor:default} 171 172/* Language toggle */ 173.lang-btn{padding:3px 8px;border-radius:4px;font-size:11px;font-weight:700;border:1.5px solid #CBD5E0;background:#fff;color:#4A5568;cursor:pointer;transition:all .15s} 174.lang-btn.active{background:#014663;color:#fff;border-color:#014663} 175.lang-btn:hover:not(.active){background:#EDF2F7} 176 177/* ââ Guide button & modal âââââââââââââââââââââââââââââââââââ */ 178.guide-btn{padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;border:1.5px solid #014663;background:#fff;color:#014663;cursor:pointer;transition:all .15s;margin-left:8px;white-space:nowrap} 179.guide-btn:hover{background:#014663;color:#fff} 180.labels-toggle-btn{padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;border:1.5px solid #CBD5E0;background:#fff;color:#4A5568;cursor:pointer;transition:all .15s;margin-left:8px;white-space:nowrap} 181.labels-toggle-btn:hover{border-color:#014663;color:#014663} 182.labels-toggle-btn.active{background:#014663;border-color:#014663;color:#fff} 183.guide-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:1000;align-items:center;justify-content:center} 184.guide-overlay.open{display:flex} 185.guide-modal-box{background:#fff;border-radius:12px;width:min(900px,94vw);height:90vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,.25)} 186.guide-modal-hdr{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #E2E8F0;flex-shrink:0} 187.guide-iframe{flex:1;border:none;width:100%} 188.guide-close-btn{background:none;border:none;font-size:16px;cursor:pointer;color:#718096;padding:2px 6px;border-radius:4px;line-height:1} 189.guide-close-btn:hover{background:#EDF2F7;color:#1A202C} 190.guide-download-btn{font-size:11px;font-weight:600;color:#014663;text-decoration:none;padding:4px 10px;border:1.5px solid #014663;border-radius:5px;transition:all .15s} 191.guide-download-btn:hover{background:#014663;color:#fff} 192
193@media(max-width:900px){ 194 .chart-all-grid{grid-template-columns:1fr} 195 .access-grid{grid-template-columns:1fr} 196 .sidebar{width:220px;min-width:220px} 197} 198@media(max-width:680px){ 199 .layout{flex-direction:column;height:auto} 200 .sidebar{width:100%;border-right:none;border-bottom:1px solid #E2E8F0;max-height:none;overflow-y:visible} 201} 202</style> 203</head> 204<body> 205 206<nav class="navbar"> 207 <div class="navbar-logo"> 208 <img src="/static/pal_logo.png" alt="PAL Network" style="height:50px;width:auto"/> 209 </div> 210 <div class="navbar-divider"></div> 211 <div class="navbar-survey"> 212 <div class="navbar-survey-title">ICAN · ICAR Learning Outcomes Dashboard</div> 213 <div class="navbar-survey-sub">2025 Survey · 11 Countries · ~89,000 Children</div> 214 </div> 215 <span class="navbar-tag">2025</span> 216 <div style="display:flex;gap:4px;margin-left:8px"> 217 <button class="lang-btn active" onclick="applyLanguage('en')">EN</button> 218 <button class="lang-btn" onclick="applyLanguage('fr')">FR</button> 219 <button class="lang-btn" onclick="applyLanguage('es')">ES</button> 220 <button class="lang-btn" onclick="applyLanguage('pt')">PT</button> 221 </div> 222<button class="guide-btn" onclick="openDemo()" title="Watch a demo">â¶ Watch a Demo</button> 223<button class="guide-btn" onclick="openGuide()" title="User Guide">ð User Guide</button> 224</nav> 225 226<!-- ââ User Guide Modal ââââââââââââââââââââââââââââââââââââââââ --> 227<div id="guide-modal" class="guide-overlay" onclick="closeGuideOnOverlay(event)"> 228 <div class="guide-modal-box"> 229 <div class="guide-modal-hdr"> 230 <span style="font-weight:700;font-size:14px;color:#014663">ð Dashboard User Guide</span> 231 <div style="display:flex;gap:8px;align-items:center"> 232 <a href="/static/user-guide.pdf" download="ICAN-ICAR_Dashboard_User_Guide.pdf" class="guide-download-btn">⬠Download PDF</a> 233 <button class="guide-close-btn" onclick="closeGuide()" title="Close">â</button> 234 </div> 235 </div> 236 <iframe id="guide-iframe" src="" class="guide-iframe" title="User Guide"></iframe> 237 </div> 238</div> 239 240<div id="demo-modal" class="guide-overlay" onclick="closeDemoOnOverlay(event)"> 241 <div class="guide-modal-box" style="height:auto;max-height:90vh"> 242 <div class="guide-modal-hdr"> 243 <span style="font-weight:700;font-size:14px;color:#014663">â¶ Dashboard Demo</span> 244 <button class="guide-close-btn" onclick="closeDemo()" title="Close">â</button> 245 </div> 246 <div style="padding:16px;background:#000;border-radius:0 0 12px 12px"> 247 <video id="demo-video" controls style="width:100%;border-radius:6px;display:block;max-height:75vh"> 248 <source src="/static/dashboard_demo_final.mp4" type="video/mp4"> 249 </video> 250 </div> 251 </div> 252</div> 253 254<div class="page-tabs"> 255 <button class="page-tab active" onclick="switchPage('mpl',this)">ð MPL Results</button> 256 <button class="page-tab" onclick="switchPage('access',this)">ð Reach & Access</button> 257</div> 258 259<div class="loading" id="loading"><div class="spinner"></div><span style="font-size:13px;color:#4A5568">Loadingâ¦</span></div> 260 261<div class="layout"> 262 263<!-- ââ Sidebar ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ --> 264<aside class="sidebar"> 265 266 <details open> 267 <summary>Countries</summary> 268 <div class="filter-body"> 269 <div class="cb-list" id="country-checks"></div> 270 <div class="cb-actions"> 271 <span class="chip" onclick="selectAll('country-checks','country-cb')">All</span> 272 <span class="chip" onclick="clearAll('country-checks','country-cb')">None</span> 273 </div> 274 </div> 275 </details> 276 277 <details> 278 <summary>Gender</summary> 279 <div class="filter-body"> 280 <div class="pill-row" id="gender-pills"> 281 <button class="pill active" data-val="all" onclick="setPill(this,'gender-pills',setGender)">All</button> 282 <button class="pill" data-val="Girls" onclick="setPill(this,'gender-pills',setGender)">Girls</button> 283 <button class="pill" data-val="Boys" onclick="setPill(this,'gender-pills',setGender)">Boys</button> 284 </div> 285 </div> 286 </details> 287 288 <details> 289 <summary>Location</summary> 290 <div class="filter-body"> 291 <div class="pill-row" id="location-pills"> 292 <button class="pill active" data-val="all" onclick="setPill(this,'location-pills',setLocation)">All</button> 293 <button class="pill" data-val="Urban" onclick="setPill(this,'location-pills',setLocation)">Urban</button> 294 <button class="pill" data-val="Rural" onclick="setPill(this,'location-pills',setLocation)">Rural</button> 295 </div> 296 </div> 297 </details> 298 299 <details> 300 <summary>Age</summary> 301 <div class="filter-body"> 302 <div class="cb-list" id="age-checks"></div> 303 <div class="cb-actions">
304 <span class="chip" onclick="selectAll('age-checks','age-cb')">All</span> 305 <span class="chip" onclick="clearAll('age-checks','age-cb')">None</span> 306 </div> 307 </div> 308 </details> 309 310 <details> 311 <summary>Grade</summary> 312 <div class="filter-body"> 313 <div style="font-size:11px;color:#A0AEC0;margin-bottom:4px">Enrolled children only · Pre-Primary to Grade 8</div> 314 <div class="cb-list" id="grade-checks"></div> 315 <div class="cb-actions"> 316 <span class="chip" onclick="selectAll('grade-checks','grade-cb')">All</span> 317 <span class="chip" onclick="clearAll('grade-checks','grade-cb')">None</span> 318 </div> 319 </div> 320 </details> 321 322 <details id="ctx-details"> 323 <summary>Contextual Variable</summary> 324 <div class="filter-body"> 325 <div style="font-size:11px;color:#C05621;margin-bottom:6px">â One at a time only</div> 326 <select class="filter-select" id="ctx-var-select" onchange="onCtxVarChange()"> 327 <option value="">â None â</option> 328 </select> 329 <div id="ctx-values-wrap" style="display:none;margin-top:8px"> 330 <div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.7px;color:#718096;margin-bottom:4px">Filter by:</div> 331 <div class="cb-list" id="ctx-value-checks"></div> 332 </div> 333 </div> 334 </details> 335 336 <div class="btn-row"> 337 <button class="btn btn-primary" onclick="applyFilters()">Apply</button> 338 <button class="btn btn-reset" onclick="resetFilters()">Reset</button> 339 </div> 340 <div class="sidebar-n" id="sidebar-n"></div> 341</aside> 342 343<!-- ââ Main area âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ --> 344<main class="main"> 345 346 <!-- MPL Page --> 347 <div class="page active" id="page-mpl"> 348 349 <div class="controls-row"> 350 <div class="ctrl-group"> 351 <div class="ctrl-label">Break down by</div> 352 <select class="filter-select" style="width:240px" id="breakdown-select" onchange="onBreakdownChange()"> 353 <option value="country">Country</option> 354 <option value="gender">Gender (Girls / Boys)</option> 355 <option value="location">Location (Urban / Rural)</option> 356 <option value="grade">Grade (learning trajectory)</option> 357 <option value="age">Age (age trajectory)</option> 358 <option value="enrollment">Enrollment Status</option> 359 <option value="home_lang">Home Language Matches Assessment</option> 360 <option value="parental_edu">Highest Parental Education</option> 361 <option value="phone">Type of Phone in Home</option> 362 <option value="computer">Access to Computer/Tablet</option> 363 <option value="school_type">School Type</option> 364 </select> 365 </div> 366 <div class="ctrl-group"> 367 <div class="ctrl-label">MPL Indicator</div> 368 <div class="mpl-tabs"> 369 <button class="mpl-tab active" data-mpl="MPL_math" onclick="setMPL(this)">Math</button> 370 <button class="mpl-tab" data-mpl="MPL_lang" onclick="setMPL(this)">Reading</button> 371 <button class="mpl-tab" data-mpl="MPL_both" onclick="setMPL(this)">Both</button> 372 <button class="mpl-tab" data-mpl="ALL" onclick="setMPL(this)">All</button> 373 </div> 374 </div> 375 <div class="ctrl-group"> 376 <div class="ctrl-label">Enable Chart Labels</div> 377 <button id="labels-toggle" class="labels-toggle-btn active" onclick="toggleLabels()">Labels On</button> 378 </div> 379 </div> 380 381 <!-- MPL info note --> 382 <div class="info-note"> 383 <details> 384 <summary>â¹ What is MPL?</summary> 385 <div class="filter-body"> 386 <strong>Minimum Proficiency Level (MPL)</strong> is the global benchmark for foundational learning under SDG 4.1.1(a). It represents the minimum competencies children need for future learning.<br><br> 387 <strong>For Reading:</strong> An MPL-aligned assessment confirms a child can accurately decode and understand simple Grade-2 text. This is measured by at least 20 score points, with a minimum of 10 dedicated to reading comprehension tasks such as recognizing common words and retrieving explicit information from the text.<br><br> 388 <strong>For Mathematics:</strong> The MPL indicates a child has mastered core number sense and computation skills. Alignment requires at least 20 score points linked to Grade-2 content, with 10 points focused on the Number and Operations domain. To meet this threshold, children must demonstrate proficiency in at least three subconstructs: identifying and counting whole numbers, representing numbers in equivalent ways, solving basic operations, or solving simple real-world problems.<br><br> 389 </div> 390 </details> 391 </div> 392 393 <!-- Chart box --> 394 <div class="chart-box" id="main-chart-box"> 395 <div class="chart-header"> 396 <div> 397 <div class="chart-title" id="chart-title">Percentage of children achieving Minimum Proficiency Levels in Math by country</div> 398 <div class="chart-sub" id="chart-sub">Survey-weighted estimates</div> 399 </div> 400 <div style="display:flex;align-items:center;gap:8px"> 401 <div class="warn-note" id="warn-note" style="display:none">â Some cells suppressed (N < 100)</div> 402 <div class="export-btns"> 403 <button class="btn-export" onclick="exportChartPNG()" title="Download chart as PNG">â PNG</button> 404 <button class="btn-export" onclick="exportTableExcel()" title="Download data as Excel">â Excel</button> 405 </div> 406 </div> 407 </div> 408 <!-- Single MPL chart --> 409 <div id="single-chart-wrap" class="chart-wrap">
410 <canvas id="mpl-chart"></canvas> 411 </div> 412 <!-- All-MPL 2-panel (Math + Reading) --> 413 <div id="all-chart-wrap" class="chart-all-grid" style="display:none"> 414 <div class="chart-panel"> 415 <div class="chart-panel-title">Math</div> 416 <div style="position:relative;height:280px"><canvas id="chart-math"></canvas></div> 417 </div> 418 <div class="chart-panel"> 419 <div class="chart-panel-title">Reading</div> 420 <div style="position:relative;height:280px"><canvas id="chart-lang"></canvas></div> 421 </div> 422 </div> 423 </div> 424 425 <!-- Data table --> 426 <div class="table-box"> 427 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px"> 428 <div class="table-title" style="margin-bottom:0" id="table-title">Detailed Results</div> 429 <button class="btn-export" onclick="exportTableExcel()" title="Download table as Excel">â Excel</button> 430 </div> 431 <table> 432 <thead> 433 <tr> 434 <th id="th-group">Country</th> 435 <th>MPL Math</th><th>MPL Reading</th><th>MPL Both</th> 436 <th>N (unweighted)</th> 437 </tr> 438 </thead> 439 <tbody id="results-tbody"> 440 <tr><td colspan="5" style="text-align:center;color:#A0AEC0;padding:24px">Apply filters to see results</td></tr> 441 </tbody> 442 </table> 443 </div> 444 445 </div><!-- end page-mpl --> 446 447 <!-- Access Page --> 448 <div class="page" id="page-access"> 449 450 <div class="access-note"> 451 The following charts show the context of communities we visited that include: children's access to school by gender, location, school type and availability of different assets in the households. 452 </div> 453 454 <div class="controls-row" style="margin-bottom:4px"> 455 <div class="ctrl-group"> 456 <div class="ctrl-label">View indicator</div> 457 <select class="filter-select" style="width:340px" id="access-view-select" onchange="onAccessViewChange()"> 458 <option value="all">All indicators</option> 459 <optgroup label="Children's access to school"> 460 <option value="acc-enrolled">Percentage of children currently enrolled</option> 461 <option value="acc-gender">Enrolled by gender (Girls vs Boys)</option> 462 <option value="acc-location">Enrolled by location (Rural vs Urban)</option> 463 <option value="acc-school">Enrolled by school type</option> 464 </optgroup> 465 <optgroup label="Household access"> 466 <option value="acc-homelang">Home language matches assessment</option> 467 <option value="acc-computer">Households with computer or tablet</option> 468 <option value="acc-phone">Households by phone availability</option> 469 <option value="acc-edu">Parental education level</option> 470 </optgroup> 471 </select> 472 </div> 473 </div> 474 475 <!-- Child Reach section --> 476 <div class="access-section"> 477 <div class="section-hdr">Children's access to school</div> 478 <div class="access-grid"> 479 <div class="access-card"> 480 <div class="access-card-title">Percentage of children currently enrolled <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-enrolled','Enrolled')">â PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-enrolled','Enrolled')">â Excel</button></div></div> 481 <div class="acc-wrap"><canvas id="acc-enrolled"></canvas></div> 482 </div> 483 <div class="access-card"> 484 <div class="access-card-title">Percentage of children currently enrolled by gender (Girls vs Boys) <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-gender','Gender')">â PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-gender','Gender')">â Excel</button></div></div> 485 <div class="acc-wrap"><canvas id="acc-gender"></canvas></div> 486 </div> 487 <div class="access-card"> 488 <div class="access-card-title">Percentage of children currently enrolled by location (Rural vs Urban) <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-location','Location')">â PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-location','Location')">â Excel</button></div></div> 489 <div class="acc-wrap">
489<canvas id="acc-location"></canvas></div> 490 </div> 491 <div class="access-card"> 492 <div class="access-card-title">Percentage of children currently enrolled by school type <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-school','School_Type')">â PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-school','School_Type')">â Excel</button></div></div> 493 <div class="acc-wrap"><canvas id="acc-school"></canvas></div> 494 </div> 495 </div> 496 </div> 497 498 <!-- Household Access section --> 499 <div class="access-section"> 500 <div class="section-hdr">Access to various assets within households</div> 501 <div class="access-grid"> 502 <div class="access-card"> 503 <div class="access-card-title">Percentage of households speaking same language as the assessment <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-homelang','Home_Language')">â PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-homelang','Home_Language')">â Excel</button></div></div> 504 <div class="acc-wrap"><canvas id="acc-homelang"></canvas></div> 505 </div> 506 <div class="access-card"> 507 <div class="access-card-title">Percentage of households with a computer or tablet available <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-computer','Computer_Tablet')">â PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-computer','Computer_Tablet')">â Excel</button></div></div> 508 <div class="acc-wrap"><canvas id="acc-computer"></canvas></div> 509 </div> 510 <div class="access-card"> 511 <div class="access-card-title">Percentage of households by phone availability <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-phone','Phone_Type')">â PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-phone','Phone_Type')">â Excel</button></div></div> 512 <div class="acc-wrap"><canvas id="acc-phone"></canvas></div> 513 </div> 514 <div class="access-card"> 515 <div class="access-card-title">Percentage of households with educated parents by highest level of education attained <div style="display:flex;gap:4px"><button class="btn-export" onclick="exportAccessChart('acc-edu','Parental_Education')">â PNG</button><button class="btn-export" onclick="exportAccessExcel('acc-edu','Parental_Education')">â Excel</button></div></div> 516 <div class="acc-wrap"><canvas id="acc-edu"></canvas></div> 517 </div> 518 </div> 519 </div> 520 521 </div><!-- end page-access --> 522 523</main> 524</div> 525 526<!-- Chatbot --> 527<button class="chat-fab" onclick="toggleChat()" title="Ask about the data">ð¬</button> 528<div class="chat-panel" id="chat-panel"> 529 <div class="chat-hdr"> 530 <div><div class="chat-hdr-title">ð Data Assistant</div> 531 <div class="chat-hdr-sub">Ask about MPL outcomes or reach & access</div></div> 532 <button class="chat-close" onclick="toggleChat()">â</button> 533 </div> 534 <div class="chat-msgs" id="chat-msgs"> 535 <div class="msg assistant"> 536 <div class="msg-bubble">Hi! Ask me about learning outcomes or access indicators â e.g. "What % of children in Kenya achieve MPL in reading?" or "What % of households in Mali have smartphones?" â </div> 537 </div> 538 </div> 539 <div class="chat-examples"> 540 <p>Try asking:</p> 541 <div class="ex-chips"> 542 <span class="chip" onclick="sendEx(this)">MPL reading rate in Kenya?</span> 543 <span class="chip" onclick="sendEx(this)">Girls vs boys math MPL in Tanzania</span> 544 <span class="chip" onclick="sendEx(this)">% enrolled in Mali by location</span> 545 <span class="chip" onclick="sendEx(this)">Smartphone access in Pakistan</span> 546 </div> 547 </div> 548 <div class="chat-input-row"> 549 <textarea class="chat-input" id="chat-input" rows="1" placeholder="Ask a questionâ¦" onkeydown="chatKey(event)"></textarea> 550 <button class="chat-send" id="chat-send" onclick="sendChat()">â¤</button> 551 </div> 552</div> 553
554<script> 555// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 556// PAL Color palette 557// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 558const PAL = { 559 teal: '#014663', 560 green: '#66B255', 561 darkGray: '#61656B', 562 lightGray: '#B4BBC1', 563 orange: '#F37230', 564 lightOrange: '#FFB68D', 565 blue: '#0077B6', 566 lightGreen: '#90BE6D', 567 midGray: '#6C757D', 568 deepOrange: '#F9844A', 569 gold: '#FFC300', 570}; 571// Ordered palette for multi-series charts 572const COLORS = [ 573 PAL.teal, PAL.green, PAL.orange, PAL.blue, 574 PAL.gold, PAL.deepOrange, PAL.lightGreen, PAL.darkGray, 575 PAL.lightOrange, PAL.midGray, PAL.lightGray, 576]; 577 578// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 579// Translations 580// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 581const T = { 582 en: { 583 navTitle: 'ICAN · ICAR Learning Outcomes Dashboard', 584 navSub: '2025 Survey · 11 Countries · ~89,000 Children', 585 tabMpl: 'ð MPL Results', 586 tabAccess: 'ð Reach & Access', 587 loading: 'Loadingâ¦', 588 // Sidebar 589 sCountries: 'Countries', 590 sGender: 'Gender', 591 sLocation: 'Location', 592 sAge: 'Age', 593 sGrade: 'Grade', 594 sCtx: 'Contextual Variable', 595 sCtxWarn: 'â One at a time only', 596 sCtxNone: 'â None â', 597 sCtxFilterBy: 'Filter by:', 598 sGradeNote: 'Enrolled children only · Pre-Primary to Grade 8', 599 sBtnApply: 'Apply', 600 sBtnReset: 'Reset', 601 sPillAll: 'All', 602 sPillGirls: 'Girls', 603 sPillBoys: 'Boys', 604 sPillUrban: 'Urban', 605 sPillRural: 'Rural', 606 sChipAll: 'All', 607 sChipNone: 'None', 608 // MPL controls 609 ctrlBreakdown: 'Break down by', 610 ctrlMpl: 'MPL Indicator', 611 bdCountry: 'Country', 612 bdGender: 'Gender (Girls / Boys)', 613 bdLocation: 'Location (Urban / Rural)', 614 bdGrade: 'Grade (learning trajectory)', 615 bdAge: 'Age (age trajectory)', 616 bdEnrollment: 'Enrollment Status', 617 bdHomeLang: 'Home Language Matches Assessment', 618 bdParentalEdu: 'Highest Parental Education', 619 bdPhone: 'Type of Phone in Home', 620 bdComputer: 'Access to Computer/Tablet', 621 bdSchoolType: 'School Type', 622 mplMath: 'Math', 623 mplReading: 'Reading', 624 mplBoth: 'Both', 625 mplAll: 'All', 626 // MPL info 627 mplInfoSummary: 'â¹ What is MPL?', 628 mplInfoBody: '<strong>Minimum Proficiency Level (MPL)</strong> is the global benchmark for foundational learning under SDG 4.1.1(a). It represents the minimum competencies children need for future learning.<br><br><strong>For Reading:</strong> An MPL-aligned assessment confirms a child can accurately decode and understand simple Grade-2 text. This is measured by at least 20 score points, with a minimum of 10 dedicated to reading comprehension tasks such as recognizing common words and retrieving explicit information from the text.<br><br><strong>For Mathematics:</strong> The MPL indicates a child has mastered core number sense and computation skills. Alignment requires at least 20 score points linked to Grade-2 content, with 10 points focused on the Number and Operations domain. To meet this threshold, children must demonstrate proficiency in at least three subconstructs: identifying and counting whole numbers, representing numbers in equivalent ways, solving basic operations, or solving simple real-world problems.', 629 // Chart 630 warnSuppressed: 'â Some cells suppressed (N < 100)', 631 chartSub: 'Survey-weighted estimates', 632 tableTitle: 'Detailed Results', 633 thMplMath: 'MPL Math', 634 thMplReading: 'MPL Reading', 635 thMplBoth: 'MPL Both',
636 thN: 'N (unweighted)', 637 noData: 'Apply filters to see results', 638 // Export buttons 639 exportPng: 'â PNG', 640 exportExcel: 'â Excel', 641 // Access tab 642 accessNote: "The following charts show the context of communities we visited that include: children's access to school by gender, location, school type and availability of different assets in the households.", 643 accessViewLabel:'View indicator', 644 accessViewAll: 'All indicators', 645 accGrpSchool: "Children's access to school", 646 accGrpHH: 'Access to various assets within households', 647 accOptEnrolled: 'Percentage of children currently enrolled', 648 accOptGender: 'Enrolled by gender (Girls vs Boys)', 649 accOptLocation: 'Enrolled by location (Rural vs Urban)', 650 accOptSchool: 'Enrolled by school type', 651 accOptHomelang: 'Home language matches assessment', 652 accOptComputer: 'Households with computer or tablet', 653 accOptPhone: 'Households by phone availability', 654 accOptEdu: 'Parental education level', 655 hdrSchool: "Children's access to school", 656 hdrHH: 'Access to various assets within households', 657 cardEnrolled: 'Percentage of children currently enrolled', 658 cardGender: 'Percentage of children currently enrolled by gender (Girls vs Boys)', 659 cardLocation: 'Percentage of children currently enrolled by location (Rural vs Urban)', 660 cardSchool: 'Percentage of children currently enrolled by school type', 661 cardHomelang: 'Percentage of households speaking same language as the assessment', 662 cardComputer: 'Percentage of households with a computer or tablet available', 663 cardPhone: 'Percentage of households by phone availability', 664 cardEdu: 'Percentage of households with educated parents by highest level of education attained', 665 // Chatbot 666 chatTitle: 'ð Data Assistant', 667 chatSub: 'Ask about MPL outcomes or reach & access', 668 chatGreeting: 'Hi! Ask me about learning outcomes or access indicators â e.g. "What % of children in Kenya achieve MPL in reading?" or "What % of households in Mali have smartphones?" â \n\nTip: Type your question in French, Spanish or Portuguese to get a response in that language.', 669 chatTryAsking: 'Try asking:', 670 chatPlaceholder:'Ask a questionâ¦', 671 chatEx1: 'MPL reading rate in Kenya?', 672 chatEx2: 'Girls vs boys math MPL in Tanzania', 673 chatEx3: '% enrolled in Mali by location', 674 chatEx4: 'Smartphone access in Pakistan', 675 mplAxisLabel: '% of children achieving MPL', 676 catGirls:'Girls', catBoys:'Boys', 677 catUrban:'Urban', catRural:'Rural', 678 catGovt:'Government', catPrivate:'Private', catOther:'Other', 679 catNoPhone:'No Phone', catBasicPhone:'Basic Phone', catSmartphone:'Smartphone', 680 catNone:'None', catPrimary:'Primary', catSecondary:'Secondary', catTertiary:'Tertiary', catUniversity:'University', 681 catEnrolled:'% Enrolled', catHomeLang:'% Home Language Match', catComputer:'% With Computer/Tablet', 682 countries: {Bangladesh:'Bangladesh',Kenya:'Kenya',Mali:'Mali',Mexico:'Mexico',Mozambique:'Mozambique',Nepal:'Nepal',Nicaragua:'Nicaragua',Pakistan:'Pakistan',Senegal:'Senegal',Tanzania:'Tanzania',Uganda:'Uganda'}, 683 }, 684 fr: { 685 navTitle: 'Tableau de bord des résultats d\'apprentissage ICAN · ICAR', 686 navSub: 'Enquête 2025 · 11 pays · ~89 000 enfants', 687 tabMpl: 'ð Résultats CPM', 688 tabAccess: 'ð Portée & Accès', 689 loading: 'Chargementâ¦', 690 sCountries: 'Pays', 691 sGender: 'Genre', 692 sLocation: 'Localisation', 693 sAge: 'Ãge', 694 sGrade: 'Classe', 695 sCtx: 'Variable contextuelle', 696 sCtxWarn: 'â Une seule à la fois', 697 sCtxNone: 'â Aucune â', 698 sCtxFilterBy: 'Filtrer par :', 699 sGradeNote: 'Enfants scolarisés uniquement · Pré-primaire à Classe 8', 700 sBtnApply: 'Appliquer', 701 sBtnReset: 'Réinitialiser', 702 sPillAll: 'Tous', 703 sPillGirls: 'Filles', 704 sPillBoys: 'Garçons', 705 sPillUrban: 'Urbain', 706 sPillRural: 'Rural', 707 sChipAll: 'Tous', 708 sChipNone: 'Aucun', 709 ctrlBreakdown: 'Ventiler par', 710 ctrlMpl: 'Indicateur CPM', 711 bdCountry: 'Pays', 712 bdGender: 'Genre (Filles / Garçons)', 713 bdLocation: 'Localisation (Urbain / Rural)', 714 bdGrade: 'Classe (trajectoire d\'apprentissage)', 715 bdAge: 'Ãge (trajectoire par âge)', 716 bdEnrollment: 'Statut de scolarisation', 717 bdHomeLang: 'Langue du foyer correspond à l\'évaluation', 718 bdParentalEdu: 'Niveau d\'éducation parental le plus élevé', 719 bdPhone: 'Type de téléphone au foyer', 720 bdComputer: 'Accès à un ordinateur/tablette', 721 bdSchoolType: 'Type d\'école', 722 mplMath: 'Maths', 723 mplReading: 'Lecture', 724 mplBoth: 'Les deux', 725 mplAll: 'Tout', 726 mplInfoSummary: 'â¹ Qu\'est-ce que le CPM ?', 727 mplInfoBody: '<strong>Niveau Minimum de Compétences (NMC)</strong> est la référence mondiale pour l\'apprentissage fondamental dans le cadre de l\'ODD 4.1.1(a). Il représente les compétences minimales dont les enfants ont besoin pour leur apprentissage futur.<br><br><strong>Pour la lecture :</strong> Une évaluation alignée sur le NMC confirme qu\'un enfant peut décoder et comprendre avec précision un texte simple de niveau CE1. Cela est mesuré par au moins 20 points de score, avec un minimum de 10 consacrés à des tâches de compréhension de lecture telles que la reconnaissance de mots courants et l\'extraction d\'informations explicites du texte.<br><br><strong>Pour les mathématiques :</strong> Le NMC indique qu\'un enfant a maîtrisé les compétences fondamentales en numération et en calcul. L\'alignement requiert au moins 20 points de score liés au contenu de CE1, avec 10 points axés sur le domaine des Nombres et Opérations. Pour atteindre ce seuil, les enfants doivent démontrer leur compétence dans au moins trois sous-construits : identifier et compter des nombres entiers, représenter des nombres de manières équivalentes, résoudre des opérations de base, ou résoudre des problèmes simples du quotidien.', 728 warnSuppressed: 'â Certaines cellules supprimées (N < 100)', 729 chartSub: 'Estimations pondérées par sondage', 730 tableTitle: 'Résultats détaillés', 731 thMplMath: 'CPM Maths', 732 thMplReading: 'CPM Lecture', 733 thMplBoth: 'CPM Les deux',
734 thN: 'N (non pondéré)', 735 noData: 'Appliquer les filtres pour voir les résultats', 736 exportPng: 'â PNG', 737 exportExcel: 'â Excel', 738 accessNote: 'Les graphiques suivants présentent le contexte des communautés visitées : accès des enfants à l\'école par genre, localisation, type d\'école et disponibilité des ressources dans les ménages.', 739 accessViewLabel:'Voir l\'indicateur', 740 accessViewAll: 'Tous les indicateurs', 741 accGrpSchool: 'Accès des enfants à l\'école', 742 accGrpHH: 'Accès aux ressources dans les ménages', 743 accOptEnrolled: 'Pourcentage d\'enfants actuellement scolarisés', 744 accOptGender: 'Scolarisés par genre (Filles vs Garçons)', 745 accOptLocation: 'Scolarisés par localisation (Rural vs Urbain)', 746 accOptSchool: 'Scolarisés par type d\'école', 747 accOptHomelang: 'Langue du foyer correspond à l\'évaluation', 748 accOptComputer: 'Ménages avec ordinateur ou tablette', 749 accOptPhone: 'Ménages par disponibilité du téléphone', 750 accOptEdu: 'Niveau d\'éducation parental', 751 hdrSchool: 'Accès des enfants à l\'école', 752 hdrHH: 'Accès aux ressources dans les ménages', 753 cardEnrolled: 'Pourcentage d\'enfants actuellement scolarisés', 754 cardGender: 'Pourcentage d\'enfants scolarisés par genre (Filles vs Garçons)', 755 cardLocation: 'Pourcentage d\'enfants scolarisés par localisation (Rural vs Urbain)', 756 cardSchool: 'Pourcentage d\'enfants scolarisés par type d\'école', 757 cardHomelang: 'Pourcentage de ménages parlant la même langue que l\'évaluation', 758 cardComputer: 'Pourcentage de ménages disposant d\'un ordinateur ou d\'une tablette', 759 cardPhone: 'Pourcentage de ménages par disponibilité du téléphone', 760 cardEdu: 'Pourcentage de ménages avec parents éduqués par niveau d\'éducation le plus élevé', 761 chatTitle: 'ð Assistant de données', 762 chatSub: 'Posez des questions sur les résultats CPM ou l\'accès', 763 chatGreeting: 'Bonjour ! Posez-moi des questions sur les résultats d\'apprentissage ou les indicateurs d\'accès â \n\nAstuce : Posez votre question dans votre langue pour obtenir une réponse dans cette langue.', 764 chatTryAsking: 'Essayez de demander :', 765 chatPlaceholder:'Posez une questionâ¦', 766 chatEx1: 'Taux CPM lecture au Kenya ?', 767 chatEx2: 'CPM maths filles vs garçons en Tanzanie', 768 chatEx3: '% scolarisés au Mali par localisation', 769 chatEx4: 'Accès aux smartphones au Pakistan', 770 mplAxisLabel: '% d\'enfants atteignant le CPM', 771 catGirls:'Filles', catBoys:'Garçons', 772 catUrban:'Urbain', catRural:'Rural', 773 catGovt:'Public', catPrivate:'Privé', catOther:'Autre', 774 catNoPhone:'Sans téléphone', catBasicPhone:'Téléphone basique', catSmartphone:'Smartphone', 775 catNone:'Aucun', catPrimary:'Primaire', catSecondary:'Secondaire', catTertiary:'Tertiaire', catUniversity:'Université', 776 catEnrolled:'% Scolarisés', catHomeLang:'% Langue du foyer', catComputer:'% Avec ordinateur/tablette', 777 countries: {Bangladesh:'Bangladesh',Kenya:'Kenya',Mali:'Mali',Mexico:'Mexique',Mozambique:'Mozambique',Nepal:'Népal',Nicaragua:'Nicaragua',Pakistan:'Pakistan',Senegal:'Sénégal',Tanzania:'Tanzanie',Uganda:'Ouganda'}, 778 }, 779 es: { 780 navTitle: 'Tablero de resultados de aprendizaje ICAN · ICAR', 781 navSub: 'Encuesta 2025 · 11 paÃses · ~89.000 niños', 782 tabMpl: 'ð Resultados NMP', 783 tabAccess: 'ð Alcance y Acceso', 784 loading: 'Cargandoâ¦', 785 sCountries: 'PaÃses', 786 sGender: 'Género', 787 sLocation: 'Ubicación', 788 sAge: 'Edad', 789 sGrade: 'Grado', 790 sCtx: 'Variable contextual', 791 sCtxWarn: 'â Solo una a la vez', 792 sCtxNone: 'â Ninguna â', 793 sCtxFilterBy: 'Filtrar por:', 794 sGradeNote: 'Solo niños matriculados · Pre-primaria a Grado 8', 795 sBtnApply: 'Aplicar', 796 sBtnReset: 'Restablecer', 797 sPillAll: 'Todos', 798 sPillGirls: 'Niñas', 799 sPillBoys: 'Niños', 800 sPillUrban: 'Urbano', 801 sPillRural: 'Rural', 802 sChipAll: 'Todos', 803 sChipNone: 'Ninguno', 804 ctrlBreakdown: 'Desglosar por', 805 ctrlMpl: 'Indicador NMP', 806 bdCountry: 'PaÃs', 807 bdGender: 'Género (Niñas / Niños)', 808 bdLocation: 'Ubicación (Urbano / Rural)', 809 bdGrade: 'Grado (trayectoria de aprendizaje)',
810 bdAge: 'Edad (trayectoria por edad)', 811 bdEnrollment: 'Estado de matriculación', 812 bdHomeLang: 'Idioma del hogar coincide con la evaluación', 813 bdParentalEdu: 'Nivel educativo parental más alto', 814 bdPhone: 'Tipo de teléfono en el hogar', 815 bdComputer: 'Acceso a computadora/tableta', 816 bdSchoolType: 'Tipo de escuela', 817 mplMath: 'Matemáticas', 818 mplReading: 'Lectura', 819 mplBoth: 'Ambos', 820 mplAll: 'Todos', 821 mplInfoSummary: '⹠¿Qué es el NMP?', 822 mplInfoBody: '<strong>Nivel MÃnimo de Competencias (NMC)</strong> es el referente mundial para el aprendizaje fundamental en el marco del ODS 4.1.1(a). Representa las competencias mÃnimas que los niños necesitan para su aprendizaje futuro.<br><br><strong>Para Lectura:</strong> Una evaluación alineada con el NMC confirma que un niño puede decodificar y comprender con precisión un texto simple de 2.° grado. Esto se mide con al menos 20 puntos de puntaje, con un mÃnimo de 10 dedicados a tareas de comprensión lectora como reconocer palabras comunes y recuperar información explÃcita del texto.<br><br><strong>Para Matemáticas:</strong> El NMC indica que un niño ha dominado las habilidades básicas de sentido numérico y cálculo. La alineación requiere al menos 20 puntos de puntaje vinculados al contenido de 2.° grado, con 10 puntos enfocados en el dominio de Números y Operaciones. Para alcanzar este umbral, los niños deben demostrar competencia en al menos tres subconstructos: identificar y contar números enteros, representar números de formas equivalentes, resolver operaciones básicas o resolver problemas simples del mundo real.', 823 warnSuppressed: 'â Algunas celdas suprimidas (N < 100)', 824 chartSub: 'Estimaciones ponderadas por muestreo', 825 tableTitle: 'Resultados detallados', 826 thMplMath: 'NMP Matemáticas', 827 thMplReading: 'NMP Lectura', 828 thMplBoth: 'NMP Ambos', 829 thN: 'N (no ponderado)', 830 noData: 'Aplique filtros para ver los resultados', 831 exportPng: 'â PNG', 832 exportExcel: 'â Excel', 833 accessNote: 'Los siguientes gráficos muestran el contexto de las comunidades visitadas: acceso de los niños a la escuela por género, ubicación, tipo de escuela y disponibilidad de recursos en los hogares.', 834 accessViewLabel:'Ver indicador', 835 accessViewAll: 'Todos los indicadores', 836 accGrpSchool: 'Acceso de los niños a la escuela', 837 accGrpHH: 'Acceso a recursos en los hogares', 838 accOptEnrolled: 'Porcentaje de niños actualmente matriculados', 839 accOptGender: 'Matriculados por género (Niñas vs Niños)', 840 accOptLocation: 'Matriculados por ubicación (Rural vs Urbano)', 841 accOptSchool: 'Matriculados por tipo de escuela', 842 accOptHomelang: 'Idioma del hogar coincide con la evaluación', 843 accOptComputer: 'Hogares con computadora o tableta', 844 accOptPhone: 'Hogares por disponibilidad de teléfono', 845 accOptEdu: 'Nivel educativo parental', 846 hdrSchool: 'Acceso de los niños a la escuela', 847 hdrHH: 'Acceso a recursos en los hogares', 848 cardEnrolled: 'Porcentaje de niños actualmente matriculados', 849 cardGender: 'Porcentaje de niños matriculados por género (Niñas vs Niños)', 850 cardLocation: 'Porcentaje de niños matriculados por ubicación (Rural vs Urbano)', 851 cardSchool: 'Porcentaje de niños matriculados por tipo de escuela', 852 cardHomelang: 'Porcentaje de hogares que hablan el mismo idioma que la evaluación', 853 cardComputer: 'Porcentaje de hogares con computadora o tableta disponible', 854 cardPhone: 'Porcentaje de hogares por disponibilidad de teléfono', 855 cardEdu: 'Porcentaje de hogares con padres educados por nivel más alto alcanzado', 856 chatTitle: 'ð Asistente de datos', 857 chatSub: 'Pregunte sobre resultados NMP o acceso', 858 chatGreeting: '¡Hola! Pregúnteme sobre resultados de aprendizaje o indicadores de acceso â \n\nConsejo: Escriba su pregunta en su idioma para recibir una respuesta en ese idioma.', 859 chatTryAsking: 'Intente preguntar:', 860 chatPlaceholder:'Haga una preguntaâ¦', 861 chatEx1: '¿Tasa NMP lectura en Kenia?',
862 chatEx2: 'NMP matemáticas niñas vs niños en Tanzania', 863 chatEx3: '% matriculados en Malà por ubicación', 864 chatEx4: 'Acceso a smartphones en Pakistán', 865 mplAxisLabel: '% de niños que alcanzan el NMP', 866 catGirls:'Niñas', catBoys:'Niños', 867 catUrban:'Urbano', catRural:'Rural', 868 catGovt:'Estatal', catPrivate:'Privado', catOther:'Otro', 869 catNoPhone:'Sin teléfono', catBasicPhone:'Teléfono básico', catSmartphone:'Smartphone', 870 catNone:'Ninguno', catPrimary:'Primaria', catSecondary:'Secundaria', catTertiary:'Terciaria', catUniversity:'Universidad', 871 catEnrolled:'% Matriculados', catHomeLang:'% Lengua del hogar', catComputer:'% Con computadora/tableta', 872 countries: {Bangladesh:'Bangladés',Kenya:'Kenia',Mali:'MalÃ',Mexico:'México',Mozambique:'Mozambique',Nepal:'Nepal',Nicaragua:'Nicaragua',Pakistan:'Pakistán',Senegal:'Senegal',Tanzania:'Tanzania',Uganda:'Uganda'}, 873 }, 874 pt: { 875 navTitle: 'Painel de Resultados de Aprendizagem ICAN · ICAR', 876 navSub: 'Pesquisa 2025 · 11 paÃses · ~89.000 crianças', 877 tabMpl: 'ð Resultados NMP', 878 tabAccess: 'ð Alcance e Acesso', 879 loading: 'Carregandoâ¦', 880 sCountries: 'PaÃses', 881 sGender: 'Género', 882 sLocation: 'Localização', 883 sAge: 'Idade', 884 sGrade: 'Série', 885 sCtx: 'Variável contextual', 886 sCtxWarn: 'â Apenas uma de cada vez', 887 sCtxNone: 'â Nenhuma â', 888 sCtxFilterBy: 'Filtrar por:', 889 sGradeNote: 'Apenas crianças matriculadas · Pré-primário a Série 8', 890 sBtnApply: 'Aplicar', 891 sBtnReset: 'Redefinir', 892 sPillAll: 'Todos', 893 sPillGirls: 'Meninas', 894 sPillBoys: 'Meninos', 895 sPillUrban: 'Urbano', 896 sPillRural: 'Rural', 897 sChipAll: 'Todos', 898 sChipNone: 'Nenhum', 899 ctrlBreakdown: 'Desagregar por', 900 ctrlMpl: 'Indicador NMP', 901 bdCountry: 'PaÃs', 902 bdGender: 'Género (Meninas / Meninos)', 903 bdLocation: 'Localização (Urbano / Rural)', 904 bdGrade: 'Série (trajetória de aprendizagem)', 905 bdAge: 'Idade (trajetória por idade)', 906 bdEnrollment: 'Status de matrÃcula', 907 bdHomeLang: 'LÃngua do lar corresponde à avaliação', 908 bdParentalEdu: 'NÃvel educacional parental mais alto', 909 bdPhone: 'Tipo de telefone no lar', 910 bdComputer: 'Acesso a computador/tablet', 911 bdSchoolType: 'Tipo de escola', 912 mplMath: 'Matemática', 913 mplReading: 'Leitura', 914 mplBoth: 'Ambos', 915 mplAll: 'Todos', 916 mplInfoSummary: 'â¹ O que é o NMP?', 917 mplInfoBody: '<strong>NÃvel MÃnimo de Proficiência (NMP)</strong> é o referencial global para a aprendizagem fundamental no âmbito do ODS 4.1.1(a). Representa as competências mÃnimas de que as crianças necessitam para a sua aprendizagem futura.<br><br><strong>Para Leitura:</strong> Uma avaliação alinhada com o NMP confirma que uma criança consegue descodificar e compreender com precisão um texto simples do 2.° ano. Isto é medido por pelo menos 20 pontos de pontuação, com um mÃnimo de 10 dedicados a tarefas de compreensão de leitura, como reconhecer palavras comuns e recuperar informação explÃcita do texto.<br><br><strong>Para Matemática:</strong> O NMP indica que uma criança dominou as competências básicas de sentido numérico e cálculo. O alinhamento requer pelo menos 20 pontos de pontuação ligados ao conteúdo do 2.° ano, com 10 pontos focados no domÃnio de Números e Operações. Para atingir este limiar, as crianças devem demonstrar proficiência em pelo menos três subconstrutos: identificar e contar números inteiros, representar números de formas equivalentes, resolver operações básicas ou resolver problemas simples do mundo real.', 918 warnSuppressed: 'â Algumas células suprimidas (N < 100)', 919 chartSub: 'Estimativas ponderadas por amostragem', 920 tableTitle: 'Resultados detalhados', 921 thMplMath: 'NMP Matemática', 922 thMplReading: 'NMP Leitura', 923 thMplBoth: 'NMP Ambos',
924 thN: 'N (não ponderado)', 925 noData: 'Aplique filtros para ver os resultados', 926 exportPng: 'â PNG', 927 exportExcel: 'â Excel', 928 accessNote: 'Os gráficos a seguir mostram o contexto das comunidades visitadas: acesso das crianças à escola por género, localização, tipo de escola e disponibilidade de recursos nos lares.', 929 accessViewLabel:'Ver indicador', 930 accessViewAll: 'Todos os indicadores', 931 accGrpSchool: 'Acesso das crianças à escola', 932 accGrpHH: 'Acesso a recursos nos lares', 933 accOptEnrolled: 'Percentagem de crianças actualmente matriculadas', 934 accOptGender: 'Matriculadas por género (Meninas vs Meninos)', 935 accOptLocation: 'Matriculadas por localização (Rural vs Urbano)', 936 accOptSchool: 'Matriculadas por tipo de escola', 937 accOptHomelang: 'LÃngua do lar corresponde à avaliação', 938 accOptComputer: 'Lares com computador ou tablet', 939 accOptPhone: 'Lares por disponibilidade de telefone', 940 accOptEdu: 'NÃvel educacional parental', 941 hdrSchool: 'Acesso das crianças à escola', 942 hdrHH: 'Acesso a recursos nos lares', 943 cardEnrolled: 'Percentagem de crianças actualmente matriculadas', 944 cardGender: 'Percentagem de crianças matriculadas por género (Meninas vs Meninos)', 945 cardLocation: 'Percentagem de crianças matriculadas por localização (Rural vs Urbano)', 946 cardSchool: 'Percentagem de crianças matriculadas por tipo de escola', 947 cardHomelang: 'Percentagem de lares que falam a mesma lÃngua da avaliação', 948 cardComputer: 'Percentagem de lares com computador ou tablet disponÃvel', 949 cardPhone: 'Percentagem de lares por disponibilidade de telefone', 950 cardEdu: 'Percentagem de lares com pais educados pelo nÃvel mais alto de educação atingido', 951 chatTitle: 'ð Assistente de dados', 952 chatSub: 'Pergunte sobre resultados NMP ou acesso', 953 chatGreeting: 'Olá! Faça-me perguntas sobre resultados de aprendizagem ou indicadores de acesso â \n\nDica: Escreva a sua pergunta no seu idioma para receber uma resposta nesse idioma.', 954 chatTryAsking: 'Tente perguntar:', 955 chatPlaceholder:'Faça uma perguntaâ¦', 956 chatEx1: 'Taxa NMP leitura no Quénia?', 957 chatEx2: 'NMP matemática meninas vs meninos na Tanzânia', 958 chatEx3: '% matriculados no Mali por localização', 959 chatEx4: 'Acesso a smartphones no Paquistão', 960 mplAxisLabel: '% de crianças que atingem o NMP', 961 catGirls:'Meninas', catBoys:'Meninos', 962 catUrban:'Urbano', catRural:'Rural', 963 catGovt:'Público', catPrivate:'Privado', catOther:'Outro', 964 catNoPhone:'Sem telefone', catBasicPhone:'Telefone básico', catSmartphone:'Smartphone', 965 catNone:'Nenhum', catPrimary:'Primário', catSecondary:'Secundário', catTertiary:'Terciário', catUniversity:'Universidade', 966 catEnrolled:'% Matriculadas', catHomeLang:'% LÃngua do lar', catComputer:'% Com computador/tablet', 967 countries: {Bangladesh:'Bangladesh',Kenya:'Quénia',Mali:'Mali',Mexico:'México',Mozambique:'Moçambique',Nepal:'Nepal',Nicaragua:'Nicarágua',Pakistan:'Paquistão',Senegal:'Senegal',Tanzania:'Tanzânia',Uganda:'Uganda'}, 968 }, 969}; 970 971let currentLang = 'en'; 972 973// Translate a country name (English key â localised display name) 974function tc(name) { 975 return (T[currentLang].countries || {})[name] || name; 976} 977// Translate a label/category key 978function tl(key) { return T[currentLang][key] || T.en[key] || key; } 979 980function applyLanguage(lang) { 981 currentLang = lang; 982 const t = T[lang]; 983 984 // Navbar buttons 985 document.querySelectorAll('.lang-btn').forEach(b => b.classList.toggle('active', b.textContent === lang.toUpperCase())); 986 987 // Navbar text 988 document.querySelector('.navbar-survey-title').textContent = t.navTitle; 989 document.querySelector('.navbar-survey-sub').textContent = t.navSub; 990 991 // Page tabs 992 const tabs = document.querySelectorAll('.page-tab'); 993 tabs[0].textContent = t.tabMpl; 994 tabs[1].innerHTML = t.tabAccess; 995 996 // Loading 997 document.querySelector('#loading span').textContent = t.loading; 998 999 // Sidebar summaries 1000 const summaries = document.querySelectorAll('aside.sidebar details summary'); 1001 summaries[0].childNodes[0].textContent = t.sCountries; 1002 summaries[1].childNodes[0].textContent = t.sGender; 1003 summaries[2].childNodes[0].textContent = t.sLocation; 1004 summaries[3].childNodes[0].textContent = t.sAge; 1005 summaries[4].childNodes[0].textContent = t.sGrade; 1006 summaries[5].childNodes[0].textContent = t.sCtx; 1007 1008 // Sidebar pills 1009 const gPills = document.querySelectorAll('#gender-pills .pill'); 1010 gPills[0].textContent = t.sPillAll; 1011 gPills[1].textContent = t.sPillGirls; 1012 gPills[2].textContent = t.sPillBoys; 1013 const lPills = document.querySelectorAll('#location-pills .pill'); 1014 lPills[0].textContent = t.sPillAll; 1015 lPills[1].textContent = t.sPillUrban; 1016 lPills[2].textContent = t.sPillRural; 1017 1018 // Sidebar chips 1019 document.querySelectorAll('#country-checks + .cb-actions .chip')[0].textContent = t.sChipAll; 1020 document.querySelectorAll('#country-checks + .cb-actions .chip')[1].textContent = t.sChipNone; 1021 document.querySelectorAll('#age-checks + .cb-actions .chip')[0].textContent = t.sChipAll; 1022 document.querySelectorAll('#age-checks + .cb-actions .chip')[1].textContent = t.sChipNone; 1023 document.querySelectorAll('#grade-checks + .cb-actions .chip')[0].textContent = t.sChipAll; 1024 document.querySelectorAll('#grade-checks + .cb-actions .chip')[1].textContent = t.sChipNone; 1025 1026 // Grade note 1027 document.querySelector('#grade-checks').previousElementSibling.textContent = t.sGradeNote; 1028 1029 // Ctx var warning
1030 document.querySelector('#ctx-details .filter-body div').textContent = t.sCtxWarn; 1031 document.querySelector('#ctx-var-select option[value=""]').textContent = t.sCtxNone; 1032 document.querySelector('#ctx-values-wrap > div').textContent = t.sCtxFilterBy; 1033 1034 // Apply / Reset buttons 1035 document.querySelector('.btn-primary').textContent = t.sBtnApply; 1036 document.querySelector('.btn-reset').textContent = t.sBtnReset; 1037 1038 // MPL controls 1039 const ctrlLabels = document.querySelectorAll('#page-mpl .ctrl-label'); 1040 ctrlLabels[0].textContent = t.ctrlBreakdown; 1041 ctrlLabels[1].textContent = t.ctrlMpl; 1042 1043 // Breakdown select options 1044 const bdSel = document.getElementById('breakdown-select'); 1045 const bdMap = ['bdCountry','bdGender','bdLocation','bdGrade','bdAge','bdEnrollment','bdHomeLang','bdParentalEdu','bdPhone','bdComputer','bdSchoolType']; 1046 bdSel.querySelectorAll('option').forEach((o,i) => { if (bdMap[i]) o.textContent = t[bdMap[i]]; }); 1047 1048 // MPL tabs 1049 const mplTabs = document.querySelectorAll('.mpl-tab'); 1050 mplTabs[0].textContent = t.mplMath; 1051 mplTabs[1].textContent = t.mplReading; 1052 mplTabs[2].textContent = t.mplBoth; 1053 mplTabs[3].textContent = t.mplAll; 1054 1055 // MPL info summary + body 1056 document.querySelector('.info-note details summary').textContent = t.mplInfoSummary; 1057 document.querySelector('.info-note details .filter-body').innerHTML = t.mplInfoBody; 1058 1059 // Chart subtitle & warn 1060 document.getElementById('chart-sub').textContent = t.chartSub; 1061 const wn = document.getElementById('warn-note'); 1062 if (wn) wn.textContent = t.warnSuppressed; 1063 1064 // Export buttons in chart box 1065 const exportBtns = document.querySelectorAll('#main-chart-box .btn-export'); 1066 if (exportBtns[0]) exportBtns[0].textContent = t.exportPng; 1067 if (exportBtns[1]) exportBtns[1].textContent = t.exportExcel; 1068 1069 // Table 1070 document.getElementById('table-title').textContent = t.tableTitle; 1071 document.querySelector('#results-tbody tr td')?.setAttribute('data-empty', t.noData); 1072 const ths = document.querySelectorAll('thead th'); 1073 if (ths[1]) ths[1].textContent = t.thMplMath; 1074 if (ths[2]) ths[2].textContent = t.thMplReading; 1075 if (ths[3]) ths[3].textContent = t.thMplBoth; 1076 if (ths[4]) ths[4].textContent = t.thN; 1077 1078 // Table Excel button 1079 const tblExcel = document.querySelector('.table-box .btn-export'); 1080 if (tblExcel) tblExcel.textContent = t.exportExcel; 1081 1082 // Access tab 1083 document.querySelector('.access-note').textContent = t.accessNote; 1084 document.querySelector('#page-access .ctrl-label').textContent = t.accessViewLabel; 1085 1086 // Access view select 1087 const avSel = document.getElementById('access-view-select'); 1088 avSel.options[0].textContent = t.accessViewAll; 1089 const optGroups = avSel.querySelectorAll('optgroup'); 1090 optGroups[0].label = t.accGrpSchool; 1091 optGroups[1].label = t.accGrpHH; 1092 const accOpts = avSel.querySelectorAll('option:not([value="all"])'); 1093 const accOptKeys = ['accOptEnrolled','accOptGender','accOptLocation','accOptSchool','accOptHomelang','accOptComputer','accOptPhone','accOptEdu']; 1094 accOpts.forEach((o,i) => { if (accOptKeys[i]) o.textContent = t[accOptKeys[i]]; }); 1095 1096 // Section headers 1097 const sectionHdrs = document.querySelectorAll('#page-access .section-hdr'); 1098 if (sectionHdrs[0]) sectionHdrs[0].textContent = t.hdrSchool; 1099 if (sectionHdrs[1]) sectionHdrs[1].textContent = t.hdrHH; 1100 1101 // Access card titles (first text node only, preserving export buttons) 1102 const cardKeys = ['cardEnrolled','cardGender','cardLocation','cardSchool','cardHomelang','cardComputer','cardPhone','cardEdu']; 1103 document.querySelectorAll('.access-card-title').forEach((el, i) => { 1104 if (cardKeys[i]) el.childNodes[0].textContent = t[cardKeys[i]] + ' '; 1105 }); 1106 1107 // Access export buttons 1108 document.querySelectorAll('.access-card .btn-export').forEach(b => { 1109 b.textContent = b.onclick?.toString().includes('Excel') ? t.exportExcel : t.exportPng; 1110 }); 1111 1112 // Chatbot 1113 document.querySelector('.chat-hdr-title').textContent = t.chatTitle; 1114 document.querySelector('.chat-hdr-sub').textContent = t.chatSub; 1115 document.querySelector('.chat-examples p').textContent = t.chatTryAsking; 1116 document.getElementById('chat-input').placeholder = t.chatPlaceholder; 1117 const exChips = document.querySelectorAll('.ex-chips .chip'); 1118 if (exChips[0]) exChips[0].textContent = t.chatEx1; 1119 if (exChips[1]) exChips[1].textContent = t.chatEx2; 1120 if (exChips[2]) exChips[2].textContent = t.chatEx3; 1121 if (exChips[3]) exChips[3].textContent = t.chatEx4; 1122 1123 // Update country checkbox labels to translated names (keep value as English) 1124 document.querySelectorAll('#country-checks .cb-item').forEach(label => { 1125 const cb = label.querySelector('input'); 1126 if (cb) label.childNodes[label.childNodes.length - 1].textContent = ' ' + tc(cb.value); 1127 }); 1128 1129 // Re-render charts with translated labels if data is loaded 1130 if (currentMplData.countries) { 1131 updateChartTitle(currentMplData.breakdown_label); 1132 renderMPLCharts(); 1133 } 1134 if (typeof accessData !== 'undefined' && accessData && accessData.countries) { 1135 renderAccessCharts(); 1136 } 1137} 1138 1139// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1140// State 1141// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1142const state = { 1143 breakdown: 'country', mpl: 'MPL_math', 1144 countries: [], gender: 'all', location: 'all', 1145 ages: [], grades: [], ctx_var: '', ctx_vals: [], page: 'mpl', 1146 accessView: 'all', 1147}; 1148let filterOptions = {}; 1149let currentMplData = {}; 1150let chartInstances = {}; 1151let showLabels = true; 1152 1153function toggleLabels() { 1154 showLabels = !showLabels; 1155 const btn = document.getElementById('labels-toggle'); 1156 btn.classList.toggle('active', showLabels); 1157 btn.textContent = showLabels ? 'Labels On' : 'Labels Off'; 1158 if (currentMplData.countries || currentMplData.groups) renderMPLCharts(); 1159 if (accessData) renderAccessCharts(); 1160} 1161const chatHistory = []; 1162 1163const CTXKEYS = new Set(['enrollment','home_lang','parental_edu','phone','computer','school_type']); 1164// Line chart breakdowns (trajectory) 1165const LINE_BREAKDOWNS = new Set(['grade','age']); 1166const MPL_SHORT = {MPL_math:'Math', MPL_lang:'Reading', MPL_both:'Both'}; 1167 1168// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1169// Init 1170// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
1171async function init() { 1172 showLoading(true); 1173 try { 1174 filterOptions = await fetch('/api/filter-options').then(r => r.json()); 1175 buildFilterUI(); 1176 await refresh(); 1177 } catch(e) { 1178 console.error(e); 1179 alert('Could not connect to server. Make sure Flask is running.'); 1180 } finally { showLoading(false); } 1181} 1182 1183function buildFilterUI() { 1184 const cc = document.getElementById('country-checks'); 1185 cc.innerHTML = filterOptions.countries.map(c => 1186 `<label class="cb-item"><input type="checkbox" class="country-cb" value="${c}" checked onchange="onCbChange()"> ${tc(c)}</label>` 1187 ).join(''); 1188 1189 const ac = document.getElementById('age-checks'); 1190 ac.innerHTML = filterOptions.ages.map(a => 1191 `<label class="cb-item"><input type="checkbox" class="age-cb" value="${a}" checked onchange="onCbChange()"> Age ${a}</label>` 1192 ).join(''); 1193 1194 const gc = document.getElementById('grade-checks'); 1195 gc.innerHTML = filterOptions.grades.map(g => 1196 `<label class="cb-item"><input type="checkbox" class="grade-cb" value="${g}" checked onchange="onCbChange()"> ${g}</label>` 1197 ).join(''); 1198 1199 const sel = document.getElementById('ctx-var-select'); 1200 sel.innerHTML = `<option value="">${T[currentLang].sCtxNone}</option>` + 1201 Object.entries(filterOptions.ctx_vars).map(([k,v]) => 1202 `<option value="${k}">${v.label}</option>`).join(''); 1203} 1204 1205// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1206// Filter helpers 1207// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1208function selectAll(listId, cls) { 1209 document.querySelectorAll(`#${listId} .${cls}`).forEach(c => c.checked = true); 1210 onCbChange(); 1211} 1212function clearAll(listId, cls) { 1213 document.querySelectorAll(`#${listId} .${cls}`).forEach(c => c.checked = false); 1214 onCbChange(); 1215} 1216function onCbChange() { /* state updated on applyFilters */ } 1217 1218function setPill(btn, groupId, fn) { 1219 document.querySelectorAll(`#${groupId} .pill`).forEach(b => b.classList.remove('active')); 1220 btn.classList.add('active'); 1221 fn(btn.dataset.val); 1222} 1223function setGender(val) { state.gender = val; } 1224function setLocation(val) { state.location = val; } 1225 1226function onCtxVarChange() { 1227 const v = document.getElementById('ctx-var-select').value; 1228 state.ctx_var = v; 1229 state.ctx_vals = []; 1230 const wrap = document.getElementById('ctx-values-wrap'); 1231 if (!v) { wrap.style.display = 'none'; return; } 1232 if (CTXKEYS.has(state.breakdown)) { 1233 alert('The current breakdown already uses a contextual variable. Switch breakdown to a standard variable first.'); 1234 document.getElementById('ctx-var-select').value = ''; 1235 state.ctx_var = ''; 1236 wrap.style.display = 'none'; 1237 return; 1238 } 1239 wrap.style.display = 'block'; 1240 const opts = filterOptions.ctx_vars[v].options; 1241 document.getElementById('ctx-value-checks').innerHTML = opts.map(o => 1242 `<label class="cb-item"><input type="checkbox" class="ctxval-cb" value="${o}" onchange="onCtxValChange()"> ${o}</label>` 1243 ).join(''); 1244} 1245function onCtxValChange() { 1246 state.ctx_vals = [...document.querySelectorAll('.ctxval-cb:checked')].map(c => c.value); 1247} 1248 1249function onAccessViewChange() { 1250 state.accessView = document.getElementById('access-view-select').value; 1251 renderAccessCharts(); 1252} 1253 1254function onBreakdownChange() { 1255 const bd = document.getElementById('breakdown-select').value; 1256 state.breakdown = bd; 1257 const ctxSel = document.getElementById('ctx-var-select'); 1258 if (CTXKEYS.has(bd)) { 1259 ctxSel.value = ''; state.ctx_var = ''; state.ctx_vals = []; 1260 document.getElementById('ctx-values-wrap').style.display = 'none'; 1261 ctxSel.disabled = true; 1262 } else { 1263 ctxSel.disabled = false; 1264 } 1265 applyFilters(); 1266} 1267 1268function setMPL(btn) { 1269 document.querySelectorAll('.mpl-tab').forEach(b => b.classList.remove('active')); 1270 btn.classList.add('active'); 1271 state.mpl = btn.dataset.mpl; 1272 if (currentMplData.groups || currentMplData.countries) renderMPLCharts(); 1273} 1274 1275function switchPage(page, btn) { 1276 document.querySelectorAll('.page-tab').forEach(b => b.classList.remove('active')); 1277 btn.classList.add('active'); 1278 document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); 1279 document.getElementById(`page-${page}`).classList.add('active'); 1280 state.page = page;
1281 document.getElementById('ctx-details').style.display = page === 'access' ? 'none' : ''; 1282 if (page === 'access') fetchAndRenderAccess(); 1283} 1284 1285// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1286// Payload 1287// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1288function buildPayload() { 1289 const f = {}; 1290 const countries = [...document.querySelectorAll('.country-cb:checked')].map(c => c.value); 1291 if (countries.length < filterOptions.countries.length) f.countries = countries; 1292 if (state.gender !== 'all') f.genders = [state.gender]; 1293 if (state.location !== 'all') f.locations = [state.location]; 1294 const ages = [...document.querySelectorAll('.age-cb:checked')].map(c => parseInt(c.value)); 1295 if (ages.length < filterOptions.ages.length) f.ages = ages; 1296 const grades = [...document.querySelectorAll('.grade-cb:checked')].map(c => c.value); 1297 if (grades.length < filterOptions.grades.length) f.grades = grades; 1298 if (state.ctx_var && state.ctx_vals.length) 1299 f.ctx_filter = { var: state.ctx_var, values: state.ctx_vals }; 1300 return f; 1301} 1302 1303async function applyFilters() { await refresh(); } 1304 1305function resetFilters() { 1306 state.gender = 'all'; state.location = 'all'; 1307 state.ctx_var = ''; state.ctx_vals = []; 1308 document.querySelectorAll('.country-cb,.age-cb,.grade-cb').forEach(c => c.checked = true); 1309 document.querySelectorAll('#gender-pills .pill').forEach(b => b.classList.toggle('active', b.dataset.val==='all')); 1310 document.querySelectorAll('#location-pills .pill').forEach(b => b.classList.toggle('active', b.dataset.val==='all')); 1311 document.getElementById('ctx-var-select').value = ''; 1312 document.getElementById('ctx-values-wrap').style.display = 'none'; 1313 refresh(); 1314} 1315 1316// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1317// Fetch & render 1318// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1319async function refresh() { 1320 showLoading(true); 1321 try { 1322 if (state.page === 'mpl') await fetchAndRenderMPL(); 1323 else await fetchAndRenderAccess(); 1324 } finally { showLoading(false); } 1325} 1326 1327async function fetchAndRenderMPL() { 1328 const filters = buildPayload(); 1329 let data; 1330 if (state.breakdown === 'country') { 1331 data = await fetch('/api/stats', { 1332 method: 'POST', headers: {'Content-Type':'application/json'}, 1333 body: JSON.stringify({filters, breakdown: 'country'}) 1334 }).then(r => r.json()); 1335 currentMplData = { 1336 countries: data.groups, categories: null, 1337 results: Object.fromEntries(data.groups.map(g => [g, {'__single__': data.results[g]}])), 1338 breakdown_label: 'Country', n_total: data.n_total, chartType: 'bar', 1339 }; 1340 } else { 1341 data = await fetch('/api/stats/country-cross', { 1342 method: 'POST', headers: {'Content-Type':'application/json'}, 1343 body: JSON.stringify({filters, secondary: state.breakdown}) 1344 }).then(r => r.json()); 1345 // grade & age â line trajectory; others â dumbbell 1346 const chartType = LINE_BREAKDOWNS.has(state.breakdown) ? 'line' : 'dumbbell'; 1347 currentMplData = {...data, chartType, breakdown_label: data.secondary_label}; 1348 } 1349 document.getElementById('sidebar-n').textContent = 1350 `Filtered total: ${(currentMplData.n_total||0).toLocaleString()} children`; 1351 renderMPLCharts(); 1352 renderTable(); 1353} 1354 1355// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1356// MPL chart rendering 1357// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1358function renderMPLCharts() { 1359 const isAll = state.mpl === 'ALL'; 1360 document.getElementById('single-chart-wrap').style.display = isAll ? 'none' : 'block'; 1361 document.getElementById('all-chart-wrap').style.display = isAll ? 'grid' : 'none'; 1362 1363 const {countries, categories, results, chartType, breakdown_label} = currentMplData; 1364 if (!countries || !countries.length) return; 1365 1366 updateChartTitle(breakdown_label); 1367 1368 let hasWarn = false;
1369 countries.forEach(c => { 1370 const cats = categories || ['__single__']; 1371 cats.forEach(cat => { 1372 ['MPL_math','MPL_lang','MPL_both'].forEach(m => { 1373 if (results[c]?.[cat]?.[m]?.warning) hasWarn = true; 1374 }); 1375 }); 1376 }); 1377 document.getElementById('warn-note').style.display = hasWarn ? '' : 'none'; 1378 1379 if (isAll) { 1380 ['MPL_math','MPL_lang'].forEach(mpl => { 1381 const canvasId = {MPL_math:'chart-math',MPL_lang:'chart-lang'}[mpl]; 1382 renderSingleChart(canvasId, mpl, countries, categories, results, chartType, true); 1383 }); 1384 } else { 1385 renderSingleChart('mpl-chart', state.mpl, countries, categories, results, chartType, false); 1386 } 1387} 1388 1389function renderSingleChart(canvasId, mpl, countries, categories, results, chartType, compact) { 1390 destroyChart(canvasId); 1391 const canvas = document.getElementById(canvasId); 1392 if (!canvas) return; 1393 const ctx = canvas.getContext('2d'); 1394 1395 if (chartType === 'bar') { 1396 const estimates = countries.map(c => results[c]?.['__single__']?.[mpl]?.estimate ?? null); 1397 const warnings = countries.map(c => results[c]?.['__single__']?.[mpl]?.warning ?? false); 1398 const ns = countries.map(c => results[c]?.['__single__']?.[mpl]?.n_unweighted ?? 0); 1399 const barData = estimates.map((v, i) => warnings[i] ? 0 : (v ?? 0)); 1400 const bgColors = estimates.map((_, i) => warnings[i] ? '#FED7AA' : PAL.teal); 1401 1402 chartInstances[canvasId] = new Chart(ctx, { 1403 type: 'bar', 1404 data: {labels: countries.map(tc), datasets: [{label: MPL_SHORT[mpl], data: barData, 1405 backgroundColor: bgColors, borderRadius: 4}]}, 1406 options: { 1407 indexAxis: 'y', responsive: true, maintainAspectRatio: false, 1408 plugins: { 1409 legend: {display: false}, 1410 tooltip: {callbacks: {label: (c) => { 1411 const i = c.dataIndex; 1412 return warnings[i] ? `â Suppressed â N=${ns[i]} (<100)` 1413 : `${estimates[i]}% (N=${ns[i].toLocaleString()})`; 1414 }}} 1415 }, 1416 scales: { 1417 x: {min:0, max:100, title:{display:!compact, text: tl('mplAxisLabel'), font:{size:10}}, 1418 grid:{color:'#EDF2F7'}, ticks:{font:{size:compact?9:11}}}, 1419 y: {grid:{display:false}, ticks:{font:{size:compact?9:11}, color:PAL.teal}} 1420 } 1421 }, 1422 plugins: [suppressPlugin(countries, warnings, true), barLabelPlugin(warnings)] 1423 }); 1424 } 1425 1426 else if (chartType === 'dumbbell') { 1427 const cats = categories || []; 1428 const reversedCountries = [...countries].reverse(); 1429 const ptRadius = compact ? 6 : 8; 1430 const datasets = cats.map((cat, ci) => ({ 1431 label: cat, 1432 data: reversedCountries.map((c, yi) => { 1433 const s = results[c]?.[cat]?.[mpl]; 1434 if (!s || s.warning || s.estimate === null) return null; 1435 return {x: s.estimate, y: yi}; 1436 }).filter(Boolean), 1437 backgroundColor: COLORS[ci % COLORS.length], 1438 pointRadius: ptRadius, pointHoverRadius: ptRadius + 2, showLine: false, 1439 })); 1440 1441 chartInstances[canvasId] = new Chart(ctx, { 1442 type: 'scatter', 1443 data: {datasets}, 1444 options: { 1445 responsive: true, maintainAspectRatio: false, 1446 plugins: { 1447 legend: {display: cats.length > 1, position:'bottom', 1448 labels:{boxWidth:10, font:{size:compact?9:11}, color:PAL.darkGray}}, 1449 tooltip: {callbacks: {label: c => `${c.dataset.label}: ${c.parsed.x}%`}} 1450 }, 1451 scales: { 1452 x: {min:0, max:100, 1453 title:{display:!compact, text: tl('mplAxisLabel'), font:{size:10}}, 1454 grid:{color:'#EDF2F7'}, ticks:{font:{size:compact?9:10}}}, 1455 y: {min: -1, max: reversedCountries.length, 1456 grid:{color:'#EDF2F7'}, 1457 ticks:{stepSize:1, font:{size:compact?9:11}, color:PAL.teal, 1458 callback: v => { const i=Math.round(v); return (i>=0&&i<reversedCountries.length)?tc(reversedCountries[i]||''):null; }}} 1459 } 1460 }, 1461 plugins: [dumbbellPlugin(reversedCountries, cats, results, mpl)] 1462 }); 1463 } 1464 1465 else if (chartType === 'line') { 1466 const cats = categories || []; 1467 const xLabel = state.breakdown === 'age' ? 'Age' : 'Grade'; 1468 const datasets = countries.map((c, ci) => ({ 1469 label: tc(c), 1470 data: cats.map(cat => { 1471 const s = results[c]?.[cat]?.[mpl]; 1472 return (!s || s.warning) ? null : s.estimate; 1473 }), 1474 borderColor: COLORS[ci % COLORS.length], 1475 backgroundColor: COLORS[ci % COLORS.length] + '22', 1476 tension: 0.3, pointRadius: compact ? 4 : 5, borderWidth: compact ? 1.5 : 2, 1477 spanGaps: false, 1478 })); 1479 1480 chartInstances[canvasId] = new Chart(ctx, { 1481 type: 'line', 1482 data: {labels: cats, datasets}, 1483 options: { 1484 responsive: true, maintainAspectRatio: false, 1485 plugins: { 1486 legend: {display: !compact, position:'bottom', 1487 labels:{boxWidth:10, font:{size:compact?9:11}}}, 1488 tooltip: {callbacks: {label: c => `${c.dataset.label}: ${c.parsed.y}%`}} 1489 }, 1490 scales: {
1491 x: {title:{display:!compact, text:xLabel, font:{size:10}}, 1492 grid:{color:'#EDF2F7'}, ticks:{font:{size:compact?9:10}}}, 1493 y: {min:0, max:100, 1494 title:{display:!compact, text: tl('mplAxisLabel'), font:{size:10}}, 1495 grid:{color:'#EDF2F7'}, ticks:{font:{size:compact?9:10}}} 1496 } 1497 }, 1498 plugins: [lineLabelPlugin()] 1499 }); 1500 } 1501} 1502 1503function suppressPlugin(labels, warnings, horizontal) { 1504 return { 1505 id: 'suppress_' + Math.random(), 1506 afterDatasetsDraw(chart) { 1507 const ctx2 = chart.ctx; 1508 ctx2.save(); 1509 ctx2.font = 'bold 10px sans-serif'; 1510 ctx2.fillStyle = '#C05621'; 1511 chart.getDatasetMeta(0).data.forEach((bar, i) => { 1512 if (!warnings[i]) return; 1513 ctx2.fillText('â N<100', horizontal ? bar.x + 4 : bar.x - 10, horizontal ? bar.y + 4 : bar.y - 4); 1514 }); 1515 ctx2.restore(); 1516 } 1517 }; 1518} 1519 1520function barLabelPlugin(warnings) { 1521 return { 1522 id: 'barLabel_' + Math.random(), 1523 afterDatasetsDraw(chart) { 1524 if (!showLabels) return; 1525 const ctx2 = chart.ctx; 1526 ctx2.save(); 1527 ctx2.font = 'bold 10px Arial, sans-serif'; 1528 ctx2.fillStyle = '#2D3748'; 1529 ctx2.textBaseline = 'middle'; 1530 ctx2.textAlign = 'left'; 1531 const meta = chart.getDatasetMeta(0); 1532 meta.data.forEach((bar, i) => { 1533 if (warnings && warnings[i]) return; 1534 const val = chart.data.datasets[0].data[i]; 1535 if (!val) return; 1536 if (val >= 95) { 1537 ctx2.fillStyle = '#fff'; 1538 ctx2.textAlign = 'right'; 1539 ctx2.fillText(`${val}%`, bar.x - 4, bar.y); 1540 ctx2.fillStyle = '#2D3748'; 1541 ctx2.textAlign = 'left'; 1542 } else { 1543 ctx2.fillText(`${val}%`, bar.x + 4, bar.y); 1544 } 1545 }); 1546 ctx2.restore(); 1547 } 1548 }; 1549} 1550 1551function stackedLabelPlugin() { 1552 return { 1553 id: 'stackedLabel_' + Math.random(), 1554 afterDatasetsDraw(chart) { 1555 if (!showLabels) return; 1556 const ctx2 = chart.ctx; 1557 ctx2.save(); 1558 ctx2.font = 'bold 10px Arial, sans-serif'; 1559 ctx2.fillStyle = '#fff'; 1560 ctx2.textBaseline = 'middle'; 1561 ctx2.textAlign = 'center'; 1562 chart.data.datasets.forEach((dataset, dsi) => { 1563 const meta = chart.getDatasetMeta(dsi); 1564 meta.data.forEach((bar, i) => { 1565 const val = dataset.data[i]; 1566 if (!val || val < 4) return; 1567 const segW = bar.x - bar.base; 1568 if (segW < 26) return; 1569 ctx2.fillText(`${Math.round(val)}%`, bar.base + segW / 2, bar.y); 1570 }); 1571 }); 1572 ctx2.restore(); 1573 } 1574 }; 1575} 1576 1577function dumbbellPlugin(countries, cats, results, mpl) { 1578 return { 1579 id: 'dumbbell_' + Math.random(), 1580 afterDatasetsDraw(chart) { 1581 const ctx2 = chart.ctx; 1582 ctx2.save(); 1583 ctx2.strokeStyle = '#CBD5E0'; 1584 ctx2.lineWidth = 1.5; 1585 countries.forEach((c, yi) => { 1586 const xs = []; 1587 cats.forEach((cat, ci) => { 1588 const pt = chart.getDatasetMeta(ci).data.find(p => 1589 p.$context?.raw && Math.abs(p.$context.raw.y - yi) < 0.1 1590 ); 1591 if (pt) xs.push(pt.x); 1592 }); 1593 if (xs.length >= 2) { 1594 const yPx = chart.scales.y.getPixelForValue(yi); 1595 ctx2.beginPath(); 1596 ctx2.moveTo(Math.min(...xs), yPx); 1597 ctx2.lineTo(Math.max(...xs), yPx); 1598 ctx2.stroke(); 1599 } 1600 }); 1601 ctx2.restore(); 1602 }, 1603 afterDraw(chart) { 1604 if (!showLabels) return; 1605 const ctx2 = chart.ctx; 1606 ctx2.save(); 1607 ctx2.font = 'bold 9px Arial, sans-serif'; 1608 ctx2.textBaseline = 'middle'; 1609 const ptRadius = chart.data.datasets[0]?.pointRadius || 6; 1610 const gap = ptRadius + 3; 1611 countries.forEach((c, yi) => { 1612 const dots = []; 1613 cats.forEach((cat, ci) => { 1614 const meta = chart.getDatasetMeta(ci); 1615 const pt = meta.data.find(p => p.$context?.raw && Math.abs(p.$context.raw.y - yi) < 0.1); 1616 if (pt) dots.push({ci, pt, x: pt.x, y: pt.y, val: pt.$context.raw.x}); 1617 }); 1618 if (!dots.length) return; 1619 dots.sort((a, b) => a.x - b.x);
1620 dots.forEach((d, idx) => { 1621 ctx2.fillStyle = chart.data.datasets[d.ci]?.backgroundColor || '#333'; 1622 if (dots.length === 1 || idx === dots.length - 1) { 1623 ctx2.textAlign = 'left'; 1624 ctx2.fillText(`${d.val}%`, d.x + gap, d.y); 1625 } else { 1626 ctx2.textAlign = 'right'; 1627 ctx2.fillText(`${d.val}%`, d.x - gap, d.y); 1628 } 1629 }); 1630 }); 1631 ctx2.restore(); 1632 } 1633 }; 1634} 1635 1636function lineLabelPlugin() { 1637 return { 1638 id: 'lineLabel_' + Math.random(), 1639 afterDatasetsDraw(chart) { 1640 if (!showLabels) return; 1641 const ctx2 = chart.ctx; 1642 ctx2.save(); 1643 ctx2.font = 'bold 9px Arial, sans-serif'; 1644 ctx2.textBaseline = 'bottom'; 1645 ctx2.textAlign = 'center'; 1646 chart.data.datasets.forEach((dataset, dsi) => { 1647 const meta = chart.getDatasetMeta(dsi); 1648 ctx2.fillStyle = dataset.borderColor || '#333'; 1649 meta.data.forEach((pt, i) => { 1650 const val = dataset.data[i]; 1651 if (val === null || val === undefined) return; 1652 ctx2.fillText(`${val}%`, pt.x, pt.y - 6); 1653 }); 1654 }); 1655 ctx2.restore(); 1656 } 1657 }; 1658} 1659 1660function destroyChart(id) { 1661 if (chartInstances[id]) { chartInstances[id].destroy(); delete chartInstances[id]; } 1662} 1663 1664function updateChartTitle(breakdownLabel) { 1665 const t = T[currentLang]; 1666 const mplNames = { 1667 MPL_math: t.mplMath, 1668 MPL_lang: t.mplReading, 1669 MPL_both: `${t.mplBoth} (${t.mplMath} & ${t.mplReading})`, 1670 ALL: `${t.mplMath}, ${t.mplReading} ${currentLang==='en'?'and':'/'} ${t.mplBoth}` 1671 }; 1672 const chartTitlePhrases = { 1673 en: (mpl, bd) => bd ? `Percentage of children achieving Minimum Proficiency Levels in ${mpl} by country and ${bd.toLowerCase()}` : `Percentage of children achieving Minimum Proficiency Levels in ${mpl} by country`, 1674 fr: (mpl, bd) => bd ? `Pourcentage d'enfants atteignant les Niveaux de Compétence Minimum en ${mpl} par pays et ${bd.toLowerCase()}` : `Pourcentage d'enfants atteignant les Niveaux de Compétence Minimum en ${mpl} par pays`, 1675 es: (mpl, bd) => bd ? `Porcentaje de niños que alcanzan los Niveles MÃnimos de Competencia en ${mpl} por paÃs y ${bd.toLowerCase()}` : `Porcentaje de niños que alcanzan los Niveles MÃnimos de Competencia en ${mpl} por paÃs`, 1676 pt: (mpl, bd) => bd ? `Percentagem de crianças que atingem os NÃveis MÃnimos de Proficiência em ${mpl} por paÃs e ${bd.toLowerCase()}` : `Percentagem de crianças que atingem os NÃveis MÃnimos de Proficiência em ${mpl} por paÃs`, 1677 }; 1678 const mplPart = mplNames[state.mpl] || state.mpl;
1679 const bd = (state.breakdown === 'country' || !breakdownLabel) ? null : breakdownLabel; 1680 document.getElementById('chart-title').textContent = (chartTitlePhrases[currentLang] || chartTitlePhrases.en)(mplPart, bd); 1681 document.getElementById('chart-sub').textContent = t.chartSub; 1682 document.getElementById('th-group').textContent = breakdownLabel || t.bdCountry; 1683 document.getElementById('table-title').textContent = `${t.tableTitle} â ${breakdownLabel || t.bdCountry}`; 1684} 1685 1686// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1687// Table 1688// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1689function renderTable() { 1690 const {countries, categories, results} = currentMplData; 1691 const tbody = document.getElementById('results-tbody'); 1692 if (!countries || !countries.length) { 1693 tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:#A0AEC0;padding:20px">No data matches current filters.</td></tr>'; 1694 return; 1695 } 1696 const cats = (categories && categories.length) ? categories : ['__single__']; 1697 const isSingle = cats[0] === '__single__'; 1698 1699 tbody.innerHTML = countries.flatMap(c => cats.map(cat => { 1700 const row = results[c]?.[cat] ?? {}; 1701 const label = isSingle ? c : `${c} â ${cat}`; 1702 const cells = ['MPL_math','MPL_lang','MPL_both'].map(mpl => { 1703 const s = row[mpl]; 1704 if (!s) return '<td>â</td>'; 1705 if (s.warning) return `<td><span class="warn-badge">â N=${s.n_unweighted}<100</span></td>`; 1706 const w = Math.round((s.estimate/100)*70); 1707 return `<td><div class="pct-bar-wrap"><div class="pct-mini-bar" style="width:${w}px"></div><span>${s.estimate}%</span></div></td>`; 1708 }).join(''); 1709 const n = row.MPL_math?.n_unweighted ?? '?'; 1710 return `<tr><td><strong>${label}</strong></td>${cells}<td class="n-col">${typeof n==='number'?n.toLocaleString():n}</td></tr>`; 1711 })).join(''); 1712} 1713 1714// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1715// Access tab 1716// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1717let accessData = null; 1718async function fetchAndRenderAccess() { 1719 showLoading(true); 1720 try { 1721 const filters = buildPayload(); 1722 delete filters.ctx_filter; 1723 accessData = await fetch('/api/access-stats', { 1724 method: 'POST', headers: {'Content-Type':'application/json'}, 1725 body: JSON.stringify({filters}) 1726 }).then(r => r.json()); 1727 document.getElementById('sidebar-n').textContent = 1728 `Filtered total: ${(accessData.n_total||0).toLocaleString()} children`; 1729 renderAccessCharts(); 1730 } finally { showLoading(false); } 1731} 1732 1733function renderAccessCharts() { 1734 if (!accessData) return; 1735 const {countries, results} = accessData; 1736 1737 // Show/hide cards based on selected view 1738 const allCards = document.querySelectorAll('.access-card'); 1739 const view = state.accessView || 'all'; 1740 allCards.forEach(card => { 1741 const canvas = card.querySelector('canvas'); 1742 if (!canvas) return; 1743 if (view === 'all') { 1744 card.style.display = ''; 1745 } else { 1746 card.style.display = (canvas.id === view) ? '' : 'none'; 1747 } 1748 }); 1749 1750 // When single view, make the visible card full-width 1751 document.querySelectorAll('.access-grid').forEach(grid => { 1752 grid.style.gridTemplateColumns = (view === 'all') ? '1fr 1fr' : '1fr'; 1753 }); 1754 1755 // ââ Girls + Boys stacked âââââââââââââââââââââââââââââââââââââââââ 1756 renderAccessStacked('acc-gender', 1757 [{key:'pct_girls', label:tl('catGirls'), color:PAL.orange}, 1758 {key:'pct_boys', label:tl('catBoys'), color:PAL.teal}], 1759 countries, results); 1760 1761 // ââ % Currently Enrolled âââââââââââââââââââââââââââââââââââââââââ 1762 renderAccessSingle('acc-enrolled', 'pct_enrolled', tl('catEnrolled'), countries, results, PAL.green); 1763 1764 // ââ Urban + Rural stacked ââââââââââââââââââââââââââââââââââââââââ 1765 renderAccessStacked('acc-location', 1766 [{key:'pct_urban', label:tl('catUrban'), color:PAL.blue}, 1767 {key:'pct_rural', label:tl('catRural'), color:PAL.lightOrange}], 1768 countries, results); 1769 1770 // ââ School type distribution ââââââââââââââââââââââââââââââââââââââ 1771 renderAccessDist('acc-school', 'school_type', countries, results, 1772 ['Government','Private','Other'], [PAL.teal, PAL.green, PAL.orange], 1773 [tl('catGovt'), tl('catPrivate'), tl('catOther')]); 1774 1775 // ââ Home language âââââââââââââââââââââââââââââââââââââââââââââââââ 1776 renderAccessSingle('acc-homelang', 'pct_home_lang', tl('catHomeLang'), countries, results, PAL.blue); 1777 1778 // ââ Computer/tablet ââââââââââââââââââââââââââââââââââââââââââââââ 1779 renderAccessSingle('acc-computer', 'pct_computer', tl('catComputer'), countries, results, PAL.gold); 1780 1781 // ââ Phone type distribution âââââââââââââââââââââââââââââââââââââââ 1782 renderAccessDist('acc-phone', 'phone_type', countries, results, 1783 ['No Phone','Basic Phone','Smartphone'], ['#C53030', PAL.orange, PAL.green], 1784 [tl('catNoPhone'), tl('catBasicPhone'), tl('catSmartphone')]); 1785 1786 // ââ Parental education ââââââââââââââââââââââââââââââââââââââââââââ 1787 renderAccessDist('acc-edu', 'parental_edu', countries, results, 1788 ['None','Primary','Secondary','Tertiary','University'], 1789 ['#C53030', PAL.orange, PAL.teal, PAL.blue, PAL.green], 1790 [tl('catNone'), tl('catPrimary'), tl('catSecondary'), tl('catTertiary'), tl('catUniversity')]); 1791} 1792 1793/** Single-series horizontal bar for one access indicator */ 1794function renderAccessSingle(cid, key, label, countries, results, color) { 1795 destroyChart(cid); 1796 const canvas = document.getElementById(cid); 1797 if (!canvas) return; 1798 const estimates = countries.map(c => results[c]?.[key]?.estimate ?? null); 1799 const warnings = countries.map(c => results[c]?.[key]?.warning ?? false); 1800 const ns = countries.map(c => results[c]?.[key]?.n_unweighted ?? 0); 1801 chartInstances[cid] = new Chart(canvas.getContext('2d'), { 1802 type: 'bar', 1803 data: {labels: countries.map(tc), datasets: [{ 1804 label, data: estimates.map((v,i) => warnings[i] ? 0 : (v??0)), 1805 backgroundColor: estimates.map((_,i) => warnings[i] ? '#FED7AA' : color), 1806 borderRadius: 3 1807 }]}, 1808 options: { 1809 indexAxis: 'y', responsive: true, maintainAspectRatio: false,
1810 plugins: {legend:{display:false}, tooltip:{callbacks:{label: c => 1811 warnings[c.dataIndex] ? `â N<100` : `${estimates[c.dataIndex]}% (N=${ns[c.dataIndex].toLocaleString()})` 1812 }}}, 1813 scales: { 1814 x:{min:0,max:100,grid:{color:'#EDF2F7'},title:{display:true,text:'%',font:{size:10}},ticks:{font:{size:10}}}, 1815 y:{grid:{display:false},ticks:{font:{size:10},color:PAL.teal}} 1816 } 1817 }, 1818 plugins: [barLabelPlugin(warnings)] 1819 }); 1820} 1821 1822/** Stacked horizontal bar for 2 indicators in one bar per country (e.g. Girls + Boys) */ 1823function renderAccessStacked(cid, series, countries, results) { 1824 destroyChart(cid); 1825 const canvas = document.getElementById(cid); 1826 if (!canvas) return; 1827 const datasets = series.map(s => ({ 1828 label: s.label, 1829 data: countries.map(c => { 1830 const v = results[c]?.[s.key]; 1831 return (v && !v.warning) ? v.estimate : 0; 1832 }), 1833 backgroundColor: s.color, 1834 borderRadius: 2, 1835 maxBarThickness: 16, 1836 })); 1837 chartInstances[cid] = new Chart(canvas.getContext('2d'), { 1838 type: 'bar', 1839 data: {labels: countries.map(tc), datasets}, 1840 options: { 1841 indexAxis: 'y', responsive: true, maintainAspectRatio: false, 1842 plugins: { 1843 legend:{display:true, position:'bottom', labels:{boxWidth:10,font:{size:10}}}, 1844 tooltip:{callbacks:{label: c => `${c.dataset.label}: ${c.parsed.x ?? '?'}%`}} 1845 }, 1846 scales: { 1847 x:{stacked:true, min:0, max:100, grid:{color:'#EDF2F7'}, 1848 title:{display:true,text:'%',font:{size:10}}, ticks:{font:{size:10}}}, 1849 y:{stacked:true, grid:{display:false}, ticks:{font:{size:10},color:PAL.teal}} 1850 } 1851 }, 1852 plugins: [stackedLabelPlugin()] 1853 }); 1854} 1855 1856function renderAccessDist(cid, key, countries, results, cats, colors, catLabels) { 1857 destroyChart(cid); 1858 const canvas = document.getElementById(cid); 1859 if (!canvas) return; 1860 const datasets = cats.map((cat, ci) => ({ 1861 label: catLabels ? catLabels[ci] : cat, 1862 data: countries.map(c => results[c]?.[key]?.[cat]?.estimate ?? 0), 1863 backgroundColor: colors[ci], borderRadius: 2, 1864 })); 1865 chartInstances[cid] = new Chart(canvas.getContext('2d'), { 1866 type: 'bar', 1867 data: {labels: countries.map(tc), datasets}, 1868 options: { 1869 indexAxis: 'y', responsive: true, maintainAspectRatio: false, 1870 plugins: {legend:{display:true,position:'bottom',labels:{boxWidth:10,font:{size:10}}}}, 1871 scales: { 1872 x:{stacked:true,min:0,max:100,grid:{color:'#EDF2F7'},title:{display:true,text:'%',font:{size:10}},ticks:{font:{size:10}}}, 1873 y:{stacked:true,grid:{display:false},ticks:{font:{size:10},color:PAL.teal}} 1874 } 1875 }, 1876 plugins: [stackedLabelPlugin()] 1877 }); 1878} 1879 1880// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1881// Chatbot 1882// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1883// ââ User Guide modal ââââââââââââââââââââââââââââââââââââââââââââââââ 1884function openDemo() { 1885 document.getElementById('demo-modal').classList.add('open'); 1886} 1887function closeDemo() { 1888 const modal = document.getElementById('demo-modal'); 1889 modal.classList.remove('open'); 1890 document.getElementById('demo-video').pause(); 1891} 1892function closeDemoOnOverlay(e) { 1893 if (e.target === document.getElementById('demo-modal')) closeDemo(); 1894} 1895 1896function openGuide() { 1897 const modal = document.getElementById('guide-modal'); 1898 const iframe = document.getElementById('guide-iframe'); 1899 const guideMap = {en:'user-guide.pdf', fr:'user-guide-fr.pdf', es:'user-guide-es.pdf', pt:'user-guide-pt.pdf'}; 1900 const pdf = '/static/' + (guideMap[currentLang] || guideMap.en); 1901 iframe.src = pdf; 1902 // Update download link to match language 1903 modal.querySelector('.guide-download-btn').href = pdf; 1904 modal.classList.add('open'); 1905 document.body.style.overflow = 'hidden'; 1906} 1907function closeGuide() { 1908 document.getElementById('guide-modal').classList.remove('open'); 1909 document.body.style.overflow = ''; 1910} 1911function closeGuideOnOverlay(e) { 1912 if (e.target === document.getElementById('guide-modal')) closeGuide(); 1913} 1914 1915function toggleChat() { document.getElementById('chat-panel').classList.toggle('open'); } 1916function chatKey(e) { if (e.key==='Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } } 1917function sendEx(chip) { document.getElementById('chat-input').value = chip.textContent.trim(); sendChat(); } 1918 1919async function sendChat() { 1920 const input = document.getElementById('chat-input'); 1921 const text = input.value.trim(); 1922 if (!text) return; 1923 addMsg('user', text); 1924 chatHistory.push({role:'user', content:text}); 1925 input.value = ''; 1926 document.getElementById('chat-send').disabled = true; 1927 const typing = addTyping(); 1928 try { 1929 const res = await fetch('/api/chat', {method:'POST', 1930 headers:{'Content-Type':'application/json'}, 1931 body: JSON.stringify({messages: chatHistory, lang: currentLang})}); 1932 const data = await res.json(); 1933 typing.remove(); 1934 const reply = data.error ? `â ï¸ ${data.error}` : data.response; 1935 addMsg('assistant', reply); 1936 if (!data.error) chatHistory.push({role:'assistant', content:reply}); 1937 } catch(e) { 1938 typing.remove(); 1939 addMsg('assistant','â ï¸ Could not reach the server. Please try again.'); 1940 } finally { document.getElementById('chat-send').disabled = false; } 1941} 1942 1943function addMsg(role, text) { 1944 const msgs = document.getElementById('chat-msgs'); 1945 const now = new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}); 1946 const div = document.createElement('div'); 1947 div.className = `msg ${role}`; 1948 div.innerHTML = `<div class="msg-bubble">${esc(text)}</div><div class="msg-time">${now}</div>`; 1949 msgs.appendChild(div); msgs.scrollTop = msgs.scrollHeight; return div; 1950} 1951function addTyping() { 1952 const msgs = document.getElementById('chat-msgs'); 1953 const div = document.createElement('div'); 1954 div.className = 'msg assistant'; 1955 div.innerHTML = '<div class="msg-bubble"><div class="typing-dots"><span></span><span></span><span></span></div></div>'; 1956 msgs.appendChild(div); msgs.scrollTop = msgs.scrollHeight; return div; 1957} 1958function esc(s) { 1959 return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\n/g,'<br>'); 1960} 1961 1962function showLoading(on) { document.getElementById('loading').classList.toggle('show',on); } 1963 1964// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1965// Export helpers 1966// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1967function _chartTitledDataURL(chart, title, subtitle) { 1968 const src = chart.canvas; 1969 const titleH = subtitle ? 56 : 36; 1970 const tmp = document.createElement('canvas'); 1971 tmp.width = src.width; 1972 tmp.height = src.height + titleH; 1973 const ctx = tmp.getContext('2d'); 1974 ctx.fillStyle = '#ffffff'; 1975 ctx.fillRect(0, 0, tmp.width, tmp.height); 1976 ctx.fillStyle = '#014663'; 1977 ctx.font = 'bold 13px Calibri, Arial, sans-serif'; 1978 ctx.textBaseline = 'alphabetic'; 1979 ctx.fillText(title, 12, subtitle ? 22 : 22); 1980 if (subtitle) { 1981 ctx.fillStyle = '#718096'; 1982 ctx.font = '11px Calibri, Arial, sans-serif'; 1983 ctx.fillText(subtitle, 12, 42); 1984 } 1985 ctx.drawImage(src, 0, titleH); 1986 return tmp.toDataURL('image/png'); 1987} 1988 1989function _downloadURL(dataURL, filename) { 1990 const a = document.createElement('a'); 1991 a.href = dataURL; 1992 a.download = filename; 1993 a.click(); 1994} 1995 1996function exportChartPNG() { 1997 const date = new Date().toISOString().slice(0,10); 1998 const subtitle = document.getElementById('chart-sub').textContent; 1999 const isAll = state.mpl === 'ALL'; 2000 if (isAll) { 2001 const baseTitle = document.getElementById('chart-title').textContent; 2002 const items = [ 2003 {id:'chart-math', label:'Math', title: baseTitle.replace(/Math.*$/, 'Math' + baseTitle.slice(baseTitle.
2003indexOf(' by')))}, 2004 {id:'chart-lang', label:'Reading', title: baseTitle.replace(/Math.*$/, 'Reading' + baseTitle.slice(baseTitle.indexOf(' by')))}, 2005 ]; 2006 items.forEach(({id, label, title}, i) => { 2007 const chart = chartInstances[id]; 2008 if (!chart) return; 2009 setTimeout(() => { 2010 _downloadURL(_chartTitledDataURL(chart, title, subtitle), `PAL_MPL_${label}_${date}.png`); 2011 }, i * 400); 2012 }); 2013 } else { 2014 const chart = chartInstances['mpl-chart']; 2015 if (!chart) return; 2016 const title = document.getElementById('chart-title').textContent; 2017 const mplName = {MPL_math:'Math', MPL_lang:'Reading', MPL_both:'Both'}[state.mpl] || state.mpl; 2018 _downloadURL(_chartTitledDataURL(chart, title, subtitle), `PAL_MPL_${mplName}_${date}.png`); 2019 } 2020} 2021 2022function exportAccessExcel(cid, label) { 2023 if (!accessData) { alert('No data loaded yet â apply filters first.'); return; } 2024 if (typeof XLSX === 'undefined') { alert('Excel library not loaded. Check your connection and refresh.'); return; } 2025 const {countries, results} = accessData; 2026 const cfg = { 2027 'acc-gender': {type:'single', cols:['Country','% Girls','% Boys'], keys:['pct_girls','pct_boys']}, 2028 'acc-enrolled': {type:'single', cols:['Country','% Enrolled'], keys:['pct_enrolled']}, 2029 'acc-location': {type:'single', cols:['Country','% Urban','% Rural'], keys:['pct_urban','pct_rural']}, 2030 'acc-homelang': {type:'single', cols:['Country','% Home Language Match'], keys:['pct_home_lang']}, 2031 'acc-computer': {type:'single', cols:['Country','% Computer/Tablet'], keys:['pct_computer']}, 2032 'acc-school': {type:'dist', cols:['Country','Government (%)','Private (%)','Other (%)'], distKey:'school_type', cats:['Government','Private','Other']}, 2033 'acc-phone': {type:'dist', cols:['Country','No Phone (%)','Basic Phone (%)','Smartphone (%)'], distKey:'phone_type', c
2033ats:['No Phone','Basic Phone','Smartphone']}, 2034 'acc-edu': {type:'dist', cols:['Country','None (%)','Primary (%)','Secondary (%)','Tertiary (%)','University (%)'], distKey:'parental_edu', cats:['None','Primary','Secondary','Tertiary','University']}, 2035 }[cid]; 2036 if (!cfg) return; 2037 const rows = [cfg.cols]; 2038 countries.forEach(c => { 2039 const r = results[c] ?? {}; 2040 if (cfg.type === 'single') { 2041 rows.push([c, ...cfg.keys.map(k => { 2042 const v = r[k]; 2043 return !v ? 'â' : v.warning ? 'Suppressed (N<100)' : (v.estimate ?? 'â'); 2044 })]); 2045 } else { 2046 rows.push([c, ...cfg.cats.map(cat => r[cfg.distKey]?.[cat]?.estimate ?? 'â')]); 2047 } 2048 }); 2049 const wb = XLSX.utils.book_new(); 2050 const ws = XLSX.utils.aoa_to_sheet(rows); 2051 ws['!cols'] = cfg.cols.map(() => ({wch: 20})); 2052 XLSX.utils.book_append_sheet(wb, ws, label.replace(/_/g,' ')); 2053 XLSX.writeFile(wb, `PAL_Access_${label}_${new Date().toISOString().slice(0,10)}.xlsx`); 2054} 2055 2056function exportAccessChart(cid, label) { 2057 const chart = chartInstances[cid]; 2058 if (!chart) { alert('No chart loaded yet â apply filters first.'); return; } 2059 const date = new Date().toISOString().slice(0,10); 2060 const canvas = document.getElementById(cid); 2061 const titleEl = canvas?.closest('.access-card')?.querySelector('.access-card-title'); 2062 const title = titleEl ? titleEl.childNodes[0].textContent.trim() : label.replace(/_/g,' '); 2063 _downloadURL(_chartTitledDataURL(chart, title, 'Survey-weighted estimates'), `PAL_Access_${label}_${date}.png`); 2064} 2065 2066function exportTableExcel() { 2067 const {countries, categories, results} = currentMplData; 2068 if (!countries || !countries.length) { alert('No data to export â apply filters first.'); return; } 2069 if (typeof XLSX === 'undefined') { alert('Excel library not loaded. Check your connection and refresh.'); return; } 2070 const cats = (categories && categories.length) ? categories : ['__single__']; 2071 const isSingle = cats[0] === '__single__'; 2072 const rows = [['Group', 'MPL Math (%)', 'MPL Reading (%)', 'MPL Both (%)', 'N (unweighted)']]; 2073 countries.forEach(c => { 2074 cats.forEach(cat => { 2075 const row = results[c]?.[cat] ?? {}; 2076 const label = isSingle ? c : `${c} â ${cat}`; 2077 rows.push([ 2078 label, 2079 row.MPL_math?.warning ? 'Suppressed (N<100)' : (row.MPL_math?.estimate ?? 'â'), 2080 row.MPL_lang?.warning ? 'Suppressed (N<100)' : (row.MPL_lang?.estimate ?? 'â'), 2081 row.MPL_both?.warning ? 'Suppressed (N<100)' : (row.MPL_both?.estimate ?? 'â'), 2082 row.MPL_math?.n_unweighted ?? 'â', 2083 ]); 2084 }); 2085 }); 2086 const wb = XLSX.utils.book_new(); 2087 const ws = XLSX.utils.aoa_to_sheet(rows); 2088 ws['!cols'] = [{wch:32},{wch:15},{wch:15},{wch:15},{wch:16}]; 2089 XLSX.utils.book_append_sheet(wb, ws, 'MPL Results'); 2090 const date = new Date().toISOString().slice(0,10); 2091 XLSX.writeFile(wb, `PAL_ICAN_ICAR_Results_${date}.xlsx`); 2092} 2093 2094window.addEventListener('DOMContentLoaded', init); 2095</script>
2095 2096</body> 2097</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.