| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"/> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| <title>VCF Upload β Beltrao Lab Tools</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=Source+Serif+4:ital,wght@0,300;0,400;0,600;1,400&family=DM+Sans:wght@300;400;500&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/> |
| <style> |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} |
| html{font-size:14px;-webkit-font-smoothing:antialiased} |
|
|
| :root{ |
| --bg-primary:#fafaf8;--bg-secondary:#f4f3ef;--bg-card:#ffffff; |
| --text-primary:#1a1a18;--text-secondary:#5a5a52;--text-muted:#9a9a90; |
| --accent:#2d5a8e;--accent-light:#e8f0f8; |
| --score-pathogenic:#c0392b;--score-ambiguous:#d68910;--score-benign:#1e8449; |
| --mech-stability:#922b21;--mech-interface:#1a5276;--mech-pockets:#1e8449;--mech-unassigned:#717d7e; |
| --border:#e0dfd8;--border-strong:#c8c7bf; |
| --font-body:'Source Serif 4',Georgia,serif; |
| --font-ui:'DM Sans',system-ui,sans-serif; |
| --font-mono:'JetBrains Mono','Fira Code',monospace; |
| } |
|
|
| body{font-family:var(--font-ui);background:var(--bg-primary);color:var(--text-primary);min-height:100vh} |
|
|
| /* ββ Header ββ */ |
| .site-header{border-bottom:1px solid var(--border);background:var(--bg-card);position:sticky;top:0;z-index:100} |
| .header-inner{max-width:1100px;margin:0 auto;padding:0 2rem;height:52px;display:flex;align-items:center;justify-content:space-between} |
| .site-logo{font-family:var(--font-ui);font-size:.9rem;font-weight:500;color:var(--text-primary);text-decoration:none;display:flex;align-items:center;gap:.4rem} |
| .logo-mark{color:var(--accent);font-size:.6rem} |
| .logo-name{color:var(--text-secondary)} |
| .logo-divider{color:var(--text-muted)} |
| .logo-tool{color:var(--text-primary);font-weight:500} |
| .site-nav{display:flex;gap:1.8rem;align-items:center} |
| .nav-link{font-family:var(--font-ui);font-size:.82rem;color:var(--text-secondary);text-decoration:none;letter-spacing:.02em;transition:color .15s} |
| .nav-link:hover{color:var(--accent)} |
| .nav-link.active{color:var(--text-primary);font-weight:500} |
|
|
| /* Dropdown */ |
| .nav-dropdown{position:relative} |
| .nav-dropdown-trigger{cursor:pointer;user-select:none} |
| .nav-dropdown-menu{display:none;position:absolute;top:calc(100% + 6px);left:50%;transform:translateX(-50%);background:var(--bg-card);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.08);min-width:220px;padding:.4rem;z-index:200} |
| .nav-dropdown:hover .nav-dropdown-menu{display:block}.nav-dropdown-menu::before{content:'';position:absolute;top:-6px;left:0;right:0;height:6px} |
| .nav-dropdown-item{display:flex;justify-content:space-between;align-items:center;padding:.55rem .8rem;border-radius:5px;text-decoration:none;transition:background .1s} |
| .nav-dropdown-item:not(.disabled):hover{background:var(--bg-secondary)} |
| .nav-dropdown-item.disabled{opacity:.45;cursor:not-allowed;pointer-events:none} |
| .dropdown-item-name{font-family:var(--font-ui);font-size:.83rem;color:var(--text-primary);font-weight:500} |
| .dropdown-item-desc{font-family:var(--font-ui);font-size:.75rem;color:var(--text-muted)} |
| .dropdown-item-badge{font-family:var(--font-ui);font-size:.65rem;color:var(--text-muted);background:var(--bg-secondary);border:1px solid var(--border);padding:.1rem .35rem;border-radius:3px} |
| .nav-dropdown-divider{height:1px;background:var(--border);margin:.3rem .5rem} |
|
|
| /* ββ Footer ββ */ |
| .site-footer{border-top:1px solid var(--border);padding:2rem;margin-top:4rem} |
| .footer-inner{max-width:1100px;margin:0 auto;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem} |
| .footer-left,.footer-right{font-family:var(--font-ui);font-size:.78rem;color:var(--text-muted)} |
| .footer-right{display:flex;gap:1.5rem} |
| .footer-right a{color:var(--text-muted);text-decoration:none} |
| .footer-right a:hover{color:var(--accent)} |
|
|
| /* ββ Layout ββ */ |
| .vcf-main{padding:2rem 0 4rem} |
| .vcf-container{max-width:900px;margin:0 auto;padding:0 2rem} |
|
|
| /* ββ Page header ββ */ |
| .breadcrumb{font-family:var(--font-ui);font-size:.78rem;color:var(--text-muted);display:flex;gap:.5rem;align-items:center;margin-bottom:1rem} |
| .breadcrumb a{color:var(--accent);text-decoration:none} |
| .breadcrumb a:hover{text-decoration:underline} |
| .page-title{font-family:var(--font-body);font-size:2rem;font-weight:300;margin-bottom:.8rem} |
| .page-desc{font-family:var(--font-ui);font-size:.95rem;color:var(--text-secondary);line-height:1.7;max-width:640px;margin-bottom:2rem} |
|
|
| /* ββ Pipeline steps ββ */ |
| .pipeline-steps{display:flex;align-items:center;gap:.6rem;margin-bottom:2rem;flex-wrap:wrap} |
| .pipeline-step{display:flex;align-items:center;gap:.5rem;background:var(--bg-secondary);border:1px solid var(--border);padding:.35rem .8rem;border-radius:4px} |
| .step-num{font-family:var(--font-mono);font-size:.72rem;color:var(--accent);font-weight:500} |
| .step-label{font-family:var(--font-ui);font-size:.78rem;color:var(--text-secondary)} |
| .pipeline-arrow{font-family:var(--font-ui);color:var(--text-muted);font-size:.85rem} |
|
|
| /* ββ Upload section ββ */ |
| .upload-section{display:flex;flex-direction:column;gap:1.2rem} |
| .drop-zone{border:2px dashed var(--border-strong);border-radius:8px;padding:2.5rem;text-align:center;cursor:pointer;transition:border-color .2s,background .2s} |
| .drop-zone:hover,.drop-zone.drag-over{border-color:var(--accent);background:var(--accent-light)} |
| .drop-icon{font-size:2rem;color:var(--text-muted);margin-bottom:.8rem} |
| .drop-title{font-family:var(--font-ui);font-size:.95rem;font-weight:500;color:var(--text-primary);margin-bottom:.3rem} |
| .drop-subtitle{font-family:var(--font-ui);font-size:.82rem;color:var(--text-muted);margin-bottom:1rem} |
| .drop-browse-btn{font-family:var(--font-ui);font-size:.83rem;border:1px solid var(--border-strong);border-radius:4px;padding:.4rem 1rem;background:var(--bg-card);cursor:pointer;color:var(--text-secondary);transition:background .15s} |
| .drop-browse-btn:hover{background:var(--bg-secondary)} |
|
|
| .divider-or{text-align:center;position:relative;margin:.2rem 0} |
| .divider-or::before{content:'';position:absolute;top:50%;left:0;right:0;height:1px;background:var(--border)} |
| .divider-or span{font-family:var(--font-ui);font-size:.78rem;color:var(--text-muted);background:var(--bg-primary);padding:0 .8rem;position:relative} |
|
|
| .vcf-paste-area{width:100%;font-family:var(--font-mono);font-size:.75rem;border:1px solid var(--border);border-radius:6px;padding:.8rem 1rem;background:var(--bg-card);color:var(--text-primary);resize:vertical;line-height:1.5} |
| .vcf-paste-area:focus{outline:none;border-color:var(--accent)} |
| .vcf-paste-area::placeholder{color:var(--text-muted)} |
|
|
| .example-section{display:flex;align-items:center;gap:.8rem} |
| .example-label{font-family:var(--font-ui);font-size:.82rem;color:var(--text-muted)} |
| .example-btn{font-family:var(--font-ui);font-size:.82rem;color:var(--accent);border:none;background:none;cursor:pointer;text-decoration:underline;padding:0} |
|
|
| .options-row{display:flex;gap:1.5rem;align-items:center;flex-wrap:wrap} |
| .option-item{display:flex;align-items:center;gap:.5rem;font-family:var(--font-ui);font-size:.83rem;color:var(--text-secondary);cursor:pointer} |
| .option-select{font-family:var(--font-ui);font-size:.82rem;border:1px solid var(--border);border-radius:4px;padding:.2rem .5rem;background:var(--bg-card);color:var(--text-primary)} |
|
|
| .analyse-btn{font-family:var(--font-ui);font-size:.95rem;font-weight:500;color:#fff;background:var(--accent);border:none;padding:.75rem 2rem;border-radius:4px;cursor:pointer;transition:background .15s;align-self:flex-start} |
| .analyse-btn:hover:not(:disabled){background:#1e4070} |
| .analyse-btn:disabled{opacity:.4;cursor:not-allowed} |
|
|
| .privacy-note{font-family:var(--font-ui);font-size:.75rem;color:var(--text-muted);display:flex;align-items:flex-start;gap:.4rem;line-height:1.5} |
| .privacy-note::before{content:'π';font-size:.8rem;flex-shrink:0;margin-top:.05rem} |
|
|
| /* ββ Progress ββ */ |
| .progress-section{margin-top:2rem;border:1px solid var(--border);border-radius:6px;padding:1.2rem} |
| .progress-header{display:flex;justify-content:space-between;font-family:var(--font-ui);font-size:.85rem;color:var(--text-secondary);margin-bottom:.6rem} |
| .progress-bar-wrap{height:4px;background:var(--bg-secondary);border-radius:2px;overflow:hidden;margin-bottom:.8rem} |
| .progress-bar{height:100%;background:var(--accent);transition:width .3s ease;border-radius:2px} |
| .progress-log{font-family:var(--font-mono);font-size:.72rem;color:var(--text-secondary);max-height:140px;overflow-y:auto;line-height:1.65} |
| .log-line{padding:.05rem 0} |
|
|
| /* ββ Results ββ */ |
| .results-stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-top:2rem;margin-bottom:1.5rem} |
| @media(max-width:700px){.results-stats-grid{grid-template-columns:repeat(2,1fr)}} |
| .stat-card{border:1px solid var(--border);border-radius:6px;padding:1rem;background:var(--bg-card)} |
| .stat-card.highlight{border-color:var(--score-pathogenic);background:#fdf5f5} |
| .stat-val{display:block;font-family:var(--font-mono);font-size:1.6rem;font-weight:500;color:var(--text-primary)} |
| .stat-card.highlight .stat-val{color:var(--score-pathogenic)} |
| .stat-lbl{display:block;font-family:var(--font-ui);font-size:.78rem;color:var(--text-muted);margin-top:.2rem} |
|
|
| .mechanism-breakdown{display:flex;gap:1.5rem;flex-wrap:wrap;margin-bottom:1.2rem;padding:.8rem 1.2rem;background:var(--bg-secondary);border-radius:6px;border:1px solid var(--border)} |
| .mech-stat{display:flex;align-items:center;gap:.5rem} |
| .mech-count{font-family:var(--font-mono);font-size:.9rem;font-weight:500;color:var(--text-primary)} |
|
|
| .results-actions{display:flex;gap:.8rem;margin-bottom:1.2rem;flex-wrap:wrap} |
| .btn-export{font-family:var(--font-ui);font-size:.83rem;font-weight:500;color:#fff;background:var(--accent);border:none;padding:.5rem 1rem;border-radius:4px;cursor:pointer;transition:background .15s} |
| .btn-export:hover{background:#1e4070} |
| .btn-secondary-sm{font-family:var(--font-ui);font-size:.83rem;color:var(--text-secondary);border:1px solid var(--border);background:var(--bg-card);padding:.5rem 1rem;border-radius:4px;cursor:pointer;transition:background .15s} |
| .btn-secondary-sm:hover{background:var(--bg-secondary)} |
|
|
| /* Table */ |
| .vcf-results-table-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:6px} |
| .vcf-results-table{width:100%;border-collapse:collapse;font-family:var(--font-ui);font-size:.82rem} |
| .vcf-results-table th{text-align:left;padding:.6rem .8rem;background:var(--bg-secondary);border-bottom:1px solid var(--border);font-weight:500;color:var(--text-secondary);white-space:nowrap;font-size:.75rem;letter-spacing:.03em} |
| .result-row{border-bottom:1px solid var(--border);transition:background .1s} |
| .result-row:last-child{border-bottom:none} |
| .result-row:hover{background:var(--accent-light)} |
| .result-row td{padding:.6rem .8rem;vertical-align:middle} |
|
|
| .vcf-coord{font-family:var(--font-mono);font-size:.72rem;color:var(--text-secondary);display:block} |
| .vcf-allele{font-family:var(--font-mono);font-size:.78rem;color:var(--text-primary);display:block} |
| .vcf-rsid{font-family:var(--font-mono);font-size:.68rem;color:var(--text-muted);display:block} |
| .protein-id{font-family:var(--font-mono);font-size:.78rem;font-weight:500;color:var(--text-primary);display:block} |
| .gene-symbol{font-family:var(--font-ui);font-size:.72rem;color:var(--text-muted);display:block} |
| .mutation-code{font-family:var(--font-mono);font-size:.82rem;color:var(--text-primary);display:block} |
| .hgvsp{font-family:var(--font-mono);font-size:.68rem;color:var(--text-muted);display:block} |
|
|
| .score-bar-wrap{height:3px;background:var(--bg-secondary);border-radius:2px;width:55px;margin-bottom:.2rem} |
| .score-bar{height:100%;border-radius:2px} |
| .score-val{font-family:var(--font-mono);font-size:.78rem} |
| .score-mono{font-family:var(--font-mono);font-size:.78rem;color:var(--text-secondary)} |
| .score-mono.pathogenic{color:var(--score-pathogenic)} |
| .score-mono.benign{color:var(--score-benign)} |
|
|
| .mech-badge{font-family:var(--font-ui);font-size:.7rem;font-weight:500;padding:.15rem .5rem;border-radius:100px;white-space:nowrap} |
| .mech-badge.stability{background:#fdecea;color:var(--mech-stability)} |
| .mech-badge.interface{background:#e8f0f8;color:var(--mech-interface)} |
| .mech-badge.pockets{background:#e8f5ec;color:var(--mech-pockets)} |
| .mech-badge.unassigned{background:#f0f0ee;color:var(--mech-unassigned)} |
|
|
| .consensus-badge{font-family:var(--font-ui);font-size:.7rem;font-weight:500;padding:.15rem .5rem;border-radius:100px;white-space:nowrap} |
| .consensus-badge.high{background:#fdecea;color:var(--score-pathogenic)} |
| .consensus-badge.medium{background:#fef3e2;color:#b7770d} |
| .consensus-badge.low{background:#e8f5ec;color:var(--score-benign)} |
|
|
| .view-link{font-family:var(--font-ui);font-size:.78rem;color:var(--accent);text-decoration:none;white-space:nowrap} |
| .view-link:hover{text-decoration:underline} |
|
|
| /* Not found */ |
| .not-found-section{margin-top:1.5rem;border:1px solid var(--border);border-radius:6px;padding:1.2rem;background:var(--bg-secondary)} |
| .not-found-section h3{font-family:var(--font-body);font-size:1rem;margin-bottom:.5rem} |
| .not-found-note{font-family:var(--font-ui);font-size:.82rem;color:var(--text-secondary);line-height:1.6;margin-bottom:1rem} |
| .not-found-item{display:flex;gap:1rem;align-items:center;padding:.4rem 0;border-bottom:1px solid var(--border);flex-wrap:wrap} |
| .not-found-item:last-child{border-bottom:none} |
| .not-found-gene{font-family:var(--font-ui);font-size:.82rem;color:var(--text-secondary)} |
| .not-found-reason{font-family:var(--font-ui);font-size:.75rem;color:var(--text-muted)} |
| .not-found-more{font-family:var(--font-ui);font-size:.78rem;color:var(--text-muted);margin-top:.5rem} |
|
|
| /* ββ Risk distribution banner ββ */ |
| .risk-banner{display:flex;gap:.6rem;margin-bottom:1.4rem;flex-wrap:wrap;align-items:stretch} |
| .risk-banner-pill{flex:1;min-width:120px;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:.8rem 1rem;border-radius:6px;border:1px solid transparent;cursor:pointer;transition:filter .15s} |
| .risk-banner-pill:hover{filter:brightness(.95)} |
| .risk-banner-pill.active{outline:2px solid currentColor;outline-offset:-2px} |
| .risk-banner-pill.pill-high{background:#fdf0f0;border-color:#f5c6c6;color:var(--score-pathogenic)} |
| .risk-banner-pill.pill-medium{background:#fef8ec;border-color:#f5e0a0;color:#a06010} |
| .risk-banner-pill.pill-low{background:#f0faf3;border-color:#a8dbb8;color:var(--score-benign)} |
| .risk-banner-pill.pill-all{background:var(--bg-secondary);border-color:var(--border);color:var(--text-secondary)} |
| .risk-banner-count{font-family:var(--font-mono);font-size:1.6rem;font-weight:500;line-height:1} |
| .risk-banner-label{font-family:var(--font-ui);font-size:.72rem;margin-top:.25rem;opacity:.8} |
|
|
| /* ββ Result row left-border risk indicator ββ */ |
| .result-row.risk-high td:first-child{border-left:3px solid var(--score-pathogenic)} |
| .result-row.risk-medium td:first-child{border-left:3px solid #d4a017} |
| .result-row.risk-low td:first-child{border-left:3px solid var(--score-benign)} |
| .result-row.risk-high{background:#fffafa} |
| .result-row.risk-high:hover{background:#fdf0f0} |
| </style> |
| </head> |
| <body> |
|
|
| |
| <header class="site-header"> |
| <div class="header-inner"> |
| <a href="landing.html" class="site-logo"> |
| <span class="logo-mark">β</span> |
| <span class="logo-name">Beltrao Lab</span> |
| <span class="logo-divider">/</span> |
| <span class="logo-tool">MutVar</span> |
| </a> |
| <nav class="site-nav"> |
| <div class="nav-dropdown"> |
| <span class="nav-link nav-dropdown-trigger">Tools βΎ</span> |
| <div class="nav-dropdown-menu"> |
| <a href="index.html" class="nav-dropdown-item"> |
| <span class="dropdown-item-name">MutVar</span> |
| <span class="dropdown-item-desc">Missense variant impact</span> |
| </a> |
| <a href="vcf-upload.html" class="nav-dropdown-item"> |
| <span class="dropdown-item-name">VCF Upload</span> |
| <span class="dropdown-item-desc">Batch variant analysis</span> |
| </a> |
| <div class="nav-dropdown-divider"></div> |
| <a href="kinaseatlas.html" class="nav-dropdown-item"> |
| <span class="dropdown-item-name">KinaseAtlas</span> |
| <span class="dropdown-item-badge">Planned</span> |
| </a> |
| </div> |
| </div> |
| <a href="landing.html#about" class="nav-link">About</a> |
| <a href="https://www.biorxiv.org/content/10.1101/2024.05.29.596373v1" class="nav-link" target="_blank">Preprint β</a> |
| </nav> |
| </div> |
| </header> |
|
|
| <main class="vcf-main"> |
| <div class="vcf-container"> |
|
|
| <div class="page-header" style="margin-bottom:2rem"> |
| <div class="breadcrumb"> |
| <a href="landing.html">Tools</a> |
| <span>β</span> |
| <span>VCF Upload</span> |
| </div> |
| <h1 class="page-title">Upload a VCF file</h1> |
| <p class="page-desc"> |
| Upload a Variant Call Format (VCF) file from your sequencing pipeline. |
| MutVar maps genomic coordinates to protein variants via Ensembl VEP and retrieves |
| precomputed pathogenicity predictions for all missense variants found in our dataset. |
| </p> |
| </div> |
|
|
| <div class="pipeline-steps"> |
| <div class="pipeline-step"><span class="step-num">1</span><span class="step-label">Parse VCF</span></div> |
| <div class="pipeline-arrow">β</div> |
| <div class="pipeline-step"><span class="step-num">2</span><span class="step-label">Map to protein (Ensembl VEP)</span></div> |
| <div class="pipeline-arrow">β</div> |
| <div class="pipeline-step"><span class="step-num">3</span><span class="step-label">Lookup MutVar predictions</span></div> |
| <div class="pipeline-arrow">β</div> |
| <div class="pipeline-step"><span class="step-num">4</span><span class="step-label">Results + export</span></div> |
| </div> |
|
|
| <div class="upload-section"> |
|
|
| <div id="drop-zone" class="drop-zone"> |
| <div class="drop-icon">β</div> |
| <div class="drop-title" id="drop-title">Drop your VCF file here</div> |
| <div class="drop-subtitle" id="drop-subtitle">or click to browse β .vcf and .vcf.gz supported</div> |
| <input type="file" id="vcf-file-input" accept=".vcf,.vcf.gz" style="display:none"/> |
| <button class="drop-browse-btn" onclick="document.getElementById('vcf-file-input').click()">Browse files</button> |
| </div> |
|
|
| <div class="divider-or"><span>or paste VCF content directly</span></div> |
|
|
| <textarea id="vcf-paste-area" class="vcf-paste-area" rows="8" |
| placeholder="##fileformat=VCFv4.2 #CHROM	POS	ID	REF	ALT	QUAL	FILTER	INFO chr11	108490993	.	G	T	.	PASS	."></textarea> |
|
|
| <div class="example-section"> |
| <span class="example-label">Don't have a VCF file?</span> |
| <button id="load-example-btn" class="example-btn">Load example VCF (7 canonical cancer driver mutations)</button> |
| </div> |
|
|
| <div class="options-row"> |
| <label class="option-item"> |
| <input type="checkbox" id="missense-only-cb" checked/> |
| <span>Missense variants only</span> |
| </label> |
| <div class="option-item"> |
| <span>Genome build:</span> |
| <select id="genome-build-select" class="option-select"> |
| <option value="GRCh38" selected>GRCh38 (hg38)</option> |
| <option value="GRCh37">GRCh37 (hg19)</option> |
| </select> |
| </div> |
| </div> |
|
|
| <button id="analyse-btn" class="analyse-btn" disabled>Analyse VCF</button> |
|
|
| <p class="privacy-note"> |
| Your VCF file is processed entirely in your browser. Variant coordinates are sent only |
| to the public <a href="https://rest.ensembl.org" target="_blank" style="color:var(--accent)">Ensembl VEP REST API</a> |
| (EMBL-EBI) for genomic annotation. No data is stored on our servers. |
| </p> |
|
|
| </div> |
|
|
| <div id="progress-section" class="progress-section" style="display:none"> |
| <div class="progress-header"> |
| <span id="progress-title">Processingβ¦</span> |
| <span id="progress-pct">0%</span> |
| </div> |
| <div class="progress-bar-wrap"> |
| <div id="progress-bar" class="progress-bar" style="width:0%"></div> |
| </div> |
| <div id="progress-log" class="progress-log"></div> |
| </div> |
|
|
| <div id="results-section" style="display:none"> |
|
|
| <div class="results-stats-grid"> |
| <div class="stat-card"><span class="stat-val" id="stat-total">β</span><span class="stat-lbl">variants in VCF</span></div> |
| <div class="stat-card"><span class="stat-val" id="stat-missense">β</span><span class="stat-lbl">missense variants</span></div> |
| <div class="stat-card"><span class="stat-val" id="stat-found">β</span><span class="stat-lbl">found in MutVar</span></div> |
| <div class="stat-card highlight"><span class="stat-val" id="stat-high-risk">β</span><span class="stat-lbl">High Risk</span></div> |
| </div> |
|
|
| |
| <div class="risk-banner" id="risk-banner"> |
| <div class="risk-banner-pill pill-all active" data-filter="all" onclick="setRiskFilter('all')"> |
| <span class="risk-banner-count" id="banner-count-all">β</span> |
| <span class="risk-banner-label">All variants</span> |
| </div> |
| <div class="risk-banner-pill pill-high" data-filter="high" onclick="setRiskFilter('high')"> |
| <span class="risk-banner-count" id="banner-count-high">β</span> |
| <span class="risk-banner-label">High Risk (β₯2/3)</span> |
| </div> |
| <div class="risk-banner-pill pill-medium" data-filter="medium" onclick="setRiskFilter('medium')"> |
| <span class="risk-banner-count" id="banner-count-medium">β</span> |
| <span class="risk-banner-label">Medium (1/3)</span> |
| </div> |
| <div class="risk-banner-pill pill-low" data-filter="low" onclick="setRiskFilter('low')"> |
| <span class="risk-banner-count" id="banner-count-low">β</span> |
| <span class="risk-banner-label">Low Risk (0/3)</span> |
| </div> |
| </div> |
|
|
| <div id="mechanism-breakdown" class="mechanism-breakdown"></div> |
|
|
| <div class="results-actions"> |
| <button id="export-csv-btn" class="btn-export">Export CSV</button> |
| <button id="show-not-found-btn" class="btn-secondary-sm"> |
| Show variants not in dataset (<span id="not-found-count">0</span>) |
| </button> |
| </div> |
|
|
| <div class="vcf-results-table-wrap"> |
| <table class="vcf-results-table" id="vcf-results-table"> |
| <thead> |
| <tr> |
| <th>VCF variant</th> |
| <th>Protein</th> |
| <th>Mutation</th> |
| <th>AlphaMissense</th> |
| <th>ESM1b LLR</th> |
| <th>ΞΞG</th> |
| <th>Mechanism</th> |
| <th>Consensus</th> |
| <th></th> |
| </tr> |
| </thead> |
| <tbody id="vcf-results-tbody"></tbody> |
| </table> |
| </div> |
|
|
| <div id="not-found-section" class="not-found-section" style="display:none"> |
| <h3>Variants not in MutVar dataset</h3> |
| <p class="not-found-note"> |
| These missense variants were identified by Ensembl VEP but are not in the MutVar dataset. |
| MutVar currently covers precomputed AlphaMissense, ESM1b, and ΞΞG predictions for |
| human proteins derived from the AlphaMissense proteome-wide release. |
| </p> |
| <div id="not-found-list"></div> |
| </div> |
|
|
| </div> |
| </div> |
| </main> |
|
|
| <footer class="site-footer"> |
| <div class="footer-inner"> |
| <div class="footer-left">Beltrao Lab Β· Institute of Molecular Systems Biology Β· ETH Zurich</div> |
| <div class="footer-right"> |
| <a href="https://imsb.ethz.ch/research/beltrao.html" target="_blank">Lab website β</a> |
| <a href="https://www.biorxiv.org/content/10.1101/2024.05.29.596373v1" target="_blank">Preprint β</a> |
| </div> |
| </div> |
| </footer> |
|
|
| <script> |
| 'use strict'; |
| |
| |
| |
| |
| const API_BASE = (location.hostname === 'localhost' || location.hostname === '127.0.0.1') |
| ? 'http://localhost:8000' : ''; |
| |
| const VEP_BATCH_SIZE = 50; |
| const VEP_RATE_DELAY = 600; |
| |
| |
| |
| const EXAMPLE_VCF = `##fileformat=VCFv4.2 |
| ##reference=GRCh38 |
| ##INFO=<ID=GENE,Number=1,Type=String,Description="Gene symbol"> |
| #CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO |
| 7\t140753336\t.\tA\tT\t100\tPASS\tGENE=BRAF |
| 17\t7674220\t.\tG\tA\t100\tPASS\tGENE=TP53 |
| 17\t7673776\t.\tC\tT\t100\tPASS\tGENE=TP53 |
| 12\t25245350\t.\tC\tT\t100\tPASS\tGENE=KRAS |
| 12\t25245350\t.\tC\tA\t100\tPASS\tGENE=KRAS |
| 2\t208248388\t.\tG\tA\t100\tPASS\tGENE=IDH1 |
| 10\t89692905\t.\tG\tA\t100\tPASS\tGENE=PTEN`; |
| |
| |
| |
| |
| |
| |
| |
| |
| function parseVCF(vcfText) { |
| const variants = []; |
| for (const line of vcfText.split(/\r?\n/)) { |
| if (line.startsWith('#') || !line.trim()) continue; |
| const cols = line.split('\t'); |
| if (cols.length < 5) continue; |
| const [chrom, pos, id, ref, alt] = cols; |
| |
| if (ref.length !== 1 || alt.length !== 1) continue; |
| if (!/^[ACGT]$/i.test(ref) || !/^[ACGT]$/i.test(alt)) continue; |
| variants.push({ |
| chrom: chrom.replace(/^chr/i, ''), |
| pos: parseInt(pos, 10), |
| id: id === '.' ? null : id, |
| ref: ref.toUpperCase(), |
| alt: alt.toUpperCase(), |
| originalChrom: chrom, |
| }); |
| } |
| return variants; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function annotateWithVEP(variants, genomeBuild) { |
| const results = new Map(); |
| const server = genomeBuild === 'GRCh37' |
| ? 'https://grch37.rest.ensembl.org' |
| : 'https://rest.ensembl.org'; |
| |
| const total = variants.length; |
| let done = 0; |
| |
| for (let i = 0; i < total; i += VEP_BATCH_SIZE) { |
| const batch = variants.slice(i, i + VEP_BATCH_SIZE); |
| const batchNum = Math.floor(i / VEP_BATCH_SIZE) + 1; |
| const batchTotal = Math.ceil(total / VEP_BATCH_SIZE); |
| updateProgress( |
| `Annotating with Ensembl VEP (batch ${batchNum}/${batchTotal})β¦`, |
| 10 + Math.round((i / total) * 55) |
| ); |
| |
| const vepVariants = batch.map(v => `${v.chrom} ${v.pos} ${v.id || '.'} ${v.ref} ${v.alt}`); |
| |
| try { |
| const resp = await fetch(`${server}/vep/human/region`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, |
| body: JSON.stringify({ variants: vepVariants, uniprot: 1, hgvs: 1 }) |
| }); |
| |
| if (!resp.ok) { |
| logProgress(`β οΈ VEP API error (batch ${batchNum}): HTTP ${resp.status}`); |
| done += batch.length; |
| continue; |
| } |
| |
| const vepData = await resp.json(); |
| |
| |
| const nWithTC = vepData.filter(r => r.transcript_consequences?.length > 0).length; |
| const nMissense = vepData.filter(r => |
| r.transcript_consequences?.some(tc => tc.consequence_terms?.includes('missense_variant')) |
| ).length; |
| logProgress(` VEP batch ${batchNum}: ${vepData.length} results, ${nWithTC} with transcripts, ${nMissense} missense`); |
| if (nWithTC === 0 && vepData.length > 0) { |
| const sample = vepData[0]; |
| const topConseq = sample.most_severe_consequence || '?'; |
| logProgress(` Sample consequence: ${topConseq} (variant: ${sample.input})`); |
| } |
| |
| let debugCount = 0; |
| for (const result of vepData) { |
| if (!result.transcript_consequences) continue; |
| |
| |
| let best = null, bestScore = -1; |
| for (const tc of result.transcript_consequences) { |
| if (!tc.consequence_terms?.includes('missense_variant')) continue; |
| const aaParts = tc.amino_acids?.split('/'); |
| if (!aaParts || aaParts.length !== 2) continue; |
| const hasSP = (tc.swissprot?.length ?? 0) > 0; |
| const score = (tc.canonical === 1 ? 10 : 0) + (hasSP ? 5 : 0) + (tc.biotype === 'protein_coding' ? 2 : 0); |
| if (score > bestScore) { bestScore = score; best = tc; } |
| } |
| if (!best) continue; |
| |
| |
| if (debugCount < 3) { |
| debugCount++; |
| logProgress(` [dbg] input="${result.input}" pos=${best.protein_position} aa=${best.amino_acids} hgvsp=${best.hgvsp} sp=${best.swissprot}`); |
| } |
| |
| |
| const uniprotId = best.swissprot?.[0]?.split('.')[0] ?? null; |
| |
| |
| let position = parseInt(best.protein_position, 10); |
| if (isNaN(position) && best.protein_position) { |
| |
| const m = String(best.protein_position).match(/^(\d+)/); |
| if (m) position = parseInt(m[1], 10); |
| } |
| if (isNaN(position) && best.hgvsp) { |
| |
| const m = best.hgvsp.match(/[p]\.[A-Za-z*]{1,3}(\d+)/); |
| if (m) position = parseInt(m[1], 10); |
| } |
| if (isNaN(position) || position <= 0) { |
| if (debugCount <= 3) logProgress(` [dbg] skipped: bad position (${best.protein_position})`); |
| continue; |
| } |
| |
| const [aaFrom, aaTo] = best.amino_acids.split('/'); |
| if (!aaFrom || !aaTo || aaFrom === '-' || aaTo === '-') { |
| if (debugCount <= 3) logProgress(` [dbg] skipped: bad amino acids (${best.amino_acids})`); |
| continue; |
| } |
| |
| |
| |
| const inChrom = String(result.seq_region_name || '').replace(/^chr/i, ''); |
| const inPos = result.start; |
| const [inRef, inAlt] = (result.allele_string || '/').split('/'); |
| const variant = batch.find(v => |
| v.chrom === inChrom && |
| v.pos === inPos && |
| v.ref === (inRef || '').toUpperCase() && |
| v.alt === (inAlt || '').toUpperCase() |
| ); |
| if (!variant) { |
| if (debugCount <= 3) logProgress(` [dbg] skipped: no match for ${inChrom}:${inPos} ${inRef}>${inAlt} in batch`); |
| continue; |
| } |
| |
| const key = `${variant.chrom}:${variant.pos}:${variant.ref}:${variant.alt}`; |
| results.set(key, { |
| geneSymbol: best.gene_symbol ?? 'β', |
| uniprotId, |
| aaFrom: aaFrom.toUpperCase(), |
| aaTo: aaTo.toUpperCase(), |
| position, |
| hgvsp: best.hgvsp ?? null, |
| isMissense: true, |
| }); |
| } |
| } catch (err) { |
| logProgress(`β οΈ Network error (batch ${batchNum}): ${err.message}`); |
| } |
| |
| done += batch.length; |
| if (i + VEP_BATCH_SIZE < total) await sleep(VEP_RATE_DELAY); |
| } |
| |
| return results; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function bulkLookupMutVar(vepResults) { |
| const toQuery = []; |
| const seen = new Set(); |
| |
| for (const ann of vepResults.values()) { |
| if (!ann.uniprotId || !ann.position || !ann.aaFrom || !ann.aaTo) continue; |
| const mutation = `${ann.aaFrom}${ann.position}${ann.aaTo}`; |
| const key = `${ann.uniprotId}/${mutation}`; |
| if (!seen.has(key)) { |
| seen.add(key); |
| toQuery.push({ protein_id: ann.uniprotId, mutation }); |
| } |
| } |
| |
| if (toQuery.length === 0) return new Map(); |
| |
| logProgress(`Querying MutVar API for ${toQuery.length} variant${toQuery.length > 1 ? 's' : ''}β¦`); |
| |
| const resp = await fetch(`${API_BASE}/api/lookup`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ variants: toQuery }), |
| }); |
| |
| if (!resp.ok) throw new Error(`MutVar API error: HTTP ${resp.status}`); |
| const { results } = await resp.json(); |
| |
| const index = new Map(); |
| for (const r of results) { |
| index.set(`${r.protein_id}/${r.mutation}`, r.found ? r.data : null); |
| } |
| |
| const nFound = results.filter(r => r.found).length; |
| logProgress(`β ${nFound} / ${results.length} variant${results.length !== 1 ? 's' : ''} found in MutVar`); |
| return index; |
| } |
| |
| |
| |
| |
| async function runAnalysis(vcfText, genomeBuild, missenseOnly) { |
| showProgress(); |
| |
| try { |
| |
| updateProgress('Parsing VCF fileβ¦', 5); |
| const variants = parseVCF(vcfText); |
| logProgress(`β Parsed ${variants.length} SNV${variants.length !== 1 ? 's' : ''} from VCF`); |
| |
| if (variants.length === 0) { |
| logProgress('β οΈ No valid single-nucleotide variants found. Only SNVs can produce missense substitutions.'); |
| return; |
| } |
| if (variants.length > 2000) { |
| logProgress(`β οΈ Large VCF (${variants.length} SNVs). Processing in batches β this may take a few minutes.`); |
| } |
| |
| |
| updateProgress('Annotating with Ensembl VEPβ¦', 10); |
| const vepResults = await annotateWithVEP(variants, genomeBuild); |
| logProgress(`β VEP annotation done: ${vepResults.size} missense variant${vepResults.size !== 1 ? 's' : ''} identified`); |
| |
| |
| updateProgress('Querying MutVar predictionsβ¦', 70); |
| const mutvarIndex = await bulkLookupMutVar(vepResults); |
| |
| |
| updateProgress('Matching resultsβ¦', 90); |
| const matched = []; |
| const notFound = []; |
| |
| for (const v of variants) { |
| const key = `${v.chrom}:${v.pos}:${v.ref}:${v.alt}`; |
| const ann = vepResults.get(key); |
| |
| if (!ann) { |
| if (!missenseOnly) notFound.push({ v, ann: null, reason: 'Not a missense variant' }); |
| continue; |
| } |
| |
| const mutCode = `${ann.aaFrom}${ann.position}${ann.aaTo}`; |
| const row = mutvarIndex.get(`${ann.uniprotId}/${mutCode}`) ?? null; |
| |
| if (row) { |
| |
| const consensusScore = |
| (row.am_label ? 1 : 0) + |
| (row.esm1b_is_pathogenic ? 1 : 0) + |
| (row.pred_ddg_label ? 1 : 0); |
| |
| const hasFull = row.esm1b_llr != null || row.pred_ddg != null; |
| const amScore = parseFloat(row.am_pathogenicity) || 0; |
| const effectiveRisk = hasFull |
| ? consensusScore |
| : (amScore >= 0.8 ? 2 : amScore > 0.564 ? 1 : 0); |
| matched.push({ v, ann, row, mutCode, consensusScore, hasFull, effectiveRisk }); |
| } else { |
| notFound.push({ |
| v, ann, |
| reason: ann.uniprotId |
| ? `${ann.uniprotId} not in MutVar dataset` |
| : 'No Swiss-Prot ID returned by VEP', |
| }); |
| } |
| } |
| |
| logProgress(`β Found ${matched.length} variant${matched.length !== 1 ? 's' : ''} in MutVar`); |
| if (notFound.filter(x => x.ann).length > 0) |
| logProgress(` ${notFound.filter(x => x.ann).length} missense variant(s) not covered in MutVar`); |
| |
| updateProgress('Rendering resultsβ¦', 95); |
| renderResults(variants, matched, notFound); |
| updateProgress('Done.', 100); |
| |
| } catch (err) { |
| logProgress(`β Error: ${err.message}`); |
| console.error(err); |
| } |
| } |
| |
| |
| |
| |
| function renderResults(allVariants, matched, notFound) { |
| document.getElementById('results-section').style.display = 'block'; |
| |
| const missenseTotal = matched.length + notFound.filter(x => x.ann).length; |
| const highRiskCount = matched.filter(m => m.effectiveRisk >= 2).length; |
| const mediumCount = matched.filter(m => m.effectiveRisk === 1).length; |
| const lowCount = matched.filter(m => m.effectiveRisk === 0).length; |
| |
| document.getElementById('stat-total').textContent = allVariants.length; |
| document.getElementById('stat-missense').textContent = missenseTotal; |
| document.getElementById('stat-found').textContent = matched.length; |
| document.getElementById('stat-high-risk').textContent = highRiskCount; |
| |
| |
| document.getElementById('banner-count-all').textContent = matched.length; |
| document.getElementById('banner-count-high').textContent = highRiskCount; |
| document.getElementById('banner-count-medium').textContent = mediumCount; |
| document.getElementById('banner-count-low').textContent = lowCount; |
| |
| |
| const mc = { Stability: 0, Interface: 0, Pockets: 0, Unassigned: 0 }; |
| matched.forEach(m => { const mech = m.row.mechanistic_label || 'Unassigned'; if (mc[mech] !== undefined) mc[mech]++; }); |
| document.getElementById('mechanism-breakdown').innerHTML = |
| Object.entries(mc).map(([mech, count]) => |
| `<div class="mech-stat"><span class="mech-badge ${mech.toLowerCase()}">${mech}</span><span class="mech-count">${count}</span></div>` |
| ).join(''); |
| |
| |
| matched.sort((a, b) => |
| b.effectiveRisk - a.effectiveRisk || |
| parseFloat(b.row.am_pathogenicity) - parseFloat(a.row.am_pathogenicity) |
| ); |
| |
| |
| window._vcfMatched = matched; |
| setRiskFilter('all'); |
| |
| |
| const notFoundMissense = notFound.filter(x => x.ann); |
| |
| document.getElementById('export-csv-btn').onclick = () => exportCSV(matched); |
| document.getElementById('not-found-count').textContent = notFoundMissense.length; |
| document.getElementById('not-found-list').innerHTML = |
| notFoundMissense.slice(0, 50).map(({ v, ann, reason }) => |
| `<div class="not-found-item"> |
| <span class="vcf-coord" style="display:inline;font-size:.78rem">${v.originalChrom}:${v.pos} ${v.ref}β${v.alt}</span> |
| <span class="not-found-gene">${ann?.geneSymbol ?? 'β'}</span> |
| <span class="not-found-reason">${reason}</span> |
| </div>` |
| ).join('') |
| + (notFoundMissense.length > 50 |
| ? `<div class="not-found-more">β¦and ${notFoundMissense.length - 50} more</div>` : ''); |
| |
| } |
| |
| |
| |
| |
| function buildRows(rows) { |
| return rows.map(({ v, ann, row, mutCode, consensusScore, hasFull, effectiveRisk }) => { |
| const am = parseFloat(row.am_pathogenicity); |
| const esm = parseFloat(row.esm1b_llr); |
| const ddg = parseFloat(row.pred_ddg); |
| const amClass = row.am_class || ''; |
| const amColor = { pathogenic: 'var(--score-pathogenic)', ambiguous: 'var(--score-ambiguous)', benign: 'var(--score-benign)' }[amClass] || 'var(--text-secondary)'; |
| const consLabel = effectiveRisk >= 2 ? 'High Risk' |
| : effectiveRisk === 1 ? 'Medium' |
| : 'Low Risk'; |
| const consClass = effectiveRisk >= 2 ? 'high' : effectiveRisk === 1 ? 'medium' : 'low'; |
| const riskRowClass = effectiveRisk >= 2 ? 'risk-high' : effectiveRisk === 1 ? 'risk-medium' : 'risk-low'; |
| const mechClass = (row.mechanistic_label || 'Unassigned').toLowerCase(); |
| const hgvspShort = ann.hgvsp ? (ann.hgvsp.split(':')[1] || '') : ''; |
| const scoreDetail = `AM: ${row.am_label ? 'pathogenic' : 'benign'} Β· ESM1b: ${row.esm1b_is_pathogenic ? 'pathogenic' : 'β'} Β· ΞΞG: ${row.pred_ddg_label ? 'destabilizing' : 'β'}`; |
| |
| return `<tr class="result-row ${riskRowClass}" data-risk="${effectiveRisk >= 2 ? 'high' : effectiveRisk === 1 ? 'medium' : 'low'}"> |
| <td> |
| <span class="vcf-coord">${v.originalChrom}:${v.pos}</span> |
| <span class="vcf-allele">${v.ref}β${v.alt}</span> |
| ${v.id ? `<span class="vcf-rsid">${v.id}</span>` : ''} |
| </td> |
| <td> |
| <span class="protein-id">${ann.uniprotId || 'β'}</span> |
| <span class="gene-symbol">${ann.geneSymbol}</span> |
| </td> |
| <td> |
| <span class="mutation-code">${row.mutation_code}</span> |
| ${hgvspShort ? `<span class="hgvsp">${hgvspShort}</span>` : ''} |
| </td> |
| <td> |
| <div class="score-bar-wrap"><div class="score-bar" style="width:${isNaN(am)?0:am*100}%;background:${amColor}"></div></div> |
| <span class="score-val" style="color:${amColor}">${isNaN(am)?'β':am.toFixed(3)}</span> |
| </td> |
| <td><span class="score-mono ${isNaN(esm)?'':esm<-4?'pathogenic':'benign'}">${isNaN(esm)?'β':esm.toFixed(2)}</span></td> |
| <td><span class="score-mono ${isNaN(ddg)?'':ddg>1.5?'pathogenic':ddg<0?'benign':''}">${isNaN(ddg)?'β':(ddg>0?'+':'')+ddg.toFixed(2)}</span></td> |
| <td><span class="mech-badge ${mechClass}">${row.mechanistic_label || 'Unassigned'}</span></td> |
| <td title="${scoreDetail}"><span class="consensus-badge ${consClass}">${consLabel}</span></td> |
| <td><a href="index.html#q=${ann.uniprotId}+${mutCode}" target="_blank" class="view-link">View β</a></td> |
| </tr>`; |
| }).join(''); |
| } |
| |
| |
| |
| |
| function setRiskFilter(filter) { |
| const matched = window._vcfMatched; |
| if (!matched) return; |
| |
| |
| document.querySelectorAll('.risk-banner-pill').forEach(p => { |
| p.classList.toggle('active', p.dataset.filter === filter); |
| }); |
| |
| const filtered = filter === 'all' ? matched |
| : filter === 'high' ? matched.filter(m => m.effectiveRisk >= 2) |
| : filter === 'medium' ? matched.filter(m => m.effectiveRisk === 1) |
| : matched.filter(m => m.effectiveRisk === 0); |
| |
| const tbody = document.getElementById('vcf-results-tbody'); |
| |
| if (filtered.length === 0) { |
| const label = filter === 'high' ? 'High Risk' : filter === 'medium' ? 'Medium' : 'Low Risk'; |
| tbody.innerHTML = `<tr><td colspan="9" style="text-align:center;padding:2rem;color:var(--text-muted);font-family:var(--font-ui)"> |
| No ${label} variants in this VCF. |
| </td></tr>`; |
| } else { |
| tbody.innerHTML = buildRows(filtered); |
| } |
| } |
| |
| |
| |
| |
| function exportCSV(matched) { |
| const headers = ['vcf_chrom','vcf_pos','vcf_ref','vcf_alt','rsid','gene','uniprot_id','mutation','hgvsp','am_pathogenicity','am_class','esm1b_llr','pred_ddg','mechanistic_label','consensus_score','consensus_label']; |
| const esc = v => `"${String(v ?? '').replace(/"/g,'""')}"`; |
| const rows = matched.map(({ v, ann, row, mutCode, consensusScore, hasFull, effectiveRisk }) => [ |
| v.originalChrom, v.pos, v.ref, v.alt, v.id ?? '', |
| ann.geneSymbol, ann.uniprotId ?? '', mutCode, ann.hgvsp ?? '', |
| row.am_pathogenicity, row.am_class, |
| row.esm1b_llr, row.pred_ddg, |
| row.mechanistic_label, consensusScore, |
| effectiveRisk >= 2 ? 'High Risk' : effectiveRisk === 1 ? 'Moderate/Pathogenic' : 'Low/Benign', |
| ].map(esc).join(',')); |
| |
| const blob = new Blob([[headers.join(','), ...rows].join('\n')], { type: 'text/csv;charset=utf-8;' }); |
| const a = Object.assign(document.createElement('a'), { |
| href: URL.createObjectURL(blob), |
| download: `mutvar_vcf_${new Date().toISOString().slice(0,10)}.csv`, |
| }); |
| a.click(); |
| URL.revokeObjectURL(a.href); |
| } |
| |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| // UI HELPERS |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| function updateProgress(msg, pct) { |
| document.getElementById('progress-title').textContent = msg; |
| document.getElementById('progress-pct').textContent = `${pct}%`; |
| document.getElementById('progress-bar').style.width = `${pct}%`; |
| } |
| function logProgress(msg) { |
| const log = document.getElementById('progress-log'); |
| const div = document.createElement('div'); |
| div.className = 'log-line'; |
| div.textContent = msg; |
| log.appendChild(div); |
| log.scrollTop = log.scrollHeight; |
| } |
| function showProgress() { |
| document.getElementById('progress-section').style.display = 'block'; |
| document.getElementById('results-section').style.display = 'none'; |
| document.getElementById('progress-log').innerHTML = ''; |
| } |
| function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } |
| |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| // EVENTS |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| document.addEventListener('DOMContentLoaded', () => { |
| const dropZone = document.getElementById('drop-zone'); |
| const fileInput = document.getElementById('vcf-file-input'); |
| const pasteArea = document.getElementById('vcf-paste-area'); |
| const analyseBtn = document.getElementById('analyse-btn'); |
| |
| const checkContent = () => { |
| analyseBtn.disabled = pasteArea.value.trim().length < 10; |
| }; |
| pasteArea.addEventListener('input', checkContent); |
| |
| // Drag & drop |
| dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); }); |
| dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over')); |
| dropZone.addEventListener('drop', e => { |
| e.preventDefault(); dropZone.classList.remove('drag-over'); |
| if (e.dataTransfer.files[0]) loadFile(e.dataTransfer.files[0]); |
| }); |
| fileInput.addEventListener('change', () => { if (fileInput.files[0]) loadFile(fileInput.files[0]); }); |
| |
| function loadFile(file) { |
| if (file.name.endsWith('.gz') && typeof DecompressionStream !== 'undefined') { |
| new Response(file.stream().pipeThrough(new DecompressionStream('gzip'))).text() |
| .then(text => { pasteArea.value = text; checkContent(); setDropLabel(file); }) |
| .catch(() => logProgress('β οΈ Could not decompress .vcf.gz. Please convert to .vcf first.')); |
| } else { |
| const reader = new FileReader(); |
| reader.onload = e => { pasteArea.value = e.target.result; checkContent(); setDropLabel(file); }; |
| reader.readAsText(file); |
| } |
| } |
| |
| function setDropLabel(file) { |
| document.getElementById('drop-title').textContent = `β ${file.name}`; |
| document.getElementById('drop-subtitle').textContent = `${(file.size / 1024).toFixed(1)} KB loaded`; |
| } |
| |
| // Example VCF |
| document.getElementById('load-example-btn').addEventListener('click', () => { |
| pasteArea.value = EXAMPLE_VCF; |
| checkContent(); |
| }); |
| |
| // Analyse |
| analyseBtn.addEventListener('click', () => { |
| if (!pasteArea.value.trim()) return; |
| runAnalysis( |
| pasteArea.value, |
| document.getElementById('genome-build-select').value, |
| document.getElementById('missense-only-cb').checked |
| ); |
| }); |
| |
| // Show/hide not-found |
| document.getElementById('show-not-found-btn').addEventListener('click', () => { |
| const s = document.getElementById('not-found-section'); |
| s.style.display = s.style.display === 'none' ? 'block' : 'none'; |
| }); |
| }); |
| </script> |
| </body> |
| </html> |
|
|