Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Diabetes Risk Predictor</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| <link href="https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" /> | |
| <style> | |
| :root { | |
| --bg: #030912; | |
| --sur: rgba(255,255,255,0.04); | |
| --sur2: rgba(255,255,255,0.07); | |
| --brd: rgba(255,255,255,0.08); | |
| --brd2: rgba(255,255,255,0.14); | |
| --cyan: #06B6D4; | |
| --cyan-lt: #67E8F9; | |
| --cyan-glow: rgba(6,182,212,0.22); | |
| --t1: #E2F4FF; | |
| --t2: #94A3B8; | |
| --t3: #475569; | |
| --hi: #F43F5E; --hi-s: rgba(244,63,94,0.13); --hi-b: rgba(244,63,94,0.28); | |
| --mo: #F97316; --mo-s: rgba(249,115,60,0.13); --mo-b: rgba(249,115,60,0.28); | |
| --lo: #10B981; --lo-s: rgba(16,185,129,0.13); --lo-b: rgba(16,185,129,0.28); | |
| --r: 14px; --r-s: 9px; --ease: cubic-bezier(.4,0,.2,1); | |
| } | |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} | |
| html{scroll-behavior:smooth} | |
| body{ | |
| font-family:'Figtree',system-ui,sans-serif; | |
| background:var(--bg); | |
| color:var(--t1); | |
| min-height:100vh; | |
| -webkit-font-smoothing:antialiased; | |
| overflow-x:hidden; | |
| } | |
| /* ββ Blood-cell canvas βββββββββββββββββββββββββββββββ */ | |
| #bgCanvas{ | |
| position:fixed;inset:0;z-index:0; | |
| pointer-events:none;opacity:.9; | |
| } | |
| /* ββ Header ββββββββββββββββββββββββββββββββββββββββββ */ | |
| header{ | |
| position:sticky;top:0;z-index:50; | |
| height:68px; | |
| background:rgba(3,9,18,.82); | |
| backdrop-filter:blur(24px); | |
| -webkit-backdrop-filter:blur(24px); | |
| border-bottom:1px solid var(--brd); | |
| display:flex;align-items:center;justify-content:space-between; | |
| padding:0 2rem; | |
| overflow:hidden; | |
| } | |
| /* ECG wave lives here as absolute background */ | |
| .ecg-track{ | |
| position:absolute;inset:0;overflow:hidden; | |
| pointer-events:none; | |
| /* fade in after brand (~300px), fade out before status pill (~120px from right) */ | |
| -webkit-mask-image:linear-gradient( | |
| to right, | |
| transparent 0px, | |
| transparent 300px, | |
| black 390px, | |
| black calc(100% - 130px), | |
| transparent calc(100% - 60px) | |
| ); | |
| mask-image:linear-gradient( | |
| to right, | |
| transparent 0px, | |
| transparent 300px, | |
| black 390px, | |
| black calc(100% - 130px), | |
| transparent calc(100% - 60px) | |
| ); | |
| } | |
| .ecg-svg{ | |
| position:absolute;top:50%;left:0; | |
| transform:translateY(-50%); | |
| will-change:transform; | |
| } | |
| /* cycle width = 140px β animation translates exactly one cycle */ | |
| .ecg-svg.playing{ | |
| animation:ecgScroll 3s linear infinite; | |
| } | |
| @keyframes ecgScroll{ | |
| from{transform:translateY(-50%) translateX(0)} | |
| to {transform:translateY(-50%) translateX(-140px)} | |
| } | |
| /* faint baseline so the full line is always visible */ | |
| .ecg-base{ | |
| fill:none; | |
| stroke:var(--cyan); | |
| stroke-width:1; | |
| opacity:.12; | |
| transition:stroke 1s ease; | |
| } | |
| .ecg-path{ | |
| fill:none; | |
| stroke:var(--cyan); | |
| stroke-width:2; | |
| stroke-linecap:round; | |
| stroke-linejoin:round; | |
| opacity:.55; | |
| transition:stroke 1s ease, opacity .5s; | |
| } | |
| .brand{position:relative;z-index:1;display:flex;align-items:center;gap:.85rem} | |
| .brand-icon{ | |
| width:40px;height:40px;border-radius:11px; | |
| background:linear-gradient(135deg,var(--cyan),var(--cyan-lt)); | |
| box-shadow:0 0 18px var(--cyan-glow); | |
| display:flex;align-items:center;justify-content:center; | |
| flex-shrink:0; | |
| } | |
| .brand-name{font-size:1rem;font-weight:700;color:var(--t1);letter-spacing:-.015em;line-height:1.2} | |
| .brand-tag{ | |
| font-size:.65rem;font-weight:700;letter-spacing:.1em; | |
| text-transform:uppercase;color:var(--cyan-lt); | |
| background:rgba(103,232,249,.1);border:1px solid rgba(103,232,249,.2); | |
| padding:.16rem .55rem;border-radius:100px; | |
| } | |
| .hd-right{position:relative;z-index:1;display:flex;align-items:center;gap:.85rem} | |
| .status-pill{ | |
| display:flex;align-items:center;gap:.6rem; | |
| padding:.55rem 1.1rem; | |
| background:rgba(16,185,129,0.10); | |
| border:1.5px solid rgba(16,185,129,0.35); | |
| border-radius:100px; | |
| font-size:.82rem;font-weight:700; | |
| color:#34D399; | |
| letter-spacing:.04em; | |
| box-shadow:0 0 14px rgba(16,185,129,0.18); | |
| } | |
| .sdot{ | |
| width:9px;height:9px;border-radius:50%; | |
| background:#10B981; | |
| box-shadow:0 0 8px #10B981, 0 0 16px rgba(16,185,129,0.5); | |
| animation:blink 2s ease infinite; | |
| flex-shrink:0; | |
| } | |
| @keyframes blink{0%,100%{opacity:1;box-shadow:0 0 8px #10B981,0 0 16px rgba(16,185,129,.5)}50%{opacity:.5;box-shadow:0 0 4px #10B981}} | |
| /* ββ Main grid βββββββββββββββββββββββββββββββββββββββ */ | |
| .wrap{ | |
| position:relative;z-index:1; | |
| max-width:1300px;margin:0 auto; | |
| padding:2rem 1.5rem 3rem; | |
| display:grid; | |
| grid-template-columns:1fr 1fr; | |
| gap:1.5rem; | |
| align-items:start; | |
| } | |
| @media(max-width:880px){.wrap{grid-template-columns:1fr}} | |
| /* ββ Glass card ββββββββββββββββββββββββββββββββββββββ */ | |
| .card{ | |
| background:var(--sur); | |
| backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px); | |
| border:1px solid var(--brd); | |
| border-radius:var(--r);overflow:hidden; | |
| } | |
| .card-hd{ | |
| padding:1.1rem 1.5rem;border-bottom:1px solid var(--brd); | |
| display:flex;align-items:center;gap:.7rem; | |
| } | |
| .card-ico{ | |
| width:30px;height:30px;border-radius:8px; | |
| background:var(--cyan-glow);border:1px solid rgba(6,182,212,.25); | |
| display:flex;align-items:center;justify-content:center; | |
| color:var(--cyan-lt);flex-shrink:0; | |
| } | |
| .card-ttl{font-size:.88rem;font-weight:600;color:var(--t1)} | |
| .card-bd{padding:1.5rem} | |
| /* ββ Presets βββββββββββββββββββββββββββββββββββββββββ */ | |
| .pre-lbl{font-size:.67rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--t3);margin-bottom:.45rem} | |
| .pre-row{display:flex;gap:.5rem;margin-bottom:1.4rem} | |
| .pre-btn{ | |
| flex:1;padding:.42rem .4rem; | |
| border-radius:var(--r-s);border:1px solid var(--brd); | |
| background:rgba(255,255,255,.025); | |
| font-family:'Figtree',sans-serif;font-size:.73rem;font-weight:600; | |
| color:var(--t2);cursor:pointer; | |
| transition:all .18s var(--ease); | |
| } | |
| .pre-btn:hover{background:var(--sur2)} | |
| .pre-btn.pl:hover{border-color:var(--lo); color:var(--lo)} | |
| .pre-btn.pm:hover{border-color:var(--mo); color:var(--mo)} | |
| .pre-btn.ph:hover{border-color:var(--hi); color:var(--hi)} | |
| .pre-btn:focus-visible{outline:2px solid var(--cyan);outline-offset:2px} | |
| /* ββ Field grid ββββββββββββββββββββββββββββββββββββββ */ | |
| .fgrid{display:grid;grid-template-columns:1fr 1fr;gap:1.1rem 1.2rem} | |
| .field{display:flex;flex-direction:column;gap:.28rem} | |
| .field.full{grid-column:1/-1} | |
| .flbl{ | |
| display:flex;justify-content:space-between;align-items:center; | |
| } | |
| .flbl-txt{ | |
| font-size:.7rem;font-weight:700; | |
| text-transform:uppercase;letter-spacing:.07em;color:var(--t2); | |
| } | |
| .flbl-txt span{font-size:.66rem;font-weight:400;text-transform:none;letter-spacing:0;color:var(--t3)} | |
| .status-tag{ | |
| font-size:.62rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase; | |
| padding:.1rem .48rem;border-radius:100px; | |
| transition:all .3s var(--ease); | |
| display:none; | |
| } | |
| /* dual control: slider + number */ | |
| .dual{display:flex;align-items:center;gap:.6rem} | |
| /* range slider */ | |
| input[type="range"]{ | |
| -webkit-appearance:none;appearance:none; | |
| flex:1;height:5px;border-radius:100px;cursor:pointer;outline:none; | |
| background:linear-gradient(to right,var(--track-fill,var(--cyan)) 0%,var(--track-fill,var(--cyan)) var(--pct,50%),rgba(255,255,255,.1) var(--pct,50%),rgba(255,255,255,.1) 100%); | |
| transition:background .1s; | |
| } | |
| input[type="range"]::-webkit-slider-thumb{ | |
| -webkit-appearance:none;appearance:none; | |
| width:14px;height:14px;border-radius:50%; | |
| background:var(--thumb-color,var(--cyan)); | |
| border:2px solid rgba(255,255,255,.25); | |
| box-shadow:0 0 8px var(--thumb-glow,var(--cyan-glow)); | |
| transition:transform .15s, box-shadow .15s; | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.25)} | |
| input[type="range"]::-moz-range-thumb{ | |
| width:14px;height:14px;border-radius:50%;border:none; | |
| background:var(--thumb-color,var(--cyan)); | |
| } | |
| input[type="range"]:focus-visible::-webkit-slider-thumb{ | |
| box-shadow:0 0 0 3px var(--cyan-glow), 0 0 8px var(--cyan-glow); | |
| } | |
| /* number input */ | |
| input[type="number"]{ | |
| -moz-appearance:textfield; | |
| width:62px;padding:.42rem .5rem; | |
| background:rgba(255,255,255,.05); | |
| border:1.5px solid var(--brd);border-radius:var(--r-s); | |
| color:var(--t1);font-family:'Figtree',sans-serif; | |
| font-size:.85rem;font-weight:600;text-align:center; | |
| outline:none;transition:border-color .18s,box-shadow .18s; | |
| } | |
| input[type="number"]::-webkit-inner-spin-button, | |
| input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none} | |
| input[type="number"]:focus{ | |
| border-color:var(--cyan); | |
| box-shadow:0 0 0 3px var(--cyan-glow); | |
| } | |
| .field-hint{font-size:.64rem;color:var(--t3)} | |
| /* ββ Run button ββββββββββββββββββββββββββββββββββββββ */ | |
| .btn-run{ | |
| margin-top:1.35rem;width:100%; | |
| padding:.85rem 1rem; | |
| background:linear-gradient(135deg,var(--cyan),var(--cyan-lt)); | |
| color:#fff;font-family:'Figtree',sans-serif; | |
| font-size:.93rem;font-weight:700;letter-spacing:.01em; | |
| border:none;border-radius:var(--r-s);cursor:pointer; | |
| display:flex;align-items:center;justify-content:center;gap:.55rem; | |
| position:relative;overflow:hidden; | |
| transition:transform .15s var(--ease),box-shadow .15s; | |
| } | |
| .btn-run:hover:not(:disabled){ | |
| transform:translateY(-1px); | |
| box-shadow:0 10px 30px rgba(6,182,212,.40); | |
| } | |
| .btn-run:active:not(:disabled){transform:translateY(0)} | |
| .btn-run:disabled{cursor:not-allowed;opacity:.6} | |
| .btn-run:focus-visible{outline:2px solid var(--cyan-lt);outline-offset:3px} | |
| .btn-run.loading::after{ | |
| content:'';position:absolute;inset:0; | |
| background:linear-gradient(90deg,transparent,rgba(255,255,255,.18),transparent); | |
| animation:shim 1.3s linear infinite; | |
| } | |
| @keyframes shim{from{transform:translateX(-100%)}to{transform:translateX(100%)}} | |
| .spin{ | |
| display:none;width:15px;height:15px; | |
| border:2.5px solid rgba(255,255,255,.3); | |
| border-top-color:#fff;border-radius:50%; | |
| animation:rot .65s linear infinite; | |
| } | |
| .btn-run.loading .spin{display:block} | |
| .btn-run.loading .btn-ic{display:none} | |
| @keyframes rot{to{transform:rotate(360deg)}} | |
| /* ββ Error βββββββββββββββββββββββββββββββββββββββββββ */ | |
| .err-box{ | |
| margin-top:.85rem;display:none; | |
| padding:.7rem .9rem; | |
| background:var(--hi-s);border:1px solid var(--hi-b); | |
| border-radius:var(--r-s);color:#FDA4AF; | |
| font-size:.8rem;align-items:flex-start;gap:.45rem; | |
| } | |
| .err-box.show{display:flex} | |
| /* ββ Results: empty ββββββββββββββββββββββββββββββββββ */ | |
| .empty-s{ | |
| padding:3.8rem 2rem; | |
| display:flex;flex-direction:column;align-items:center;gap:.9rem;text-align:center; | |
| } | |
| .empty-ring{ | |
| width:64px;height:64px;border-radius:50%; | |
| border:1.5px dashed rgba(255,255,255,.13); | |
| display:flex;align-items:center;justify-content:center; | |
| color:var(--t3); | |
| animation:pulse-ring 3s ease infinite; | |
| } | |
| @keyframes pulse-ring{ | |
| 0%,100%{box-shadow:0 0 0 0 rgba(6,182,212,0)} | |
| 50%{box-shadow:0 0 0 8px rgba(6,182,212,.07)} | |
| } | |
| .empty-h{font-size:.9rem;font-weight:600;color:var(--t2)} | |
| .empty-p{font-size:.77rem;color:var(--t3);max-width:195px;line-height:1.6} | |
| /* ββ Results: output βββββββββββββββββββββββββββββββββ */ | |
| .res-out{display:none} | |
| .res-out.show{display:block;animation:fadeUp .4s var(--ease)} | |
| @keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}} | |
| /* scanning overlay */ | |
| .scan-overlay{ | |
| position:absolute;inset:0;pointer-events:none;z-index:20; | |
| background:linear-gradient(to bottom,transparent 44%,rgba(6,182,212,.07) 48%,rgba(6,182,212,.16) 50%,rgba(6,182,212,.07) 52%,transparent 56%); | |
| display:none; | |
| } | |
| .card.scanning .scan-overlay{display:block;animation:scanDown 1s ease infinite} | |
| @keyframes scanDown{from{transform:translateY(-110%)}to{transform:translateY(110%)}} | |
| /* risk badge */ | |
| .r-badge{ | |
| display:inline-flex;align-items:center;gap:.4rem; | |
| padding:.3rem .8rem;border-radius:100px; | |
| font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.09em; | |
| margin-bottom:1.3rem; | |
| } | |
| .r-badge.high {background:var(--hi-s);color:#FB7185;border:1px solid var(--hi-b)} | |
| .r-badge.moderate{background:var(--mo-s);color:#FB923C;border:1px solid var(--mo-b)} | |
| .r-badge.low {background:var(--lo-s);color:#34D399;border:1px solid var(--lo-b)} | |
| /* gauge */ | |
| .gauge-wrap{display:flex;justify-content:center;margin-bottom:1.4rem} | |
| .gauge-container{position:relative;width:170px;height:170px;display:flex;align-items:center;justify-content:center} | |
| .gauge-svg{position:absolute;inset:0;transform:rotate(-90deg)} | |
| .g-track{fill:none;stroke:rgba(255,255,255,.06);stroke-width:12} | |
| .g-fill{ | |
| fill:none;stroke-width:12;stroke-linecap:round; | |
| stroke-dasharray:408.41; /* 2ΟΓ65 */ | |
| stroke-dashoffset:408.41; | |
| transition:stroke-dashoffset 1s var(--ease),stroke .5s ease; | |
| } | |
| /* outer glow ring */ | |
| .g-outer{fill:none;stroke-width:1.5;stroke-dasharray:408.41;stroke-dashoffset:408.41;opacity:.35;transition:stroke-dashoffset 1s var(--ease),stroke .5s ease} | |
| .gauge-info{ | |
| position:relative;z-index:1; | |
| display:flex;flex-direction:column;align-items:center;gap:0; | |
| } | |
| .g-pct{font-size:2.4rem;font-weight:800;letter-spacing:-.05em;line-height:1} | |
| .g-lbl{font-size:.64rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--t3);margin-top:.18rem} | |
| /* stats row */ | |
| .stats{ | |
| display:grid;grid-template-columns:1fr 1px 1fr 1px 1fr; | |
| background:rgba(255,255,255,.025);border:1px solid var(--brd); | |
| border-radius:var(--r-s);margin-bottom:1.35rem;overflow:hidden; | |
| } | |
| .stat{padding:.72rem .5rem;text-align:center} | |
| .stat-v{font-size:.92rem;font-weight:700;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | |
| .stat-k{font-size:.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--t3);margin-top:.1rem} | |
| .st-div{background:var(--brd);align-self:stretch} | |
| /* shap */ | |
| .shap-hd{ | |
| display:flex;align-items:center;gap:.45rem; | |
| font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em; | |
| color:var(--t2);margin-bottom:.75rem; | |
| } | |
| .shap-ico{ | |
| width:20px;height:20px;border-radius:5px; | |
| background:rgba(249,115,22,.12);border:1px solid rgba(249,115,22,.22); | |
| display:flex;align-items:center;justify-content:center;color:#FB923C;flex-shrink:0; | |
| } | |
| /* plain-language summary box */ | |
| .shap-summary{ | |
| padding:.7rem .9rem; | |
| background:rgba(255,255,255,.035); | |
| border:1px solid var(--brd); | |
| border-left:3px solid var(--cyan); | |
| border-radius:var(--r-s); | |
| font-size:.79rem;line-height:1.65;color:var(--t2); | |
| margin-bottom:1rem; | |
| } | |
| .shap-summary strong{color:var(--t1);font-weight:600} | |
| /* legend row */ | |
| .shap-legend{ | |
| display:flex;gap:1rem;margin-bottom:.8rem; | |
| } | |
| .legend-item{ | |
| display:flex;align-items:center;gap:.35rem; | |
| font-size:.67rem;font-weight:600;color:var(--t3); | |
| } | |
| .legend-dot{width:8px;height:8px;border-radius:2px;flex-shrink:0} | |
| .legend-dot.p{background:linear-gradient(90deg,#BE123C,#F87171)} | |
| .legend-dot.n{background:linear-gradient(90deg,#065F46,#34D399)} | |
| /* each bar row */ | |
| .sh-row{ | |
| display:grid;grid-template-columns:8.5rem 1fr 5.2rem; | |
| align-items:center;gap:.5rem;margin-bottom:.55rem; | |
| opacity:0;animation:shIn .3s var(--ease) forwards; | |
| cursor:default; | |
| } | |
| .sh-row:hover .sh-track{background:rgba(255,255,255,.1)} | |
| @keyframes shIn{from{opacity:0;transform:translateX(-6px)}to{opacity:1;transform:translateX(0)}} | |
| .sh-nm{font-size:.74rem;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | |
| .sh-track{height:5px;background:rgba(255,255,255,.06);border-radius:100px;overflow:hidden;transition:background .2s} | |
| .sh-bar{height:100%;border-radius:100px;transition:width .6s var(--ease)} | |
| .sh-bar.p{background:linear-gradient(90deg,#BE123C,#F87171)} | |
| .sh-bar.n{background:linear-gradient(90deg,#065F46,#34D399)} | |
| /* impact badge (replaces raw decimal) */ | |
| .sh-badge{ | |
| font-size:.63rem;font-weight:700;text-align:center; | |
| padding:.18rem .48rem;border-radius:100px; | |
| white-space:nowrap;cursor:help; | |
| transition:opacity .15s; | |
| } | |
| .sh-badge:hover{opacity:.75} | |
| .sh-badge.strong {background:rgba(244,63,94,.15); color:#FB7185; border:1px solid rgba(244,63,94,.3)} | |
| .sh-badge.moderate{background:rgba(249,115,22,.15);color:#FB923C; border:1px solid rgba(249,115,22,.3)} | |
| .sh-badge.low {background:rgba(100,116,139,.15);color:#94A3B8;border:1px solid rgba(100,116,139,.25)} | |
| /* ββ Footer ββββββββββββββββββββββββββββββββββββββββββ */ | |
| footer{ | |
| position:relative;z-index:1; | |
| text-align:center;padding:1.25rem 2rem 2.5rem; | |
| color:var(--t3);font-size:.71rem;line-height:1.7; | |
| border-top:1px solid var(--brd); | |
| } | |
| @media(prefers-reduced-motion:reduce){ | |
| *,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- floating blood-cell particles --> | |
| <canvas id="bgCanvas"></canvas> | |
| <!-- ββ HEADER ββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <header> | |
| <!-- ECG animated background --> | |
| <div class="ecg-track" aria-hidden="true"> | |
| <svg class="ecg-svg" id="ecgSvg" height="60"> | |
| <!-- full-width faint baseline so the line is always visible --> | |
| <line class="ecg-base" id="ecgBase" x1="0" y1="30" x2="9999" y2="30"/> | |
| <!-- animated ECG waveform --> | |
| <path class="ecg-path" id="ecgPath"/> | |
| </svg> | |
| </div> | |
| <div class="brand"> | |
| <div class="brand-icon" aria-hidden="true"> | |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <div class="brand-name">Diabetes Risk Predictor</div> | |
| <div class="brand-tag">XGBoost Β· SHAP Explainability</div> | |
| </div> | |
| </div> | |
| <div class="hd-right"> | |
| <div class="status-pill" role="status" aria-label="API status"> | |
| <div class="sdot" aria-hidden="true"></div> | |
| <span>API Live</span> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- ββ MAIN ββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="wrap"> | |
| <!-- ββ LEFT: Patient Input βββββββββββββββββββββββββββ --> | |
| <section class="card" aria-label="Patient parameter input"> | |
| <div class="card-hd"> | |
| <div class="card-ico" aria-hidden="true"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/> | |
| </svg> | |
| </div> | |
| <span class="card-ttl">Patient Parameters</span> | |
| </div> | |
| <div class="card-bd"> | |
| <div class="pre-lbl">Quick Presets</div> | |
| <div class="pre-row"> | |
| <button class="pre-btn pl" type="button" onclick="loadPreset('low')" aria-label="Load low-risk preset">Low Risk</button> | |
| <button class="pre-btn pm" type="button" onclick="loadPreset('moderate')" aria-label="Load moderate-risk preset">Moderate</button> | |
| <button class="pre-btn ph" type="button" onclick="loadPreset('high')" aria-label="Load high-risk preset">High Risk</button> | |
| </div> | |
| <form id="pForm" onsubmit="return false;" novalidate> | |
| <div class="fgrid"> | |
| <!-- pregnancies --> | |
| <div class="field" id="f-pregnancies"> | |
| <div class="flbl"> | |
| <span class="flbl-txt">Pregnancies</span> | |
| <span class="status-tag" id="st-pregnancies"></span> | |
| </div> | |
| <div class="dual"> | |
| <input type="range" id="r-pregnancies" min="0" max="17" step="1" value="2" oninput="sync('pregnancies',this.value)" aria-label="Pregnancies slider"/> | |
| <input type="number" id="pregnancies" min="0" max="17" step="1" value="2" oninput="sync('pregnancies',this.value,true)" aria-label="Pregnancies value"/> | |
| </div> | |
| <span class="field-hint">0 β 17</span> | |
| </div> | |
| <!-- glucose --> | |
| <div class="field" id="f-glucose"> | |
| <div class="flbl"> | |
| <span class="flbl-txt">Glucose <span>mg/dL</span></span> | |
| <span class="status-tag" id="st-glucose"></span> | |
| </div> | |
| <div class="dual"> | |
| <input type="range" id="r-glucose" min="50" max="250" step="1" value="120" oninput="sync('glucose',this.value)" aria-label="Glucose slider"/> | |
| <input type="number" id="glucose" min="50" max="250" step="1" value="120" oninput="sync('glucose',this.value,true)" aria-label="Glucose value"/> | |
| </div> | |
| <span class="field-hint">50 β 250</span> | |
| </div> | |
| <!-- blood_pressure --> | |
| <div class="field" id="f-blood_pressure"> | |
| <div class="flbl"> | |
| <span class="flbl-txt">Blood Pressure <span>mm Hg</span></span> | |
| <span class="status-tag" id="st-blood_pressure"></span> | |
| </div> | |
| <div class="dual"> | |
| <input type="range" id="r-blood_pressure" min="40" max="130" step="1" value="72" oninput="sync('blood_pressure',this.value)" aria-label="Blood pressure slider"/> | |
| <input type="number" id="blood_pressure" min="40" max="130" step="1" value="72" oninput="sync('blood_pressure',this.value,true)" aria-label="Blood pressure value"/> | |
| </div> | |
| <span class="field-hint">40 β 130</span> | |
| </div> | |
| <!-- skin_thickness --> | |
| <div class="field" id="f-skin_thickness"> | |
| <div class="flbl"> | |
| <span class="flbl-txt">Skin Thickness <span>mm</span></span> | |
| <span class="status-tag" id="st-skin_thickness"></span> | |
| </div> | |
| <div class="dual"> | |
| <input type="range" id="r-skin_thickness" min="0" max="100" step="1" value="20" oninput="sync('skin_thickness',this.value)" aria-label="Skin thickness slider"/> | |
| <input type="number" id="skin_thickness" min="0" max="100" step="1" value="20" oninput="sync('skin_thickness',this.value,true)" aria-label="Skin thickness value"/> | |
| </div> | |
| <span class="field-hint">0 β 100</span> | |
| </div> | |
| <!-- insulin --> | |
| <div class="field" id="f-insulin"> | |
| <div class="flbl"> | |
| <span class="flbl-txt">Insulin <span>mu U/ml</span></span> | |
| <span class="status-tag" id="st-insulin"></span> | |
| </div> | |
| <div class="dual"> | |
| <input type="range" id="r-insulin" min="0" max="900" step="1" value="80" oninput="sync('insulin',this.value)" aria-label="Insulin slider"/> | |
| <input type="number" id="insulin" min="0" max="900" step="1" value="80" oninput="sync('insulin',this.value,true)" aria-label="Insulin value"/> | |
| </div> | |
| <span class="field-hint">0 β 900</span> | |
| </div> | |
| <!-- bmi --> | |
| <div class="field" id="f-bmi"> | |
| <div class="flbl"> | |
| <span class="flbl-txt">BMI</span> | |
| <span class="status-tag" id="st-bmi"></span> | |
| </div> | |
| <div class="dual"> | |
| <input type="range" id="r-bmi" min="10" max="70" step="0.1" value="25" oninput="sync('bmi',this.value)" aria-label="BMI slider"/> | |
| <input type="number" id="bmi" min="10" max="70" step="0.1" value="25" oninput="sync('bmi',this.value,true)" aria-label="BMI value"/> | |
| </div> | |
| <span class="field-hint">10.0 β 70.0</span> | |
| </div> | |
| <!-- dpf --> | |
| <div class="field full" id="f-dpf"> | |
| <div class="flbl"> | |
| <span class="flbl-txt">Diabetes Pedigree Function</span> | |
| <span class="status-tag" id="st-dpf"></span> | |
| </div> | |
| <div class="dual"> | |
| <input type="range" id="r-dpf" min="0" max="2.5" step="0.01" value="0.35" oninput="sync('dpf',this.value)" aria-label="Diabetes pedigree slider"/> | |
| <input type="number" id="dpf" min="0" max="2.5" step="0.01" value="0.35" oninput="sync('dpf',this.value,true)" aria-label="Diabetes pedigree value"/> | |
| </div> | |
| <span class="field-hint">0.0 β 2.5 Β· Hereditary risk score</span> | |
| </div> | |
| <!-- age --> | |
| <div class="field" id="f-age"> | |
| <div class="flbl"> | |
| <span class="flbl-txt">Age</span> | |
| <span class="status-tag" id="st-age"></span> | |
| </div> | |
| <div class="dual"> | |
| <input type="range" id="r-age" min="18" max="90" step="1" value="30" oninput="sync('age',this.value)" aria-label="Age slider"/> | |
| <input type="number" id="age" min="18" max="90" step="1" value="30" oninput="sync('age',this.value,true)" aria-label="Age value"/> | |
| </div> | |
| <span class="field-hint">18 β 90</span> | |
| </div> | |
| </div> | |
| <button class="btn-run" id="runBtn" type="submit" onclick="runAnalysis()" aria-label="Run diabetes risk analysis"> | |
| <div class="spin" aria-hidden="true"></div> | |
| <svg class="btn-ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> | |
| <polygon points="5 3 19 12 5 21 5 3"/> | |
| </svg> | |
| <span id="btnTxt">Run Analysis</span> | |
| </button> | |
| <div class="err-box" id="errBox" role="alert" aria-live="assertive"> | |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0;margin-top:1px"> | |
| <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/> | |
| </svg> | |
| <span id="errTxt"></span> | |
| </div> | |
| </form> | |
| </div> | |
| </section> | |
| <!-- ββ RIGHT: Risk Assessment ββββββββββββββββββββββββ --> | |
| <section class="card" id="resultCard" aria-label="Prediction results" style="position:relative" aria-live="polite"> | |
| <div class="scan-overlay" id="scanOverlay" aria-hidden="true"></div> | |
| <div class="card-hd"> | |
| <div class="card-ico" aria-hidden="true"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M22 12h-4l-3 9L9 3l-3 9H2"/> | |
| </svg> | |
| </div> | |
| <span class="card-ttl">Risk Assessment</span> | |
| </div> | |
| <div class="card-bd"> | |
| <!-- Empty state --> | |
| <div class="empty-s" id="emptyS"> | |
| <div class="empty-ring" aria-hidden="true"> | |
| <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/> | |
| </svg> | |
| </div> | |
| <div class="empty-h">Awaiting Analysis</div> | |
| <p class="empty-p">Adjust the patient parameters on the left and click Run Analysis.</p> | |
| </div> | |
| <!-- Result output --> | |
| <div class="res-out" id="resOut"> | |
| <div class="r-badge" id="rBadge" role="status"></div> | |
| <!-- Gauge --> | |
| <div class="gauge-wrap"> | |
| <div class="gauge-container"> | |
| <svg class="gauge-svg" width="170" height="170" viewBox="0 0 170 170" aria-hidden="true"> | |
| <!-- outer decoration ring --> | |
| <circle class="g-outer" id="gOuter" cx="85" cy="85" r="78" stroke="var(--cyan)"/> | |
| <!-- track --> | |
| <circle class="g-track" cx="85" cy="85" r="65"/> | |
| <!-- fill arc --> | |
| <circle class="g-fill" id="gFill" cx="85" cy="85" r="65"/> | |
| </svg> | |
| <div class="gauge-info" aria-label="Risk probability gauge"> | |
| <div class="g-pct" id="gPct">β</div> | |
| <div class="g-lbl">Risk Score</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Stats --> | |
| <div class="stats" role="list"> | |
| <div class="stat" role="listitem"> | |
| <div class="stat-v" id="sLabel">β</div> | |
| <div class="stat-k">Diagnosis</div> | |
| </div> | |
| <div class="st-div" aria-hidden="true"></div> | |
| <div class="stat" role="listitem"> | |
| <div class="stat-v" id="sProb">β</div> | |
| <div class="stat-k">Probability</div> | |
| </div> | |
| <div class="st-div" aria-hidden="true"></div> | |
| <div class="stat" role="listitem"> | |
| <div class="stat-v">40%</div> | |
| <div class="stat-k">Threshold</div> | |
| </div> | |
| </div> | |
| <!-- SHAP --> | |
| <div> | |
| <div class="shap-hd"> | |
| <div class="shap-ico" aria-hidden="true"> | |
| <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> | |
| <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/> | |
| </svg> | |
| </div> | |
| What's Driving This Result? | |
| </div> | |
| <div id="shapBars" role="list" aria-label="SHAP feature contributions"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| <!-- ββ FOOTER ββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <footer> | |
| <strong style="color:var(--t2);font-weight:600">Clinical Disclaimer</strong><br> | |
| This interface is for research and educational purposes only. Predictions are generated by a machine learning model | |
| and do not constitute medical advice, diagnosis, or treatment. | |
| Always consult a qualified healthcare professional for clinical decisions. | |
| </footer> | |
| <script> | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CONFIG | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const API = ''; | |
| const CIRC = 2 * Math.PI * 65; // β 408.41 | |
| const RISK = { | |
| high: { color:'#F43F5E', light:'#FB7185', glow:'rgba(244,63,94,.35)', label:'High Risk', ecg:'#F43F5E' }, | |
| moderate: { color:'#F97316', light:'#FB923C', glow:'rgba(249,115,22,.35)', label:'Moderate Risk', ecg:'#F97316' }, | |
| low: { color:'#10B981', light:'#34D399', glow:'rgba(16,185,129,.35)', label:'Low Risk', ecg:'#10B981' }, | |
| }; | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ECG WAVEFORM β built in JS, set on SVG path | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| (function buildECG() { | |
| /* | |
| * Cycle width = 140px, height = 60, baseline = y=30. | |
| * Denser waveform so the whole header is filled with visible spikes. | |
| * SVG width = screen width + 2 extra cycles so there is never a gap. | |
| */ | |
| const W = 140; // cycle width β must match CSS keyframe translateX | |
| const BL = 30; // baseline y | |
| const cycle = [ // [x_offset, y_relative_to_baseline] | |
| [0,0],[18,0], | |
| [21,-3],[24,-8],[27,-3],[30,0], // P wave | |
| [35,0],[37,4],[40,-34],[44,10],[47,0], // QRS complex | |
| [50,0],[53,-2],[56,-8],[60,-13],[64,-8],[68,0], // T wave | |
| [W,0], // flat to next cycle | |
| ]; | |
| function buildPath(cycles) { | |
| let d = ''; | |
| for (let c = 0; c < cycles; c++) { | |
| cycle.forEach(([x, dy], i) => { | |
| const cmd = (c === 0 && i === 0) ? 'M' : 'L'; | |
| d += `${cmd}${c * W + x},${BL + dy} `; | |
| }); | |
| } | |
| return d.trim(); | |
| } | |
| function init() { | |
| const screenW = window.innerWidth; | |
| const cycles = Math.ceil(screenW / W) + 3; // extra cycles for seamless fill | |
| const svg = document.getElementById('ecgSvg'); | |
| svg.setAttribute('width', cycles * W); | |
| svg.setAttribute('viewBox',`0 0 ${cycles * W} 60`); | |
| document.getElementById('ecgPath').setAttribute('d', buildPath(cycles)); | |
| svg.classList.add('playing'); | |
| } | |
| init(); | |
| window.addEventListener('resize', init); | |
| })(); | |
| function setECGColor(color) { | |
| document.querySelector('.ecg-path').style.stroke = color; | |
| document.querySelector('.ecg-base').style.stroke = color; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| BLOOD-CELL CANVAS PARTICLES | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| (function initCanvas() { | |
| const canvas = document.getElementById('bgCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| function resize() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| } | |
| resize(); | |
| window.addEventListener('resize', resize); | |
| const CELLS = Array.from({length: 28}, () => ({ | |
| x: Math.random() * window.innerWidth, | |
| y: Math.random() * window.innerHeight, | |
| rx: 7 + Math.random() * 11, | |
| vy: -(0.18 + Math.random() * 0.35), | |
| vx: (Math.random() - .5) * 0.25, | |
| ang: Math.random() * Math.PI, | |
| va: (Math.random() - .5) * 0.006, | |
| op: 0.025 + Math.random() * 0.04, | |
| })); | |
| function drawFrame() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| CELLS.forEach(c => { | |
| ctx.save(); | |
| ctx.translate(c.x, c.y); | |
| ctx.rotate(c.ang); | |
| /* outer cell body */ | |
| ctx.beginPath(); | |
| ctx.ellipse(0, 0, c.rx, c.rx * 0.58, 0, 0, Math.PI * 2); | |
| ctx.fillStyle = `rgba(200,40,60,${c.op})`; | |
| ctx.fill(); | |
| /* biconcave dimple */ | |
| ctx.beginPath(); | |
| ctx.ellipse(0, 0, c.rx * 0.52, c.rx * 0.22, 0, 0, Math.PI * 2); | |
| ctx.fillStyle = `rgba(0,4,12,${c.op * 1.6})`; | |
| ctx.fill(); | |
| ctx.restore(); | |
| c.x += c.vx; | |
| c.y += c.vy; | |
| c.ang += c.va; | |
| if (c.y < -25) { c.y = canvas.height + 25; c.x = Math.random() * canvas.width; } | |
| if (c.x < -25) c.x = canvas.width + 25; | |
| if (c.x > canvas.width + 25) c.x = -25; | |
| }); | |
| requestAnimationFrame(drawFrame); | |
| } | |
| drawFrame(); | |
| })(); | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CLINICAL STATUS TAGS β update as sliders move | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const STATUS_MAP = { | |
| glucose: [ | |
| { max:100, lbl:'Normal', css:'var(--lo-s)', brd:'var(--lo-b)', col:'#34D399' }, | |
| { max:126, lbl:'Pre-Diabetic',css:'var(--mo-s)', brd:'var(--mo-b)', col:'#FB923C' }, | |
| { max:999, lbl:'Diabetic', css:'var(--hi-s)', brd:'var(--hi-b)', col:'#FB7185' }, | |
| ], | |
| bmi: [ | |
| { max:18.5, lbl:'Underweight',css:'var(--mo-s)',brd:'var(--mo-b)',col:'#FB923C' }, | |
| { max:25, lbl:'Normal', css:'var(--lo-s)',brd:'var(--lo-b)',col:'#34D399' }, | |
| { max:30, lbl:'Overweight', css:'var(--mo-s)',brd:'var(--mo-b)',col:'#FB923C' }, | |
| { max:999, lbl:'Obese', css:'var(--hi-s)',brd:'var(--hi-b)',col:'#FB7185' }, | |
| ], | |
| blood_pressure: [ | |
| { max:80, lbl:'Normal', css:'var(--lo-s)',brd:'var(--lo-b)',col:'#34D399' }, | |
| { max:90, lbl:'Elevated', css:'var(--mo-s)',brd:'var(--mo-b)',col:'#FB923C' }, | |
| { max:999, lbl:'High', css:'var(--hi-s)',brd:'var(--hi-b)',col:'#FB7185' }, | |
| ], | |
| insulin: [ | |
| { max:100, lbl:'Normal', css:'var(--lo-s)',brd:'var(--lo-b)',col:'#34D399' }, | |
| { max:166, lbl:'Elevated',css:'var(--mo-s)',brd:'var(--mo-b)',col:'#FB923C' }, | |
| { max:999, lbl:'High', css:'var(--hi-s)',brd:'var(--hi-b)',col:'#FB7185' }, | |
| ], | |
| }; | |
| function updateStatus(id, val) { | |
| const tag = document.getElementById(`st-${id}`); | |
| if (!tag) return; | |
| const rules = STATUS_MAP[id]; | |
| if (!rules) { tag.style.display = 'none'; return; } | |
| const r = rules.find(r => +val <= r.max) || rules[rules.length - 1]; | |
| const rng = document.getElementById(`r-${id}`); | |
| const num = document.getElementById(id); | |
| tag.textContent = r.lbl; | |
| tag.style.display = 'inline-flex'; | |
| tag.style.background = r.css; | |
| tag.style.border = `1px solid ${r.brd}`; | |
| tag.style.color = r.col; | |
| /* tint the slider thumb to match */ | |
| if (rng) { | |
| rng.style.setProperty('--track-fill', r.col); | |
| rng.style.setProperty('--thumb-color', r.col); | |
| rng.style.setProperty('--thumb-glow', r.css); | |
| } | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SLIDER β NUMBER SYNC | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function updateSliderProgress(rng) { | |
| const mn = +rng.min, mx = +rng.max, val = +rng.value; | |
| const pct = ((val - mn) / (mx - mn) * 100).toFixed(1) + '%'; | |
| rng.style.setProperty('--pct', pct); | |
| } | |
| function sync(id, val, fromNum = false) { | |
| const rng = document.getElementById(`r-${id}`); | |
| const num = document.getElementById(id); | |
| if (!fromNum && rng) { num.value = parseFloat(val); } | |
| if (fromNum && rng) { rng.value = parseFloat(val); } | |
| if (rng) updateSliderProgress(rng); | |
| updateStatus(id, val); | |
| } | |
| /* initialise all sliders on load */ | |
| ['pregnancies','glucose','blood_pressure','skin_thickness','insulin','bmi','dpf','age'].forEach(id => { | |
| const rng = document.getElementById(`r-${id}`); | |
| if (rng) { updateSliderProgress(rng); updateStatus(id, rng.value); } | |
| }); | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PRESETS | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const PRESETS = { | |
| low: { pregnancies:1, glucose:88, blood_pressure:66, skin_thickness:16, insulin:48, bmi:21.6, dpf:0.17, age:24 }, | |
| moderate: { pregnancies:3, glucose:128, blood_pressure:77, skin_thickness:27, insulin:138, bmi:30.2, dpf:0.42, age:37 }, | |
| high: { pregnancies:7, glucose:174, blood_pressure:90, skin_thickness:44, insulin:295, bmi:43.1, dpf:0.93, age:57 }, | |
| }; | |
| function loadPreset(key) { | |
| const p = PRESETS[key]; | |
| const map = { pregnancies:'pregnancies', glucose:'glucose', blood_pressure:'blood_pressure', | |
| skin_thickness:'skin_thickness', insulin:'insulin', bmi:'bmi', dpf:'dpf', age:'age' }; | |
| Object.entries(map).forEach(([fid, pk]) => { | |
| const val = p[pk]; | |
| document.getElementById(fid).value = val; | |
| const rng = document.getElementById(`r-${fid}`); | |
| if (rng) { rng.value = val; updateSliderProgress(rng); } | |
| updateStatus(fid, val); | |
| }); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| UI HELPERS | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function setErr(msg) { | |
| const box = document.getElementById('errBox'); | |
| document.getElementById('errTxt').textContent = msg; | |
| box.classList.toggle('show', !!msg); | |
| } | |
| function setLoading(on) { | |
| const btn = document.getElementById('runBtn'); | |
| const rc = document.getElementById('resultCard'); | |
| btn.disabled = on; | |
| btn.classList.toggle('loading', on); | |
| document.getElementById('btnTxt').textContent = on ? 'Analyzingβ¦' : 'Run Analysis'; | |
| rc.classList.toggle('scanning', on); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| GAUGE | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function animateGauge(prob, level) { | |
| const r = RISK[level] || RISK.low; | |
| const arc = document.getElementById('gFill'); | |
| const out = document.getElementById('gOuter'); | |
| const pct = document.getElementById('gPct'); | |
| arc.style.strokeDashoffset = (CIRC * (1 - prob)).toFixed(2); | |
| arc.style.stroke = r.color; | |
| out.style.strokeDashoffset = (CIRC * (1 - prob)).toFixed(2); | |
| out.style.stroke = r.color; | |
| pct.textContent = `${Math.round(prob * 100)}%`; | |
| pct.style.color = r.light; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SHAP β PLAIN-LANGUAGE HELPERS | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const FEATURE_LABELS = { | |
| 'Glucose': 'Blood Sugar', | |
| 'BMI': 'Body Mass Index', | |
| 'Age': 'Age', | |
| 'Insulin': 'Insulin Level', | |
| 'DiabetesPedigreeFunction': 'Family History Score', | |
| 'BloodPressure': 'Blood Pressure', | |
| 'SkinThickness': 'Skin Thickness', | |
| 'Pregnancies': 'No. of Pregnancies', | |
| 'Glucose_BMI': 'Sugar Γ BMI Combined', | |
| 'Age_BMI': 'Age Γ BMI Combined', | |
| 'Glucose_squared': 'High Blood Sugar Intensity', | |
| 'Insulin_BMI': 'Insulin Γ BMI Combined', | |
| }; | |
| function impactLevel(absVal) { | |
| if (absVal >= 0.30) return { label: 'Strong', cls: 'strong' }; | |
| if (absVal >= 0.10) return { label: 'Moderate', cls: 'moderate' }; | |
| return { label: 'Low', cls: 'low' }; | |
| } | |
| function plainName(feature) { | |
| return FEATURE_LABELS[feature] || feature; | |
| } | |
| function generateSummary(contributions) { | |
| const pos = contributions.filter(c => c.direction === 'increases risk'); | |
| const neg = contributions.filter(c => c.direction !== 'increases risk'); | |
| let s1 = ''; | |
| if (pos.length >= 2) { | |
| s1 = `<strong>${plainName(pos[0].feature)}</strong> and <strong>${plainName(pos[1].feature)}</strong> are the main factors raising this patient's risk.`; | |
| } else if (pos.length === 1) { | |
| s1 = `<strong>${plainName(pos[0].feature)}</strong> is the main factor raising this patient's risk.`; | |
| } else { | |
| s1 = 'No single factor is strongly increasing this patient\'s risk.'; | |
| } | |
| let s2 = ''; | |
| if (neg.length >= 2) { | |
| s2 = `<strong>${plainName(neg[0].feature)}</strong> and <strong>${plainName(neg[1].feature)}</strong> are helping to keep the risk lower.`; | |
| } else if (neg.length === 1) { | |
| s2 = `<strong>${plainName(neg[0].feature)}</strong> is helping to keep the risk lower.`; | |
| } else { | |
| s2 = 'No significant protective factors were identified.'; | |
| } | |
| return s1 + ' ' + s2; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SHAP BARS | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function renderShap(contributions) { | |
| const wrap = document.getElementById('shapBars'); | |
| wrap.innerHTML = ''; | |
| const maxA = Math.max(...contributions.map(c => Math.abs(c.shap_value)), 1e-6); | |
| /* plain-language summary */ | |
| const summaryDiv = document.createElement('div'); | |
| summaryDiv.className = 'shap-summary'; | |
| summaryDiv.innerHTML = generateSummary(contributions); | |
| wrap.appendChild(summaryDiv); | |
| /* legend */ | |
| const legend = document.createElement('div'); | |
| legend.className = 'shap-legend'; | |
| legend.innerHTML = ` | |
| <div class="legend-item"><div class="legend-dot p"></div>Increases risk</div> | |
| <div class="legend-item"><div class="legend-dot n"></div>Decreases risk</div>`; | |
| wrap.appendChild(legend); | |
| /* bars */ | |
| contributions.forEach((c, i) => { | |
| const pos = c.direction === 'increases risk'; | |
| const pct = (Math.abs(c.shap_value) / maxA * 100).toFixed(1); | |
| const absV = Math.abs(c.shap_value); | |
| const impact = impactLevel(absV); | |
| const label = plainName(c.feature); | |
| const sign = pos ? '+' : 'β'; | |
| const row = document.createElement('div'); | |
| row.className = 'sh-row'; | |
| row.style.animationDelay = `${i * 38}ms`; | |
| row.setAttribute('role', 'listitem'); | |
| row.setAttribute('aria-label', `${label}: ${impact.label} impact β ${c.direction}`); | |
| row.title = `${c.feature}: SHAP ${pos ? '+' : ''}${c.shap_value.toFixed(4)}`; | |
| row.innerHTML = ` | |
| <span class="sh-nm">${label}</span> | |
| <div class="sh-track" aria-hidden="true"> | |
| <div class="sh-bar ${pos ? 'p' : 'n'}" style="width:${pct}%"></div> | |
| </div> | |
| <span class="sh-badge ${impact.cls}" title="SHAP ${sign}${absV.toFixed(3)}">${impact.label}</span>`; | |
| wrap.appendChild(row); | |
| }); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SHOW RESULT | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function showResult(data) { | |
| const level = (data.risk_level || 'low').toLowerCase(); | |
| const r = RISK[level] || RISK.low; | |
| /* ECG changes colour to match risk */ | |
| setECGColor(r.ecg); | |
| /* Badge */ | |
| const badge = document.getElementById('rBadge'); | |
| badge.className = `r-badge ${level}`; | |
| badge.innerHTML = ` | |
| <svg width="7" height="7" viewBox="0 0 8 8" aria-hidden="true"> | |
| <circle cx="4" cy="4" r="4" fill="${r.color}"/> | |
| </svg> | |
| ${r.label}`; | |
| /* Gauge */ | |
| animateGauge(data.probability, level); | |
| /* Stats */ | |
| const lbl = document.getElementById('sLabel'); | |
| lbl.textContent = data.prediction; | |
| lbl.style.color = r.light; | |
| document.getElementById('sProb').textContent = `${(data.probability * 100).toFixed(1)}%`; | |
| /* SHAP */ | |
| if (Array.isArray(data.contributions)) renderShap(data.contributions); | |
| /* Reveal */ | |
| document.getElementById('emptyS').style.display = 'none'; | |
| const out = document.getElementById('resOut'); | |
| out.classList.remove('show'); | |
| void out.offsetWidth; | |
| out.classList.add('show'); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MAIN β RUN ANALYSIS | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function runAnalysis() { | |
| setErr(''); | |
| setLoading(true); | |
| const IDS = ['pregnancies','glucose','blood_pressure','skin_thickness','insulin','bmi','dpf','age']; | |
| const API_KEYS = ['pregnancies','glucose','blood_pressure','skin_thickness','insulin','bmi','diabetes_pedigree_function','age']; | |
| const payload = {}; | |
| for (let i = 0; i < IDS.length; i++) { | |
| const v = parseFloat(document.getElementById(IDS[i]).value); | |
| if (isNaN(v)) { | |
| setErr(`"${IDS[i].replace(/_/g,' ')}" is not a valid number.`); | |
| setLoading(false); | |
| return; | |
| } | |
| payload[API_KEYS[i]] = v; | |
| } | |
| try { | |
| const res = await fetch(`${API}/predict/explain`, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify(payload), | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) { setErr(data.error || `Server error (${res.status}).`); setLoading(false); return; } | |
| showResult(data); | |
| } catch { | |
| setErr('Cannot reach the API β ensure Flask is running on localhost:5000.'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |