Spaces:
Running
Running
File size: 6,099 Bytes
95e9035 a56b947 95e9035 e32795d 95e9035 e32795d 41d0a94 2fe9546 41d0a94 95e9035 41d0a94 a56b947 95e9035 a56b947 95e9035 41d0a94 e32795d a56b947 41d0a94 95e9035 a56b947 2fe9546 41d0a94 2fe9546 41d0a94 2fe9546 41d0a94 2fe9546 41d0a94 2fe9546 41d0a94 2fe9546 41d0a94 2fe9546 41d0a94 2fe9546 a56b947 41d0a94 2fe9546 41d0a94 2fe9546 41d0a94 454af7f e32795d 95e9035 2fe9546 454af7f 95e9035 a56b947 95e9035 a56b947 2fe9546 a56b947 95e9035 e32795d 95e9035 a56b947 95e9035 41d0a94 95e9035 6c7f30f 95e9035 a56b947 e32795d a56b947 e32795d a56b947 95e9035 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | 'use client';
import { useEffect, useRef, useState } from 'react';
declare global {
interface Window {
igv: any;
}
}
const IGV_CDN_URL = 'https://cdn.jsdelivr.net/npm/igv@3.8.5/dist/igv.min.js';
interface IGVBrowserProps {
referenceUrl: string;
bamUrl?: string;
baiUrl?: string;
vcfUrl?: string;
locus?: string;
className?: string;
}
function loadScript(src: string): Promise<void> {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
resolve();
return;
}
const script = document.createElement('script');
script.src = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load ${src}`));
document.head.appendChild(script);
});
}
async function fetchBlobUrl(url: string, label: string): Promise<string> {
const resp = await fetch(url);
if (!resp.ok) throw new Error(`Failed to fetch ${label}: HTTP ${resp.status}`);
const blob = await resp.blob();
if (blob.size === 0) throw new Error(`${label} is empty (0 bytes)`);
return URL.createObjectURL(blob);
}
export default function IGVBrowser({
referenceUrl,
bamUrl,
baiUrl,
vcfUrl,
locus,
className = '',
}: IGVBrowserProps) {
const containerRef = useRef<HTMLDivElement>(null);
const browserRef = useRef<any>(null);
const blobUrlsRef = useRef<string[]>([]);
const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading');
const [errorMsg, setErrorMsg] = useState('');
useEffect(() => {
if (!containerRef.current || !referenceUrl) return;
let cancelled = false;
async function init() {
try {
setStatus('loading');
setErrorMsg('');
// Clean up previous blob URLs
for (const u of blobUrlsRef.current) URL.revokeObjectURL(u);
blobUrlsRef.current = [];
// Load igv.js from CDN
await loadScript(IGV_CDN_URL);
if (cancelled || !containerRef.current) return;
if (!window.igv) throw new Error('igv.js failed to load from CDN');
// Destroy previous instance
if (browserRef.current) {
try { browserRef.current.destroy(); } catch {}
browserRef.current = null;
}
containerRef.current.innerHTML = '';
const tracks: any[] = [];
// Fetch reference FASTA as Blob URL
let refBlobUrl: string;
try {
refBlobUrl = await fetchBlobUrl(referenceUrl, 'reference FASTA');
blobUrlsRef.current.push(refBlobUrl);
} catch (e: any) {
throw new Error(`Reference genome fetch failed: ${e.message}`);
}
// Fetch BAM + BAI as Blob URLs (igv.js ONLY supports BAM for alignment tracks)
let hasAlignment = false;
if (bamUrl && baiUrl) {
try {
const bamBlob = await fetchBlobUrl(bamUrl, 'BAM');
const baiBlob = await fetchBlobUrl(baiUrl, 'BAI');
blobUrlsRef.current.push(bamBlob, baiBlob);
tracks.push({
name: 'Aligned Reads',
url: bamBlob,
type: 'alignment',
format: 'bam',
indexURL: baiBlob,
color: '#3b82f6',
height: 200,
displayMode: 'expanded',
indexed: true,
});
hasAlignment = true;
} catch (e: any) {
console.error('[IGV] BAM/BAI load failed:', e.message);
}
}
// Fetch VCF as Blob URL
if (vcfUrl) {
try {
const vcfBlob = await fetchBlobUrl(vcfUrl, 'VCF');
blobUrlsRef.current.push(vcfBlob);
tracks.push({
name: 'Variants',
url: vcfBlob,
format: 'vcf',
type: 'variant',
color: '#f59e0b',
height: 120,
displayMode: 'expanded',
indexed: false,
});
} catch (e: any) {
console.error('[IGV] VCF load failed:', e.message);
}
}
const browser = await window.igv.createBrowser(containerRef.current, {
reference: {
fastaURL: refBlobUrl,
indexed: false,
},
tracks,
locus: locus || '1',
showNavigation: true,
showRuler: true,
});
if (!cancelled) {
browserRef.current = browser;
setStatus('ready');
} else {
try { browser.destroy(); } catch {}
}
} catch (err: any) {
console.error('[IGV] Error:', err);
if (!cancelled) {
setStatus('error');
setErrorMsg(err?.message || 'Failed to initialize genome browser');
}
}
}
const timer = setTimeout(init, 200);
return () => {
cancelled = true;
clearTimeout(timer);
if (browserRef.current) {
try { browserRef.current.destroy(); } catch {}
browserRef.current = null;
}
for (const u of blobUrlsRef.current) URL.revokeObjectURL(u);
blobUrlsRef.current = [];
};
}, [referenceUrl, bamUrl, baiUrl, vcfUrl, locus]);
return (
<div className={className}>
{status === 'loading' && (
<div className="flex items-center justify-center py-12 bg-surface-1 rounded-xl">
<div className="w-5 h-5 border-2 border-accent-cyan/30 border-t-accent-cyan rounded-full animate-spin" />
<span className="ml-3 text-sm text-text-muted">Loading genome browser...</span>
</div>
)}
<div
ref={containerRef}
style={{
width: '100%',
minHeight: status === 'ready' ? '500px' : '0px',
display: status === 'loading' ? 'none' : 'block',
}}
/>
{status === 'error' && (
<div className="p-4 bg-error/5 border border-error/20 rounded-xl mt-2">
<p className="text-sm text-error font-medium">Genome browser failed to load</p>
<p className="text-xs text-error/70 mt-1 font-mono">{errorMsg}</p>
</div>
)}
</div>
);
}
|