'use client'; import { useEffect, useRef, useState } from 'react'; import { CircleNotch as LoaderCircle, DownloadSimple as Download } from '@phosphor-icons/react'; import { downloadText } from '@/lib/export-utils'; interface Props { stId: string; geneName?: string; height?: number; } declare global { interface Window { Reactome?: { Diagram: { create: (params: { placeHolder: string; width?: number; height?: number; proxyPrefix?: string; }) => { loadDiagram: (stId: string) => void; flagItems: (term: string) => void; selectItem: (stId: string) => void; resetHighlight: () => void; resetSelection: () => void; onDiagramLoaded: (cb: (stId: string) => void) => void; onObjectSelected: (cb: (obj: { stId: string; displayName: string }) => void) => void; onObjectHovered: (cb: (obj: { stId: string; displayName: string }) => void) => void; }; }; }; onReactomeDiagramReady?: () => void; } } const ISOLATION_CSS = (id: string) => ` #${id} { color-scheme: light; background: #fff; color: #1a1a2e; } #${id} * { color: revert !important; background-color: revert !important; background-image: revert !important; font-family: revert !important; font-size: revert !important; font-weight: revert !important; line-height: revert !important; text-align: revert !important; } `; export default function PathwayDiagram({ stId, geneName, height = 400 }: Props) { const containerId = useRef(`diagram-${stId}-${Math.random().toString(36).slice(2, 8)}`).current; const [loaded, setLoaded] = useState(false); const [error, setError] = useState(null); type DiagramInstance = ReturnType['Diagram']['create']>; const diagramRef = useRef(null); const initCalled = useRef(false); const containerRef = useRef(null); const exportSvg = () => { const svg = containerRef.current?.querySelector('svg'); if (!svg) return; downloadText(new XMLSerializer().serializeToString(svg), `${stId}_pathway.svg`); }; const exportPng = () => { const svg = containerRef.current?.querySelector('svg'); if (!svg) return; const svgStr = new XMLSerializer().serializeToString(svg); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d')!; const img = new window.Image(); img.onload = () => { canvas.width = img.width; canvas.height = img.height; ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); const a = document.createElement('a'); a.download = `${stId}_pathway.png`; a.href = canvas.toDataURL('image/png'); a.click(); }; img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr))); }; useEffect(() => { const style = document.createElement('style'); style.textContent = ISOLATION_CSS(containerId); document.head.appendChild(style); if (initCalled.current) return; initCalled.current = true; if (window.Reactome?.Diagram) { initDiagram(); return; } const script = document.createElement('script'); script.src = 'https://reactome.org/DiagramJs/diagram/diagram.nocache.js'; script.async = true; script.onerror = () => setError('Failed to load Reactome DiagramJs'); document.head.appendChild(script); const origReady = window.onReactomeDiagramReady; window.onReactomeDiagramReady = () => { origReady?.(); initDiagram(); }; return () => { style.remove(); if (diagramRef.current) { diagramRef.current.resetHighlight(); diagramRef.current.resetSelection(); } }; }, []); function initDiagram() { if (!window.Reactome?.Diagram) { const check = setInterval(() => { if (window.Reactome?.Diagram) { clearInterval(check); createDiagram(); } }, 200); setTimeout(() => clearInterval(check), 10000); return; } createDiagram(); } function createDiagram() { try { const diagram = window.Reactome!.Diagram.create({ placeHolder: containerId, width: 950, height, }); diagramRef.current = diagram; diagram.onDiagramLoaded(() => { setLoaded(true); if (geneName) { diagram.flagItems(geneName); } }); diagram.loadDiagram(stId); } catch { setError('Failed to initialize diagram viewer'); } } return (
{loaded && (
)}
{!loaded && !error && (
)} {error && (

{error}

)}
); }