Mieaz commited on
Commit
60513e1
·
verified ·
1 Parent(s): 366308c

Upload app.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.js +272 -823
app.js CHANGED
@@ -1,6 +1,7 @@
1
  /**
2
- * MVA Syndrome 5D Genomic Studio — Client Engine
3
- * High-performance browser for Clinical Phenotypes, 5M Whole-Genome Variants, Mosaicism, and WGS Flowcell metrics.
 
4
  */
5
 
6
  (function () {
@@ -15,8 +16,13 @@
15
  chromosomes: null,
16
  wgs: null,
17
  pathway: null,
 
 
 
 
 
18
 
19
- // Filters
20
  selectedGene: 'ALL',
21
  selectedType: 'ALL',
22
  selectedMosaic: 'ALL',
@@ -55,16 +61,35 @@
55
  btnResetChr: document.getElementById('btnResetChr'),
56
  variantTableBody: document.getElementById('variantTableBody'),
57
 
58
- // Tab 3: Mosaicism
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  mosaicListSummary: document.getElementById('mosaicListSummary'),
60
  hetListSummary: document.getElementById('hetListSummary'),
61
  homListSummary: document.getElementById('homListSummary'),
62
  topMosaicTable: document.getElementById('topMosaicTable'),
63
 
64
- // Tab 4: WGS
65
  laneTableBody: document.getElementById('laneTableBody'),
66
 
67
- // Tab 5: Pathway
68
  pathwayComplexes: document.getElementById('pathwayComplexes'),
69
 
70
  // Modals
@@ -88,12 +113,28 @@
88
 
89
  async function loadAllDatasets() {
90
  try {
91
- const [clinicalRes, genesRes, chrRes, wgsRes, pathRes] = await Promise.all([
 
 
 
 
 
 
 
 
 
 
 
92
  fetch('data/clinical_phenotype.json').then(r => r.json()),
93
  fetch('data/candidate_genes_variants.json').then(r => r.json()),
94
  fetch('data/chromosome_stats.json').then(r => r.json()),
95
  fetch('data/wgs_sequencing_metrics.json').then(r => r.json()),
96
- fetch('data/pathway_network.json').then(r => r.json())
 
 
 
 
 
97
  ]);
98
 
99
  state.clinical = clinicalRes;
@@ -101,8 +142,13 @@
101
  state.chromosomes = chrRes;
102
  state.wgs = wgsRes;
103
  state.pathway = pathRes;
 
 
 
 
 
104
 
105
- // Flatten all variants
106
  state.allVariants = [];
107
  Object.entries(state.genesData.genes).forEach(([gname, gentry]) => {
108
  gentry.variants.forEach(v => {
@@ -125,6 +171,11 @@
125
  renderClinicalTab();
126
  renderChromosomeKaryotype();
127
  applyVariantFilters();
 
 
 
 
 
128
  renderMosaicTab();
129
  renderWGSTab();
130
  renderPathwayTab();
@@ -221,7 +272,7 @@
221
 
222
  function renderVariantTable() {
223
  els.variantTableBody.innerHTML = '';
224
- const slice = state.filteredVariants.slice(0, 150); // High-performance virtualization
225
 
226
  if (slice.length === 0) {
227
  els.variantTableBody.innerHTML = `
@@ -264,7 +315,184 @@
264
  }
265
 
266
  // -------------------------------------------------------------
267
- // Tab 3: Mosaicism & VAF Spectrum
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  // -------------------------------------------------------------
269
  function renderMosaicTab() {
270
  const mosaicVars = state.allVariants.filter(v => v.classification === 'Mosaic Somatic Candidate');
@@ -306,7 +534,7 @@
306
  }
307
 
308
  // -------------------------------------------------------------
309
- // Tab 4: WGS Sequencing Metrics
310
  // -------------------------------------------------------------
311
  function renderWGSTab() {
312
  if (!state.wgs) return;
@@ -328,7 +556,7 @@
328
  }
329
 
330
  // -------------------------------------------------------------
331
- // Tab 5: Mitotic Pathway Network
332
  // -------------------------------------------------------------
333
  function renderPathwayTab() {
334
  if (!state.pathway) return;
@@ -398,7 +626,7 @@
398
  };
399
  });
400
 
401
- // Filters
402
  els.geneFilter.onchange = (e) => {
403
  state.selectedGene = e.target.value;
404
  applyVariantFilters();
@@ -425,826 +653,47 @@
425
  applyVariantFilters();
426
  };
427
 
428
- // Modals
429
- els.btnCloseVarModal.onclick = () => els.modalVariant.classList.add('hidden');
430
- els.btnShortcuts.onclick = () => els.modalHelp.classList.remove('hidden');
431
- els.btnCloseHelpModal.onclick = () => els.modalHelp.classList.add('hidden');
432
-
433
- // Keyboard Shortcuts
434
- window.onkeydown = (e) => {
435
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
436
-
437
- switch (e.key) {
438
- case '1':
439
- case '2':
440
- case '3':
441
- case '4':
442
- case '5':
443
- case '6':
444
- case '7':
445
- case '8':
446
- const idx = parseInt(e.key, 10) - 1;
447
- if (els.navTabs[idx]) els.navTabs[idx].click();
448
- break;
449
- case '/':
450
- e.preventDefault();
451
- if (els.navTabs[1]) els.navTabs[1].click();
452
- els.variantSearch.focus();
453
- break;
454
- case 'Escape':
455
- els.modalVariant.classList.add('hidden');
456
- els.modalHelp.classList.add('hidden');
457
- break;
458
- }
459
- };
460
- }
461
-
462
- // -------------------------------------------------------------
463
- // Animation 1: 4D Mitotic Nondisjunction & SAC Failure (7s)
464
- // -------------------------------------------------------------
465
- const mitosisSim = {
466
- canvas: null,
467
- ctx: null,
468
- time: 0.0, // 0.0 to 7.0 seconds
469
- maxTime: 7.0,
470
- isPlaying: true,
471
- speed: 1.0,
472
- scenario: 'MVA', // 'WT' or 'MVA'
473
- lastTimestamp: 0,
474
-
475
- // Chromosomes
476
- chromosomes: [
477
- { name: 'Chr 1', color: '#38bdf8', yOff: -110, lag: false },
478
- { name: 'Chr 4', color: '#38bdf8', yOff: -75, lag: false },
479
- { name: 'Chr 7 (MAD1L1)', color: '#a855f7', yOff: -40, lag: false },
480
- { name: 'Chr 11 (WT1/CEP57)', color: '#fbbf24', yOff: -5, lag: false },
481
- { name: 'Chr 15 (BUB1B)', color: '#f43f5e', yOff: 30, lag: true }, // Lagging
482
- { name: 'Chr 17 (TP53)', color: '#38bdf8', yOff: 65, lag: false },
483
- { name: 'Chr 18 (CEP192)', color: '#ec4899', yOff: 100, lag: false }
484
- ]
485
- };
486
-
487
- function initMitosisAnim() {
488
- mitosisSim.canvas = document.getElementById('mitosisCanvas');
489
- if (!mitosisSim.canvas) return;
490
- mitosisSim.ctx = mitosisSim.canvas.getContext('2d');
491
-
492
- const btnPlay = document.getElementById('btnPlayMitosis');
493
- const btnReset = document.getElementById('btnResetMitosis');
494
- const btnWT = document.getElementById('btnScenWT');
495
- const btnMVA = document.getElementById('btnScenMVA');
496
- const range = document.getElementById('mitosisTimeline');
497
- const speedBtns = document.querySelectorAll('.btn-speed-mitosis');
498
-
499
- if (btnPlay) {
500
- btnPlay.onclick = () => {
501
- mitosisSim.isPlaying = !mitosisSim.isPlaying;
502
- btnPlay.textContent = mitosisSim.isPlaying ? '⏸ Pause' : '▶ Play (7s)';
503
- btnPlay.classList.toggle('playing', mitosisSim.isPlaying);
504
- };
505
  }
506
 
507
- if (btnReset) {
508
- btnReset.onclick = () => {
509
- mitosisSim.time = 0.0;
510
- if (range) range.value = 0;
511
- };
512
  }
513
 
514
- if (btnWT && btnMVA) {
515
- btnWT.onclick = () => {
516
- mitosisSim.scenario = 'WT';
517
- btnWT.classList.add('active');
518
- btnMVA.classList.remove('active');
519
- document.getElementById('mitosisCallout').innerHTML = `<strong>Wild-Type Mechanism:</strong> Intact BUBR1/MAD1L1 complex actively detects tension absence on unattached chromatid 15, arresting anaphase onset until bipolar amphitelic biorientation is achieved. Symmetric 46,XX division results.`;
520
- };
521
- btnMVA.onclick = () => {
522
- mitosisSim.scenario = 'MVA';
523
- btnMVA.classList.add('active');
524
- btnWT.classList.remove('active');
525
- document.getElementById('mitosisCallout').innerHTML = `<strong>Diagnostic Mechanism:</strong> In MVA syndrome, BUBR1/TRIP13/CEP57 deficiency allows premature anaphase firing while chromosome 15 is syntelic. The lagging chromatid fails to segregate and forms an aneuploid micronucleus.`;
526
- };
527
  }
528
 
529
- if (range) {
530
- range.oninput = (e) => {
531
- mitosisSim.time = (parseInt(e.target.value, 10) / 700) * mitosisSim.maxTime;
532
- };
533
  }
534
 
535
- speedBtns.forEach(b => {
536
- b.onclick = () => {
537
- speedBtns.forEach(x => x.classList.remove('active'));
538
- b.classList.add('active');
539
- mitosisSim.speed = parseFloat(b.dataset.speed);
540
- };
541
- });
542
-
543
- requestAnimationFrame(renderMitosisLoop);
544
- }
545
-
546
- function renderMitosisLoop(timestamp) {
547
- if (!mitosisSim.lastTimestamp) mitosisSim.lastTimestamp = timestamp;
548
- const dt = (timestamp - mitosisSim.lastTimestamp) / 1000;
549
- mitosisSim.lastTimestamp = timestamp;
550
-
551
- if (mitosisSim.isPlaying && mitosisSim.ctx) {
552
- mitosisSim.time += dt * mitosisSim.speed;
553
- if (mitosisSim.time > mitosisSim.maxTime) {
554
- mitosisSim.time = 0.0;
555
- }
556
- const range = document.getElementById('mitosisTimeline');
557
- if (range) range.value = Math.floor((mitosisSim.time / mitosisSim.maxTime) * 700);
558
- }
559
-
560
- if (mitosisSim.ctx) {
561
- drawMitosisFrame(mitosisSim.time);
562
- }
563
-
564
- requestAnimationFrame(renderMitosisLoop);
565
- }
566
-
567
- function drawMitosisFrame(t) {
568
- const ctx = mitosisSim.ctx;
569
- const w = mitosisSim.canvas.width;
570
- const h = mitosisSim.canvas.height;
571
- const cx = w / 2;
572
- const cy = h / 2;
573
-
574
- // Clear background
575
- ctx.fillStyle = '#02050c';
576
- ctx.fillRect(0, 0, w, h);
577
-
578
- // Grid pattern
579
- ctx.strokeStyle = 'rgba(30, 45, 74, 0.25)';
580
- ctx.lineWidth = 1;
581
- for (let x = 0; x < w; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); }
582
- for (let y = 0; y < h; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); }
583
-
584
- // Phase Calculation
585
- let phaseName = 'Prophase (Centrosome Migration)';
586
- let sacStatus = 'Active Wait-Anaphase';
587
- let tension = 1.0;
588
- let mccLevel = 90;
589
- let apcLevel = 10;
590
- let karyoA = '46,XX (Diploid)';
591
- let karyoB = '46,XX (Diploid)';
592
-
593
- const isMVA = mitosisSim.scenario === 'MVA';
594
-
595
- if (t < 2.0) {
596
- // Phase 1: Prophase / Prometaphase
597
- phaseName = 'Prophase / Prometaphase (Microtubule Capture)';
598
- sacStatus = 'Active Wait-Anaphase (BUBR1/MAD2 Bound)';
599
- tension = 1.0 + (t / 2.0) * 2.5;
600
- mccLevel = 95 - (t / 2.0) * 10;
601
- apcLevel = 5 + (t / 2.0) * 10;
602
- } else if (t < 4.5) {
603
- // Phase 2: Metaphase Congression
604
- phaseName = 'Metaphase Plate Congression';
605
- tension = 3.5 + ((t - 2.0) / 2.5) * 1.5;
606
- if (isMVA) {
607
- sacStatus = '⚠️ SAC LEAKAGE (Mutant BUBR1 Premature Release)';
608
- mccLevel = 85 - ((t - 2.0) / 2.5) * 60; // Collapses in MVA
609
- apcLevel = 15 + ((t - 2.0) / 2.5) * 75; // Premature firing
610
- } else {
611
- sacStatus = 'SAC ARREST (Syntelic Kinetochore Detected)';
612
- mccLevel = 90;
613
- apcLevel = 15;
614
- }
615
- } else if (t < 6.0) {
616
- // Phase 3: Anaphase
617
- phaseName = isMVA ? '⚡ Premature Anaphase (Lagging Chromatid)' : 'Normal Bipolar Anaphase';
618
- sacStatus = isMVA ? 'Anaphase Fired (Cohesin Cleaved Prematurely)' : 'Bipolar Segregation';
619
- tension = 6.5;
620
- mccLevel = 10;
621
- apcLevel = 95;
622
- if (isMVA) {
623
- karyoA = '47,XX (+15 Trisomy Pole)';
624
- karyoB = '45,XX (-15 Monosomy Pole)';
625
- }
626
- } else {
627
- // Phase 4: Telophase / Micronuclei
628
- phaseName = isMVA ? '💥 Nondisjunction & Micronucleus Entrapment' : 'Euploid Daughter Cells';
629
- sacStatus = isMVA ? 'Aneuploid Segregation Error Complete' : 'Normal Telophase';
630
- tension = 0.5;
631
- mccLevel = 5;
632
- apcLevel = 100;
633
- if (isMVA) {
634
- karyoA = '47,XX (Trisomy 15 clone)';
635
- karyoB = '45,XX (Monosomy 15 + Micronucleus)';
636
- }
637
- }
638
-
639
- // Update HUD labels
640
- document.getElementById('mitosisPhaseName').textContent = phaseName;
641
- document.getElementById('mitosisClock').textContent = `${t.toFixed(2)}s / 7.00s`;
642
- document.getElementById('mitosisSacStatus').textContent = sacStatus;
643
- document.getElementById('mitosisSacStatus').style.color = isMVA && t > 2.0 ? 'var(--accent-red)' : 'var(--accent-gold)';
644
-
645
- document.getElementById('barTension').style.width = `${Math.min(100, (tension / 6.0) * 100)}%`;
646
- document.getElementById('valTension').textContent = `${tension.toFixed(1)} pN`;
647
-
648
- document.getElementById('barMCC').style.width = `${mccLevel}%`;
649
- document.getElementById('valMCC').textContent = `${mccLevel}% Active`;
650
-
651
- document.getElementById('barAPC').style.width = `${apcLevel}%`;
652
- document.getElementById('valAPC').textContent = `${apcLevel}% Activated`;
653
-
654
- document.getElementById('karyoA').textContent = karyoA;
655
- document.getElementById('karyoB').textContent = karyoB;
656
-
657
- // 1. Draw Centrosome Spindle Poles (Left & Right)
658
- const poleX1 = cx - 380 + Math.sin(t) * 2;
659
- const poleX2 = cx + 380 - Math.sin(t) * 2;
660
- const poleY = cy;
661
-
662
- // Centrosome radial astral rays
663
- ctx.save();
664
- for (let pole of [{ x: poleX1, y: poleY }, { x: poleX2, y: poleY }]) {
665
- for (let angle = 0; angle < Math.PI * 2; angle += Math.PI / 8) {
666
- ctx.beginPath();
667
- ctx.moveTo(pole.x, pole.y);
668
- ctx.lineTo(pole.x + Math.cos(angle) * 70, pole.y + Math.sin(angle) * 70);
669
- ctx.strokeStyle = 'rgba(56, 189, 248, 0.25)';
670
- ctx.lineWidth = 1.5;
671
- ctx.stroke();
672
- }
673
-
674
- // Centrosome Core (CEP57 / CEP192 barrel)
675
- ctx.beginPath();
676
- ctx.arc(pole.x, pole.y, 14, 0, Math.PI * 2);
677
- ctx.fillStyle = '#0284c7';
678
- ctx.fill();
679
- ctx.strokeStyle = '#38bdf8';
680
- ctx.lineWidth = 3;
681
- ctx.stroke();
682
-
683
- ctx.fillStyle = '#fff';
684
- ctx.font = '700 9px JetBrains Mono';
685
- ctx.fillText('CENTROSOME', pole.x - 30, pole.y - 20);
686
- ctx.fillStyle = 'rgba(56, 189, 248, 0.8)';
687
- ctx.font = '600 8px JetBrains Mono';
688
- ctx.fillText('(CEP57/192)', pole.x - 24, pole.y + 24);
689
- }
690
- ctx.restore();
691
-
692
- // 2. Draw Cleavage Furrow in Anaphase/Telophase
693
- if (t > 4.5) {
694
- const furrowConstrict = Math.min(1.0, (t - 4.5) / 2.0);
695
- ctx.save();
696
- ctx.strokeStyle = 'rgba(236, 72, 153, 0.6)';
697
- ctx.lineWidth = 2;
698
- ctx.setLineDash([4, 4]);
699
- ctx.beginPath();
700
- ctx.moveTo(cx, 40);
701
- ctx.lineTo(cx, 40 + furrowConstrict * 100);
702
- ctx.moveTo(cx, h - 40);
703
- ctx.lineTo(cx, h - 40 - furrowConstrict * 100);
704
- ctx.stroke();
705
- ctx.fillStyle = 'rgba(236, 72, 153, 0.8)';
706
- ctx.font = '700 9px JetBrains Mono';
707
- ctx.fillText('CLEAVAGE FURROW', cx - 45, 30);
708
- ctx.restore();
709
- }
710
-
711
- // 3. Draw Chromosomes and Spindle K-Fibers
712
- mitosisSim.chromosomes.forEach((chr, i) => {
713
- let chrX1 = cx - 10;
714
- let chrX2 = cx + 10;
715
- let chrY = cy + chr.yOff;
716
-
717
- if (t < 2.0) {
718
- // Congression wandering
719
- const wander = Math.sin(t * 4 + i) * (30 - t * 12);
720
- chrX1 = cx - 12 + wander;
721
- chrX2 = cx + 12 + wander;
722
- } else if (t < 4.5) {
723
- // Metaphase Plate Lock
724
- chrX1 = cx - 8;
725
- chrX2 = cx + 8;
726
- } else {
727
- // Anaphase Pulling
728
- const sepProgress = Math.min(1.0, (t - 4.5) / 2.0);
729
- const pullDist = sepProgress * 260;
730
-
731
- if (chr.lag && isMVA) {
732
- // Lagging Chromosome 15 entrapped in center
733
- chrX1 = cx - 15 + Math.sin(t * 6) * 4;
734
- chrX2 = cx + 15 + Math.sin(t * 6) * 4;
735
- } else {
736
- chrX1 = cx - 8 - pullDist;
737
- chrX2 = cx + 8 + pullDist;
738
- }
739
- }
740
-
741
- // Draw Kinetochore Microtubule Fibers (K-Fibers)
742
- ctx.save();
743
- ctx.beginPath();
744
- ctx.moveTo(poleX1, poleY);
745
- ctx.lineTo(chrX1, chrY);
746
- ctx.strokeStyle = (chr.lag && isMVA && t > 2.0) ? 'rgba(244, 63, 94, 0.7)' : 'rgba(16, 185, 129, 0.7)';
747
- ctx.lineWidth = (chr.lag && isMVA) ? 2.5 : 1.8;
748
- ctx.stroke();
749
-
750
- ctx.beginPath();
751
- ctx.moveTo(poleX2, poleY);
752
- ctx.lineTo(chrX2, chrY);
753
- ctx.strokeStyle = (chr.lag && isMVA && t > 2.0) ? 'rgba(251, 191, 36, 0.5)' : 'rgba(16, 185, 129, 0.7)';
754
- ctx.lineWidth = 1.8;
755
- ctx.stroke();
756
- ctx.restore();
757
-
758
- // Draw Sister Chromatids
759
- ctx.save();
760
- ctx.fillStyle = chr.color;
761
- ctx.shadowColor = chr.color;
762
- ctx.shadowBlur = (chr.lag && isMVA) ? 14 : 6;
763
-
764
- // Left Chromatid
765
- ctx.beginPath();
766
- ctx.roundRect(chrX1 - 10, chrY - 14, 10, 28, 5);
767
- ctx.fill();
768
-
769
- // Right Chromatid
770
- ctx.beginPath();
771
- ctx.roundRect(chrX2, chrY - 14, 10, 28, 5);
772
- ctx.fill();
773
-
774
- // Centromere / Kinetochore Ring
775
- ctx.fillStyle = '#fff';
776
- ctx.beginPath();
777
- ctx.arc(chrX1, chrY, 3, 0, Math.PI * 2);
778
- ctx.arc(chrX2, chrY, 3, 0, Math.PI * 2);
779
- ctx.fill();
780
-
781
- // Label
782
- ctx.shadowBlur = 0;
783
- ctx.fillStyle = '#f1f5f9';
784
- ctx.font = '700 9px JetBrains Mono';
785
- ctx.fillText(chr.name, chrX2 + 16, chrY + 3);
786
-
787
- // Lagging Warning Badge
788
- if (chr.lag && isMVA && t > 4.5) {
789
- ctx.fillStyle = '#f43f5e';
790
- ctx.font = '800 10px JetBrains Mono';
791
- ctx.fillText('⚠️ LAGGING ANEUPLOID CHROMOSOME', cx - 95, chrY + 30);
792
- }
793
-
794
- ctx.restore();
795
- });
796
-
797
- // 4. Draw Entrapped Micronucleus at t > 6.0 in MVA
798
- if (isMVA && t > 6.0) {
799
- ctx.save();
800
- ctx.beginPath();
801
- ctx.arc(cx, cy + 30, 36, 0, Math.PI * 2);
802
- ctx.strokeStyle = '#f43f5e';
803
- ctx.lineWidth = 2;
804
- ctx.setLineDash([3, 3]);
805
- ctx.stroke();
806
- ctx.fillStyle = 'rgba(244, 63, 94, 0.15)';
807
- ctx.fill();
808
-
809
- ctx.fillStyle = '#f43f5e';
810
- ctx.font = '800 10px JetBrains Mono';
811
- ctx.fillText('ISOLATED MICRONUCLEUS', cx - 65, cy + 80);
812
- ctx.fillText('(DNA Damage & Chromothripsis)', cx - 80, cy + 94);
813
- ctx.restore();
814
- }
815
- }
816
-
817
- // -------------------------------------------------------------
818
- // Animation 2: Clonal Variegation & Tissue Dynamics (8s)
819
- // -------------------------------------------------------------
820
- const clonalSim = {
821
- canvas: null,
822
- ctx: null,
823
- time: 0.0,
824
- maxTime: 8.0,
825
- isPlaying: true,
826
- speed: 1.0,
827
- lastTimestamp: 0
828
- };
829
-
830
- function initClonalAnim() {
831
- clonalSim.canvas = document.getElementById('clonalCanvas');
832
- if (!clonalSim.canvas) return;
833
- clonalSim.ctx = clonalSim.canvas.getContext('2d');
834
-
835
- const btnPlay = document.getElementById('btnPlayClonal');
836
- const btnReset = document.getElementById('btnResetClonal');
837
- const range = document.getElementById('clonalTimeline');
838
- const speedBtns = document.querySelectorAll('.btn-speed-clonal');
839
-
840
- if (btnPlay) {
841
- btnPlay.onclick = () => {
842
- clonalSim.isPlaying = !clonalSim.isPlaying;
843
- btnPlay.textContent = clonalSim.isPlaying ? '⏸ Pause' : '▶ Play (8s)';
844
- btnPlay.classList.toggle('playing', clonalSim.isPlaying);
845
- };
846
- }
847
-
848
- if (btnReset) {
849
- btnReset.onclick = () => {
850
- clonalSim.time = 0.0;
851
- if (range) range.value = 0;
852
- };
853
- }
854
-
855
- if (range) {
856
- range.oninput = (e) => {
857
- clonalSim.time = (parseInt(e.target.value, 10) / 800) * clonalSim.maxTime;
858
- };
859
- }
860
-
861
- speedBtns.forEach(b => {
862
- b.onclick = () => {
863
- speedBtns.forEach(x => x.classList.remove('active'));
864
- b.classList.add('active');
865
- clonalSim.speed = parseFloat(b.dataset.speed);
866
- };
867
- });
868
-
869
- requestAnimationFrame(renderClonalLoop);
870
- }
871
-
872
- function renderClonalLoop(timestamp) {
873
- if (!clonalSim.lastTimestamp) clonalSim.lastTimestamp = timestamp;
874
- const dt = (timestamp - clonalSim.lastTimestamp) / 1000;
875
- clonalSim.lastTimestamp = timestamp;
876
-
877
- if (clonalSim.isPlaying && clonalSim.ctx) {
878
- clonalSim.time += dt * clonalSim.speed;
879
- if (clonalSim.time > clonalSim.maxTime) clonalSim.time = 0.0;
880
- const range = document.getElementById('clonalTimeline');
881
- if (range) range.value = Math.floor((clonalSim.time / clonalSim.maxTime) * 800);
882
- }
883
-
884
- if (clonalSim.ctx) {
885
- drawClonalFrame(clonalSim.time);
886
- }
887
-
888
- requestAnimationFrame(renderClonalLoop);
889
- }
890
 
891
- function drawClonalFrame(t) {
892
- const ctx = clonalSim.ctx;
893
- const w = clonalSim.canvas.width;
894
- const h = clonalSim.canvas.height;
895
-
896
- ctx.fillStyle = '#02050c';
897
- ctx.fillRect(0, 0, w, h);
898
-
899
- // Compute Clonal Proportions
900
- let pEuploid = 100.0;
901
- let pTrisomy = 0.0;
902
- let pMonosomy = 0.0;
903
- let pComplex = 0.0;
904
- let stageName = 'Zygote & Cleavage (100% Diploid 46,XX)';
905
- let muscleMosaic = 0;
906
- let kidneyMosaic = 0;
907
-
908
- if (t < 2.0) {
909
- stageName = 'Cleavage / 8-Cell Morula (Diploid Euploid)';
910
- pEuploid = 100.0;
911
- } else if (t < 4.5) {
912
- stageName = 'Blastocyst: Post-Zygotic Somatic Nondisjunction';
913
- const prog = (t - 2.0) / 2.5;
914
- pTrisomy = prog * 18.0;
915
- pMonosomy = prog * 16.0;
916
- pEuploid = 100.0 - (pTrisomy + pMonosomy);
917
- muscleMosaic = Math.round(prog * 12);
918
- kidneyMosaic = Math.round(prog * 15);
919
- } else if (t < 7.0) {
920
- stageName = 'Gastrula: Mesodermal Lineage Trapping';
921
- const prog = (t - 4.5) / 2.5;
922
- pTrisomy = 18.0 + prog * 12.0;
923
- pMonosomy = 16.0 + prog * 14.0;
924
- pComplex = prog * 6.0;
925
- pEuploid = 100.0 - (pTrisomy + pMonosomy + pComplex);
926
- muscleMosaic = Math.round(12 + prog * 26); // Reaches 38%
927
- kidneyMosaic = Math.round(15 + prog * 20); // Reaches 35%
928
- } else {
929
- stageName = 'Organogenesis: Rhabdomyosarcoma & Nephrocalcinosis Foci';
930
- pTrisomy = 30.0;
931
- pMonosomy = 30.0;
932
- pComplex = 6.0;
933
- pEuploid = 34.0;
934
- muscleMosaic = 38;
935
- kidneyMosaic = 35;
936
- }
937
 
938
- // HUD Update
939
- document.getElementById('clonalStageName').textContent = stageName;
940
- document.getElementById('clonalClock').textContent = `${t.toFixed(2)}s / 8.00s`;
941
- document.getElementById('clonalMosaicPct').textContent = `${(100.0 - pEuploid).toFixed(1)}%`;
942
-
943
- document.getElementById('pctEuploid').textContent = `${pEuploid.toFixed(1)}%`;
944
- document.getElementById('pctTrisomy').textContent = `${pTrisomy.toFixed(1)}%`;
945
- document.getElementById('pctMonosomy').textContent = `${pMonosomy.toFixed(1)}%`;
946
- document.getElementById('pctComplex').textContent = `${pComplex.toFixed(1)}%`;
947
-
948
- document.getElementById('barMuscleMosaic').style.width = `${(muscleMosaic / 40) * 100}%`;
949
- document.getElementById('txtMuscleMosaic').textContent = `${muscleMosaic}% VAF Mosaicism (Rhabdomyosarcoma Driver)`;
950
-
951
- document.getElementById('barKidneyMosaic').style.width = `${(kidneyMosaic / 40) * 100}%`;
952
- document.getElementById('txtKidneyMosaic').textContent = `${kidneyMosaic}% VAF Mosaicism (Congenital Nephrocalcinosis)`;
953
-
954
- // Draw Clonal Branching Phylogenetic Tree
955
- ctx.save();
956
- const treeX = 80;
957
- const treeY = 80;
958
-
959
- ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)';
960
- ctx.lineWidth = 3;
961
-
962
- // Root Zygote
963
- ctx.beginPath();
964
- ctx.arc(treeX, h / 2, 22, 0, Math.PI * 2);
965
- ctx.fillStyle = '#0284c7';
966
- ctx.fill();
967
- ctx.stroke();
968
- ctx.fillStyle = '#fff';
969
- ctx.font = '700 10px JetBrains Mono';
970
- ctx.fillText('ZYGOTE', treeX - 18, h / 2 + 4);
971
-
972
- if (t >= 2.0) {
973
- const bProgress = Math.min(1.0, (t - 2.0) / 2.5);
974
-
975
- // Branch 1: Euploid Clone (Blue)
976
- ctx.beginPath();
977
- ctx.moveTo(treeX + 22, h / 2);
978
- ctx.bezierCurveTo(treeX + 150, h / 2, treeX + 250, h / 2 - 120, treeX + 350 * bProgress, h / 2 - 120 * bProgress);
979
- ctx.strokeStyle = '#38bdf8';
980
- ctx.stroke();
981
-
982
- // Branch 2: Trisomy 15 Clone (Red)
983
- ctx.beginPath();
984
- ctx.moveTo(treeX + 22, h / 2);
985
- ctx.bezierCurveTo(treeX + 150, h / 2, treeX + 250, h / 2, treeX + 350 * bProgress, h / 2);
986
- ctx.strokeStyle = '#f43f5e';
987
- ctx.stroke();
988
-
989
- // Branch 3: Monosomy 15 Clone (Yellow)
990
- ctx.beginPath();
991
- ctx.moveTo(treeX + 22, h / 2);
992
- ctx.bezierCurveTo(treeX + 150, h / 2, treeX + 250, h / 2 + 120, treeX + 350 * bProgress, h / 2 + 120 * bProgress);
993
- ctx.strokeStyle = '#fbbf24';
994
- ctx.stroke();
995
-
996
- // Clone Colony Discs
997
- if (t >= 4.5) {
998
- const c1X = treeX + 360;
999
- const c2X = treeX + 360;
1000
- const c3X = treeX + 360;
1001
-
1002
- // Euploid Disc
1003
- ctx.beginPath();
1004
- ctx.arc(c1X, h / 2 - 120, 28, 0, Math.PI * 2);
1005
- ctx.fillStyle = 'rgba(56, 189, 248, 0.2)';
1006
- ctx.fill();
1007
- ctx.strokeStyle = '#38bdf8';
1008
- ctx.stroke();
1009
- ctx.fillStyle = '#38bdf8';
1010
- ctx.fillText('46,XX Normal', c1X + 36, h / 2 - 120);
1011
-
1012
- // Trisomy Disc (Muscle / Rhabdomyosarcoma Focus)
1013
- ctx.beginPath();
1014
- ctx.arc(c2X, h / 2, 34, 0, Math.PI * 2);
1015
- ctx.fillStyle = 'rgba(244, 63, 94, 0.25)';
1016
- ctx.fill();
1017
- ctx.strokeStyle = '#f43f5e';
1018
- ctx.stroke();
1019
- ctx.fillStyle = '#f43f5e';
1020
- ctx.fillText('47,XX (+15 Trisomy)', c2X + 44, h / 2 - 6);
1021
- ctx.fillStyle = '#fca5a5';
1022
- ctx.font = '600 9px JetBrains Mono';
1023
- ctx.fillText('→ Rhabdomyosarcoma Niche (VAF: 38%)', c2X + 44, h / 2 + 10);
1024
-
1025
- // Monosomy Disc (Renal / Nephrocalcinosis Focus)
1026
- ctx.beginPath();
1027
- ctx.arc(c3X, h / 2 + 120, 30, 0, Math.PI * 2);
1028
- ctx.fillStyle = 'rgba(251, 191, 36, 0.25)';
1029
- ctx.fill();
1030
- ctx.strokeStyle = '#fbbf24';
1031
- ctx.stroke();
1032
- ctx.fillStyle = '#fbbf24';
1033
- ctx.font = '700 10px JetBrains Mono';
1034
- ctx.fillText('45,XX (-15 Monosomy)', c3X + 40, h / 2 + 116);
1035
- ctx.fillStyle = '#fde68a';
1036
- ctx.font = '600 9px JetBrains Mono';
1037
- ctx.fillText('→ Congenital Nephrocalcinosis (VAF: 35%)', c3X + 40, h / 2 + 130);
1038
  }
1039
- }
1040
- ctx.restore();
1041
- }
1042
-
1043
- // -------------------------------------------------------------
1044
- // Animation 3: Kinetochore Molecular Machinery (5s)
1045
- // -------------------------------------------------------------
1046
- const kinetoSim = {
1047
- canvas: null,
1048
- ctx: null,
1049
- time: 0.0,
1050
- maxTime: 5.0,
1051
- isPlaying: true,
1052
- speed: 1.0,
1053
- lastTimestamp: 0
1054
- };
1055
-
1056
- function initKinetoAnim() {
1057
- kinetoSim.canvas = document.getElementById('kinetoCanvas');
1058
- if (!kinetoSim.canvas) return;
1059
- kinetoSim.ctx = kinetoSim.canvas.getContext('2d');
1060
-
1061
- const btnPlay = document.getElementById('btnPlayKineto');
1062
- const btnReset = document.getElementById('btnResetKineto');
1063
- const range = document.getElementById('kinetoTimeline');
1064
- const speedBtns = document.querySelectorAll('.btn-speed-kineto');
1065
-
1066
- if (btnPlay) {
1067
- btnPlay.onclick = () => {
1068
- kinetoSim.isPlaying = !kinetoSim.isPlaying;
1069
- btnPlay.textContent = kinetoSim.isPlaying ? '⏸ Pause' : '▶ Play (5s)';
1070
- btnPlay.classList.toggle('playing', kinetoSim.isPlaying);
1071
- };
1072
- }
1073
-
1074
- if (btnReset) {
1075
- btnReset.onclick = () => {
1076
- kinetoSim.time = 0.0;
1077
- if (range) range.value = 0;
1078
- };
1079
- }
1080
-
1081
- if (range) {
1082
- range.oninput = (e) => {
1083
- kinetoSim.time = (parseInt(e.target.value, 10) / 500) * kinetoSim.maxTime;
1084
- };
1085
- }
1086
-
1087
- speedBtns.forEach(b => {
1088
- b.onclick = () => {
1089
- speedBtns.forEach(x => x.classList.remove('active'));
1090
- b.classList.add('active');
1091
- kinetoSim.speed = parseFloat(b.dataset.speed);
1092
- };
1093
- });
1094
-
1095
- requestAnimationFrame(renderKinetoLoop);
1096
- }
1097
-
1098
- function renderKinetoLoop(timestamp) {
1099
- if (!kinetoSim.lastTimestamp) kinetoSim.lastTimestamp = timestamp;
1100
- const dt = (timestamp - kinetoSim.lastTimestamp) / 1000;
1101
- kinetoSim.lastTimestamp = timestamp;
1102
-
1103
- if (kinetoSim.isPlaying && kinetoSim.ctx) {
1104
- kinetoSim.time += dt * kinetoSim.speed;
1105
- if (kinetoSim.time > kinetoSim.maxTime) kinetoSim.time = 0.0;
1106
- const range = document.getElementById('kinetoTimeline');
1107
- if (range) range.value = Math.floor((kinetoSim.time / kinetoSim.maxTime) * 500);
1108
- }
1109
-
1110
- if (kinetoSim.ctx) {
1111
- drawKinetoFrame(kinetoSim.time);
1112
- }
1113
-
1114
- requestAnimationFrame(renderKinetoLoop);
1115
- }
1116
-
1117
- function drawKinetoFrame(t) {
1118
- const ctx = kinetoSim.ctx;
1119
- const w = kinetoSim.canvas.width;
1120
- const h = kinetoSim.canvas.height;
1121
- const cy = h / 2;
1122
-
1123
- ctx.fillStyle = '#02050c';
1124
- ctx.fillRect(0, 0, w, h);
1125
-
1126
- // Stepping Physics
1127
- const stepCycle = (t % 1.0) / 1.0;
1128
- const stepCount = Math.floor(t);
1129
- const displacementNm = (stepCount * 8.0 + stepCycle * 8.0).toFixed(1);
1130
- const pullForce = (3.5 + Math.sin(t * 3) * 1.5).toFixed(1);
1131
- const atpRate = (12.0 + Math.cos(t * 4) * 4.0).toFixed(1);
1132
-
1133
- document.getElementById('kinetoClock').textContent = `${t.toFixed(2)}s / 5.00s`;
1134
- document.getElementById('valDisplacement').textContent = `${displacementNm} nm / 40 nm`;
1135
- document.getElementById('barStepDisplacement').style.width = `${(parseFloat(displacementNm) / 40.0) * 100}%`;
1136
-
1137
- document.getElementById('valPullForce').textContent = `${pullForce} pN`;
1138
- document.getElementById('barPullForce').style.width = `${(parseFloat(pullForce) / 7.0) * 100}%`;
1139
-
1140
- document.getElementById('valAtpRate').textContent = `${atpRate} ATP / sec`;
1141
- document.getElementById('barAtpRate').style.width = `${(parseFloat(atpRate) / 20.0) * 100}%`;
1142
-
1143
- // 1. Draw Microtubule Lattice (Tubulin Dimers)
1144
- const mtY = cy + 80;
1145
- ctx.save();
1146
- for (let x = 60; x < w - 60; x += 32) {
1147
- // Alpha-tubulin
1148
- ctx.beginPath();
1149
- ctx.arc(x, mtY, 12, 0, Math.PI * 2);
1150
- ctx.fillStyle = '#0369a1';
1151
- ctx.fill();
1152
- ctx.strokeStyle = '#38bdf8';
1153
- ctx.stroke();
1154
-
1155
- // Beta-tubulin
1156
- ctx.beginPath();
1157
- ctx.arc(x + 16, mtY, 12, 0, Math.PI * 2);
1158
- ctx.fillStyle = '#0284c7';
1159
- ctx.fill();
1160
- ctx.strokeStyle = '#7dd3fc';
1161
- ctx.stroke();
1162
- }
1163
- ctx.fillStyle = '#38bdf8';
1164
- ctx.font = '700 10px JetBrains Mono';
1165
- ctx.fillText('MICROTUBULE PROTOFILAMENT (α/β-Tubulin Dimers, 8.0 nm period)', 60, mtY + 36);
1166
- ctx.restore();
1167
-
1168
- // 2. Draw CENPE Kinesin Motor Stepping along Protofilament
1169
- const motorBaseX = 140 + (parseFloat(displacementNm) / 40.0) * 550;
1170
- const motorY = mtY - 14;
1171
-
1172
- ctx.save();
1173
- // Motor Heads
1174
- ctx.beginPath();
1175
- ctx.arc(motorBaseX, motorY, 10, 0, Math.PI * 2);
1176
- ctx.arc(motorBaseX + 16, motorY - Math.abs(Math.sin(stepCycle * Math.PI)) * 14, 10, 0, Math.PI * 2);
1177
- ctx.fillStyle = '#fbbf24';
1178
- ctx.fill();
1179
- ctx.strokeStyle = '#fef08a';
1180
- ctx.lineWidth = 2;
1181
- ctx.stroke();
1182
-
1183
- // Stalk connecting to Kinetochore Core
1184
- ctx.beginPath();
1185
- ctx.moveTo(motorBaseX + 8, motorY - 10);
1186
- ctx.lineTo(motorBaseX + 8, cy - 60);
1187
- ctx.strokeStyle = 'rgba(251, 191, 36, 0.8)';
1188
- ctx.lineWidth = 4;
1189
- ctx.stroke();
1190
- ctx.fillStyle = '#fbbf24';
1191
- ctx.font = '800 10px JetBrains Mono';
1192
- ctx.fillText('CENPE Kinesin-7 Motor Head (8.0 nm Hand-over-Hand Step)', motorBaseX - 40, motorY - 26);
1193
- ctx.restore();
1194
-
1195
- // 3. Draw Outer Kinetochore Scaffold & BUBR1 Kinase Complex
1196
- const coreX = motorBaseX + 8;
1197
- const coreY = cy - 80;
1198
-
1199
- ctx.save();
1200
- // Outer Plate (Ndc80 / KMN Network)
1201
- ctx.beginPath();
1202
- ctx.roundRect(coreX - 70, coreY - 20, 140, 40, 8);
1203
- ctx.fillStyle = 'rgba(236, 72, 153, 0.2)';
1204
- ctx.fill();
1205
- ctx.strokeStyle = '#ec4899';
1206
- ctx.lineWidth = 2;
1207
- ctx.stroke();
1208
-
1209
- ctx.fillStyle = '#fff';
1210
- ctx.font = '800 10px JetBrains Mono';
1211
- ctx.fillText('OUTER KINETOCHORE (Ndc80/KMN)', coreX - 60, coreY);
1212
-
1213
- // BUBR1 Phosphorylation Glow
1214
- if (Math.sin(t * 8) > 0) {
1215
- ctx.beginPath();
1216
- ctx.arc(coreX - 45, coreY + 30, 12, 0, Math.PI * 2);
1217
- ctx.fillStyle = '#f43f5e';
1218
- ctx.shadowColor = '#f43f5e';
1219
- ctx.shadowBlur = 12;
1220
- ctx.fill();
1221
- ctx.fillStyle = '#fff';
1222
- ctx.font = '700 8px JetBrains Mono';
1223
- ctx.fillText('BUBR1-P', coreX - 62, coreY + 33);
1224
- }
1225
-
1226
- // TRIP13 AAA+ Hexamer Ring
1227
- ctx.beginPath();
1228
- ctx.arc(coreX + 45, coreY + 30, 14, 0, Math.PI * 2);
1229
- ctx.fillStyle = 'rgba(168, 85, 247, 0.3)';
1230
- ctx.strokeStyle = '#a855f7';
1231
- ctx.lineWidth = 2;
1232
- ctx.fill();
1233
- ctx.stroke();
1234
- ctx.fillStyle = '#d8b4fe';
1235
- ctx.font = '700 8px JetBrains Mono';
1236
- ctx.fillText('TRIP13', coreX + 32, coreY + 33);
1237
- ctx.restore();
1238
  }
1239
 
1240
- // -------------------------------------------------------------
1241
- // App Bootstrapper
1242
- // -------------------------------------------------------------
1243
- window.addEventListener('DOMContentLoaded', () => {
1244
- init();
1245
- initMitosisAnim();
1246
- initClonalAnim();
1247
- initKinetoAnim();
1248
- });
1249
  })();
1250
-
 
1
  /**
2
+ * MVA Syndrome Multi-Dimensional Genomic Studio — Client Engine
3
+ * High-performance browser for Clinical Phenotypes, 5M WGS Variants, Mosaicism,
4
+ * Candidate Gene Burden, Syndromic Differentials, and Mitotic Interactome Matrix Tables.
5
  */
6
 
7
  (function () {
 
16
  chromosomes: null,
17
  wgs: null,
18
  pathway: null,
19
+ geneBurden: null,
20
+ syndromicDiff: null,
21
+ mosaicStrat: null,
22
+ mitoticMachinery: null,
23
+ karyotypeInstability: null,
24
 
25
+ // Variant Filters
26
  selectedGene: 'ALL',
27
  selectedType: 'ALL',
28
  selectedMosaic: 'ALL',
 
61
  btnResetChr: document.getElementById('btnResetChr'),
62
  variantTableBody: document.getElementById('variantTableBody'),
63
 
64
+ // Tab 3: Gene Burden Matrix
65
+ geneBurdenTableBody: document.getElementById('geneBurdenTableBody'),
66
+ searchGeneBurden: document.getElementById('searchGeneBurden'),
67
+
68
+ // Tab 4: Syndromic Differential Matrix
69
+ syndromicDiffTableBody: document.getElementById('syndromicDiffTableBody'),
70
+ searchSyndromicDiff: document.getElementById('searchSyndromicDiff'),
71
+
72
+ // Tab 5: Mosaic Allelic Stratification
73
+ mosaicStratTableBody: document.getElementById('mosaicStratTableBody'),
74
+
75
+ // Tab 6: Mitotic Machinery Interactome
76
+ mitoticMachineryTableBody: document.getElementById('mitoticMachineryTableBody'),
77
+ searchMitoticMachinery: document.getElementById('searchMitoticMachinery'),
78
+
79
+ // Tab 7: 24-Chromosome Karyotype Matrix
80
+ karyotypeMatrixTableBody: document.getElementById('karyotypeMatrixTableBody'),
81
+ searchKaryotypeMatrix: document.getElementById('searchKaryotypeMatrix'),
82
+
83
+ // Tab 8: Mosaicism Spectrum
84
  mosaicListSummary: document.getElementById('mosaicListSummary'),
85
  hetListSummary: document.getElementById('hetListSummary'),
86
  homListSummary: document.getElementById('homListSummary'),
87
  topMosaicTable: document.getElementById('topMosaicTable'),
88
 
89
+ // Tab 9: WGS
90
  laneTableBody: document.getElementById('laneTableBody'),
91
 
92
+ // Tab 10: Pathway
93
  pathwayComplexes: document.getElementById('pathwayComplexes'),
94
 
95
  // Modals
 
113
 
114
  async function loadAllDatasets() {
115
  try {
116
+ const [
117
+ clinicalRes,
118
+ genesRes,
119
+ chrRes,
120
+ wgsRes,
121
+ pathRes,
122
+ geneBurdenRes,
123
+ syndromicDiffRes,
124
+ mosaicStratRes,
125
+ mitoticMachineryRes,
126
+ karyoInstabilityRes
127
+ ] = await Promise.all([
128
  fetch('data/clinical_phenotype.json').then(r => r.json()),
129
  fetch('data/candidate_genes_variants.json').then(r => r.json()),
130
  fetch('data/chromosome_stats.json').then(r => r.json()),
131
  fetch('data/wgs_sequencing_metrics.json').then(r => r.json()),
132
+ fetch('data/pathway_network.json').then(r => r.json()),
133
+ fetch('data/gene_burden_matrix.json').then(r => r.json()),
134
+ fetch('data/syndromic_differential_matrix.json').then(r => r.json()),
135
+ fetch('data/mosaic_stratification_matrix.json').then(r => r.json()),
136
+ fetch('data/mitotic_machinery_matrix.json').then(r => r.json()),
137
+ fetch('data/karyotype_instability_matrix.json').then(r => r.json())
138
  ]);
139
 
140
  state.clinical = clinicalRes;
 
142
  state.chromosomes = chrRes;
143
  state.wgs = wgsRes;
144
  state.pathway = pathRes;
145
+ state.geneBurden = geneBurdenRes;
146
+ state.syndromicDiff = syndromicDiffRes;
147
+ state.mosaicStrat = mosaicStratRes;
148
+ state.mitoticMachinery = mitoticMachineryRes;
149
+ state.karyotypeInstability = karyoInstabilityRes;
150
 
151
+ // Flatten all candidate variants
152
  state.allVariants = [];
153
  Object.entries(state.genesData.genes).forEach(([gname, gentry]) => {
154
  gentry.variants.forEach(v => {
 
171
  renderClinicalTab();
172
  renderChromosomeKaryotype();
173
  applyVariantFilters();
174
+ renderGeneBurdenTable();
175
+ renderSyndromicDiffTable();
176
+ renderMosaicStratTable();
177
+ renderMitoticMachineryTable();
178
+ renderKaryotypeMatrixTable();
179
  renderMosaicTab();
180
  renderWGSTab();
181
  renderPathwayTab();
 
272
 
273
  function renderVariantTable() {
274
  els.variantTableBody.innerHTML = '';
275
+ const slice = state.filteredVariants.slice(0, 150);
276
 
277
  if (slice.length === 0) {
278
  els.variantTableBody.innerHTML = `
 
315
  }
316
 
317
  // -------------------------------------------------------------
318
+ // Tab 3: Candidate Gene Burden Matrix
319
+ // -------------------------------------------------------------
320
+ function renderGeneBurdenTable(filter = '') {
321
+ if (!state.geneBurden) return;
322
+ els.geneBurdenTableBody.innerHTML = '';
323
+ let genes = state.geneBurden.genes || [];
324
+
325
+ if (filter.trim() !== '') {
326
+ const q = filter.toLowerCase().trim();
327
+ genes = genes.filter(g =>
328
+ g.gene.toLowerCase().includes(q) ||
329
+ g.syndrome.toLowerCase().includes(q) ||
330
+ g.complex.toLowerCase().includes(q) ||
331
+ g.role.toLowerCase().includes(q) ||
332
+ g.clinical_link.toLowerCase().includes(q)
333
+ );
334
+ }
335
+
336
+ genes.forEach(g => {
337
+ const tr = document.createElement('tr');
338
+ let tierBadge = `<span class="badge-tier2">${g.pathogenicity_tier}</span>`;
339
+ if (g.pathogenicity_tier.includes('Tier 1')) {
340
+ tierBadge = `<span class="badge-tier1">${g.pathogenicity_tier}</span>`;
341
+ } else if (g.pathogenicity_tier.includes('Tier 3')) {
342
+ tierBadge = `<span class="badge-tier3">${g.pathogenicity_tier}</span>`;
343
+ }
344
+
345
+ tr.innerHTML = `
346
+ <td><strong style="color:var(--primary-cyan); font-size: 13px;">${g.gene}</strong></td>
347
+ <td><span class="font-mono" style="color:var(--text-muted);">${g.omim}</span><br><strong style="font-size:11px; color:var(--text-dim);">${g.cytoband}</strong></td>
348
+ <td><strong style="color:#fff;">${g.syndrome}</strong></td>
349
+ <td><span style="color:var(--primary-cyan); font-weight:600;">${g.complex}</span><br><span style="font-size:11px; color:var(--text-muted);">${g.role}</span></td>
350
+ <td><span style="font-size:11px; color:var(--text-main);">${g.inheritance}</span></td>
351
+ <td class="font-mono"><strong>${g.patient_variants}</strong></td>
352
+ <td class="font-mono"><strong style="color:${g.highest_vaf.includes('Mosaic') ? 'var(--accent-gold)' : 'var(--text-main)'};">${g.highest_vaf}</strong></td>
353
+ <td class="font-mono">${g.mosaic_candidates > 0 ? `<span class="tag-mosaic">${g.mosaic_candidates} loci</span>` : '0'}</td>
354
+ <td style="font-size:11px; color:var(--text-muted);">${g.clinical_link}</td>
355
+ <td>${tierBadge}</td>
356
+ `;
357
+ els.geneBurdenTableBody.appendChild(tr);
358
+ });
359
+ }
360
+
361
+ // -------------------------------------------------------------
362
+ // Tab 4: Phenotype Differential Matrix
363
+ // -------------------------------------------------------------
364
+ function renderSyndromicDiffTable(filter = '') {
365
+ if (!state.syndromicDiff) return;
366
+ els.syndromicDiffTableBody.innerHTML = '';
367
+ let phenos = state.syndromicDiff.phenotypes || [];
368
+
369
+ if (filter.trim() !== '') {
370
+ const q = filter.toLowerCase().trim();
371
+ phenos = phenos.filter(p =>
372
+ p.feature.toLowerCase().includes(q) ||
373
+ p.hpo.toLowerCase().includes(q) ||
374
+ p.competing_syndromes.toLowerCase().includes(q) ||
375
+ p.mva_genes.toLowerCase().includes(q) ||
376
+ p.molecular_mechanism.toLowerCase().includes(q)
377
+ );
378
+ }
379
+
380
+ phenos.forEach(p => {
381
+ const tr = document.createElement('tr');
382
+ tr.innerHTML = `
383
+ <td>
384
+ <strong style="color:var(--text-main); font-size:13px;">${p.feature}</strong><br>
385
+ <span class="hpo-code">${p.hpo}</span>
386
+ </td>
387
+ <td><span class="badge-mva-match">${p.mva_syndrome_match}</span></td>
388
+ <td><strong style="color:var(--primary-cyan); font-family:var(--font-mono);">${p.mva_genes}</strong></td>
389
+ <td style="font-size:11px; color:var(--text-muted);">${p.competing_syndromes}</td>
390
+ <td style="font-size:11px; line-height:1.45; color:#e2e8f0;">${p.molecular_mechanism}</td>
391
+ <td><span class="badge-patho">${p.discriminatory_power}</span></td>
392
+ `;
393
+ els.syndromicDiffTableBody.appendChild(tr);
394
+ });
395
+ }
396
+
397
+ // -------------------------------------------------------------
398
+ // Tab 5: Mosaic Allelic Stratification
399
+ // -------------------------------------------------------------
400
+ function renderMosaicStratTable() {
401
+ if (!state.mosaicStrat) return;
402
+ els.mosaicStratTableBody.innerHTML = '';
403
+ state.mosaicStrat.bins.forEach(b => {
404
+ const tr = document.createElement('tr');
405
+ let vafStyle = 'color:var(--text-main);';
406
+ if (b.vaf_range.includes('5.0%') || b.vaf_range.includes('15.1%') || b.vaf_range.includes('28.1%')) {
407
+ vafStyle = 'color:var(--accent-gold); font-weight:800;';
408
+ }
409
+
410
+ tr.innerHTML = `
411
+ <td><strong class="font-mono" style="${vafStyle}">${b.vaf_range}</strong></td>
412
+ <td><strong style="color:#fff;">${b.category}</strong><br><span style="font-size:10px; color:var(--text-dim);">${b.inferred_timing}</span></td>
413
+ <td class="font-mono"><strong>${b.variant_count}</strong></td>
414
+ <td class="font-mono">${b.mean_dp}</td>
415
+ <td><strong style="color:var(--primary-cyan);">${b.tissue_penetrance}</strong></td>
416
+ <td><span style="font-family:var(--font-mono); color:var(--text-main);">${b.key_genes_affected}</span></td>
417
+ <td style="font-size:11px; color:var(--text-muted);">${b.biological_impact}</td>
418
+ `;
419
+ els.mosaicStratTableBody.appendChild(tr);
420
+ });
421
+ }
422
+
423
+ // -------------------------------------------------------------
424
+ // Tab 6: Mitotic Machinery & Interactome Functional Table
425
+ // -------------------------------------------------------------
426
+ function renderMitoticMachineryTable(filter = '') {
427
+ if (!state.mitoticMachinery) return;
428
+ els.mitoticMachineryTableBody.innerHTML = '';
429
+ let proteins = state.mitoticMachinery.proteins || [];
430
+
431
+ if (filter.trim() !== '') {
432
+ const q = filter.toLowerCase().trim();
433
+ proteins = proteins.filter(p =>
434
+ p.protein.toLowerCase().includes(q) ||
435
+ p.uniprot.toLowerCase().includes(q) ||
436
+ p.complex.toLowerCase().includes(q) ||
437
+ p.mva_type.toLowerCase().includes(q) ||
438
+ p.biochemical_function.toLowerCase().includes(q)
439
+ );
440
+ }
441
+
442
+ proteins.forEach(p => {
443
+ const tr = document.createElement('tr');
444
+ tr.innerHTML = `
445
+ <td>
446
+ <strong style="color:var(--primary-cyan); font-size:13px;">${p.protein}</strong><br>
447
+ <span class="font-mono" style="font-size:10px; color:var(--text-dim);">UniProt: ${p.uniprot}</span>
448
+ </td>
449
+ <td class="font-mono">${p.length_aa} aa</td>
450
+ <td style="font-size:11px; color:var(--text-main);">${p.subcellular_location}</td>
451
+ <td style="font-size:11px; color:#e2e8f0; line-height:1.4;">${p.biochemical_function}</td>
452
+ <td><span style="color:var(--primary-cyan); font-weight:600; font-size:11px;">${p.complex}</span></td>
453
+ <td><span class="badge-patho">${p.mva_type}</span></td>
454
+ <td style="font-size:11px; color:var(--accent-gold); font-family:var(--font-mono);">${p.proband_findings}</td>
455
+ `;
456
+ els.mitoticMachineryTableBody.appendChild(tr);
457
+ });
458
+ }
459
+
460
+ // -------------------------------------------------------------
461
+ // Tab 7: 24-Chromosome Karyotype Matrix
462
+ // -------------------------------------------------------------
463
+ function renderKaryotypeMatrixTable(filter = '') {
464
+ if (!state.karyotypeInstability) return;
465
+ els.karyotypeMatrixTableBody.innerHTML = '';
466
+ let chrs = state.karyotypeInstability.chromosomes || [];
467
+
468
+ if (filter.trim() !== '') {
469
+ const q = filter.toLowerCase().trim();
470
+ chrs = chrs.filter(c =>
471
+ c.chr.toLowerCase().includes(q) ||
472
+ c.aneuploidy_vulnerability.toLowerCase().includes(q)
473
+ );
474
+ }
475
+
476
+ chrs.forEach(c => {
477
+ const tr = document.createElement('tr');
478
+ const isMvaKey = ['chr15', 'chr11', 'chr5', 'chr18', 'chr7', 'chr8'].includes(c.chr);
479
+ tr.innerHTML = `
480
+ <td><strong style="color:${isMvaKey ? 'var(--accent-gold)' : 'var(--primary-cyan)'}; font-size:13px;">${c.chr}</strong></td>
481
+ <td class="font-mono">${c.length_bp}</td>
482
+ <td class="font-mono"><strong>${c.total_variants}</strong></td>
483
+ <td class="font-mono">${c.snvs}</td>
484
+ <td class="font-mono">${c.indels}</td>
485
+ <td class="font-mono"><strong>${c.density_per_mb}</strong></td>
486
+ <td class="font-mono">${c.snv_indel_ratio}</td>
487
+ <td class="font-mono" style="font-size:11px;">${c.heterozygous} / ${c.homozygous}</td>
488
+ <td style="font-size:11px; color:${isMvaKey ? '#fef08a' : 'var(--text-muted)'}; font-weight:${isMvaKey ? '600' : '400'};">${c.aneuploidy_vulnerability}</td>
489
+ `;
490
+ els.karyotypeMatrixTableBody.appendChild(tr);
491
+ });
492
+ }
493
+
494
+ // -------------------------------------------------------------
495
+ // Tab 8: Mosaicism Spectrum
496
  // -------------------------------------------------------------
497
  function renderMosaicTab() {
498
  const mosaicVars = state.allVariants.filter(v => v.classification === 'Mosaic Somatic Candidate');
 
534
  }
535
 
536
  // -------------------------------------------------------------
537
+ // Tab 9: WGS Sequencing Metrics
538
  // -------------------------------------------------------------
539
  function renderWGSTab() {
540
  if (!state.wgs) return;
 
556
  }
557
 
558
  // -------------------------------------------------------------
559
+ // Tab 10: Mitotic Pathway Network
560
  // -------------------------------------------------------------
561
  function renderPathwayTab() {
562
  if (!state.pathway) return;
 
626
  };
627
  });
628
 
629
+ // Variant Filters
630
  els.geneFilter.onchange = (e) => {
631
  state.selectedGene = e.target.value;
632
  applyVariantFilters();
 
653
  applyVariantFilters();
654
  };
655
 
656
+ // Infotable Search Filters
657
+ if (els.searchGeneBurden) {
658
+ els.searchGeneBurden.oninput = (e) => renderGeneBurdenTable(e.target.value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  }
660
 
661
+ if (els.searchSyndromicDiff) {
662
+ els.searchSyndromicDiff.oninput = (e) => renderSyndromicDiffTable(e.target.value);
 
 
 
663
  }
664
 
665
+ if (els.searchMitoticMachinery) {
666
+ els.searchMitoticMachinery.oninput = (e) => renderMitoticMachineryTable(e.target.value);
 
 
 
 
 
 
 
 
 
 
 
667
  }
668
 
669
+ if (els.searchKaryotypeMatrix) {
670
+ els.searchKaryotypeMatrix.oninput = (e) => renderKaryotypeMatrixTable(e.target.value);
 
 
671
  }
672
 
673
+ // Modals
674
+ els.btnCloseVarModal.onclick = () => els.modalVariant.classList.add('hidden');
675
+ els.btnShortcuts.onclick = () => els.modalHelp.classList.remove('hidden');
676
+ els.btnCloseHelpModal.onclick = () => els.modalHelp.classList.add('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
 
678
+ // Keyboard Shortcuts (1-9 and 0)
679
+ window.onkeydown = (e) => {
680
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
 
682
+ if (['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'].includes(e.key)) {
683
+ let idx = parseInt(e.key, 10) - 1;
684
+ if (e.key === '0') idx = 9;
685
+ if (els.navTabs[idx]) els.navTabs[idx].click();
686
+ } else if (e.key === '/') {
687
+ e.preventDefault();
688
+ if (els.navTabs[1]) els.navTabs[1].click();
689
+ els.variantSearch.focus();
690
+ } else if (e.key === 'Escape') {
691
+ els.modalVariant.classList.add('hidden');
692
+ els.modalHelp.classList.add('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  }
694
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  }
696
 
697
+ // Launch on DOM ready
698
+ window.addEventListener('DOMContentLoaded', init);
 
 
 
 
 
 
 
699
  })();