248 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
		
		
			
		
	
	
			248 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| 
								 | 
							
								<template>
							 | 
						||
| 
								 | 
							
								  <div style="position: relative;">
							 | 
						||
| 
								 | 
							
								    <h2>KEGG Pathway Viewer (Compact Overlay)</h2>
							 | 
						||
| 
								 | 
							
								    <div ref="cyContainer" style="width: 100%; height: 800px; border: 1px solid #ccc; position: relative;"></div>
							 | 
						||
| 
								 | 
							
								    <div ref="overlayContainer" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1000;"></div>
							 | 
						||
| 
								 | 
							
								  </div>
							 | 
						||
| 
								 | 
							
								</template>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								<script setup>
							 | 
						||
| 
								 | 
							
								import { ref, onMounted, nextTick } from 'vue'
							 | 
						||
| 
								 | 
							
								import cytoscape from 'cytoscape'
							 | 
						||
| 
								 | 
							
								import { Chart, registerables } from 'chart.js'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Chart.register(...registerables)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const cyContainer = ref(null)
							 | 
						||
| 
								 | 
							
								const overlayContainer = ref(null)
							 | 
						||
| 
								 | 
							
								const chartMap = new Map()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function toRenderedPosition(pos, cy) {
							 | 
						||
| 
								 | 
							
								  const zoom = cy.zoom()
							 | 
						||
| 
								 | 
							
								  const pan = cy.pan()
							 | 
						||
| 
								 | 
							
								  return {
							 | 
						||
| 
								 | 
							
								    x: pos.x * zoom + pan.x,
							 | 
						||
| 
								 | 
							
								    y: pos.y * zoom + pan.y
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								onMounted(async () => {
							 | 
						||
| 
								 | 
							
								  await nextTick()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const resizeOverlay = () => {
							 | 
						||
| 
								 | 
							
								    if (!cyContainer.value || !overlayContainer.value) return
							 | 
						||
| 
								 | 
							
								    overlayContainer.value.style.width = cyContainer.value.offsetWidth + 'px'
							 | 
						||
| 
								 | 
							
								    overlayContainer.value.style.height = cyContainer.value.offsetHeight + 'px'
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  resizeOverlay()
							 | 
						||
| 
								 | 
							
								  window.addEventListener('resize', resizeOverlay)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const res = await fetch('/pon00061.xml')
							 | 
						||
| 
								 | 
							
								  const xmlText = await res.text()
							 | 
						||
| 
								 | 
							
								  const parser = new DOMParser()
							 | 
						||
| 
								 | 
							
								  const xmlDoc = parser.parseFromString(xmlText, 'application/xml')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const scale = 5
							 | 
						||
| 
								 | 
							
								  const entryMap = new Map()
							 | 
						||
| 
								 | 
							
								  const entryDataMap = new Map()
							 | 
						||
| 
								 | 
							
								  const parentMap = new Map()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const entries = Array.from(xmlDoc.getElementsByTagName('entry'))
							 | 
						||
| 
								 | 
							
								  for (const entry of entries) {
							 | 
						||
| 
								 | 
							
								    const id = entry.getAttribute('id')
							 | 
						||
| 
								 | 
							
								    entryDataMap.set(id, entry)
							 | 
						||
| 
								 | 
							
								    if (entry.getAttribute('type') === 'group') {
							 | 
						||
| 
								 | 
							
								      const components = entry.getElementsByTagName('component')
							 | 
						||
| 
								 | 
							
								      for (const comp of components) {
							 | 
						||
| 
								 | 
							
								        const childId = comp.getAttribute('id')
							 | 
						||
| 
								 | 
							
								        parentMap.set(childId, id)
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const nodes = entries.map(entry => {
							 | 
						||
| 
								 | 
							
								    const id = entry.getAttribute('id')
							 | 
						||
| 
								 | 
							
								    const graphics = entry.getElementsByTagName('graphics')[0]
							 | 
						||
| 
								 | 
							
								    const x = parseFloat(graphics?.getAttribute('x') || '0') * scale
							 | 
						||
| 
								 | 
							
								    const y = parseFloat(graphics?.getAttribute('y') || '0') * scale
							 | 
						||
| 
								 | 
							
								    const label = graphics?.getAttribute('name') || id
							 | 
						||
| 
								 | 
							
								    const fgColor = graphics?.getAttribute('fgcolor') || '#000000'
							 | 
						||
| 
								 | 
							
								    const bgColor = graphics?.getAttribute('bgcolor') || '#ffffff'
							 | 
						||
| 
								 | 
							
								    const entryType = entry.getAttribute('type')
							 | 
						||
| 
								 | 
							
								    const shapeType = entryType === 'compound' ? 'compound' : entryType === 'group' ? 'group' : entryType === 'ortholog' ? 'ortholog' : 'gene'
							 | 
						||
| 
								 | 
							
								    const parent = parentMap.get(id)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const node = {
							 | 
						||
| 
								 | 
							
								      data: {
							 | 
						||
| 
								 | 
							
								        id,
							 | 
						||
| 
								 | 
							
								        label,
							 | 
						||
| 
								 | 
							
								        link: entry.getAttribute('link') || null,
							 | 
						||
| 
								 | 
							
								        reaction: entry.getAttribute('reaction') || null
							 | 
						||
| 
								 | 
							
								      },
							 | 
						||
| 
								 | 
							
								      position: { x, y },
							 | 
						||
| 
								 | 
							
								      classes: shapeType,
							 | 
						||
| 
								 | 
							
								      style: {
							 | 
						||
| 
								 | 
							
								        color: fgColor,
							 | 
						||
| 
								 | 
							
								        'background-color': bgColor
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    if (parent) node.data.parent = parent
							 | 
						||
| 
								 | 
							
								    entryMap.set(id, true)
							 | 
						||
| 
								 | 
							
								    return node
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  function resolveToRealNode(id) {
							 | 
						||
| 
								 | 
							
								    if (!entryMap.has(id)) return null
							 | 
						||
| 
								 | 
							
								    const entry = entryDataMap.get(id)
							 | 
						||
| 
								 | 
							
								    if (entry?.getAttribute('type') === 'group') {
							 | 
						||
| 
								 | 
							
								      const components = Array.from(entry.getElementsByTagName('component'))
							 | 
						||
| 
								 | 
							
								      if (components.length > 0) return components[0].getAttribute('id')
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    return id
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const edges = []
							 | 
						||
| 
								 | 
							
								  const relations = Array.from(xmlDoc.getElementsByTagName('relation'))
							 | 
						||
| 
								 | 
							
								  relations.forEach((rel, i) => {
							 | 
						||
| 
								 | 
							
								    let source = resolveToRealNode(rel.getAttribute('entry1'))
							 | 
						||
| 
								 | 
							
								    let target = resolveToRealNode(rel.getAttribute('entry2'))
							 | 
						||
| 
								 | 
							
								    const type = rel.getAttribute('type')
							 | 
						||
| 
								 | 
							
								    const subtypes = Array.from(rel.getElementsByTagName('subtype'))
							 | 
						||
| 
								 | 
							
								    const compoundSubtype = subtypes.find(s => s.getAttribute('name') === 'compound')
							 | 
						||
| 
								 | 
							
								    if (compoundSubtype) {
							 | 
						||
| 
								 | 
							
								      const compoundId = compoundSubtype.getAttribute('value')
							 | 
						||
| 
								 | 
							
								      if (entryMap.has(source) && entryMap.has(target) && entryMap.has(compoundId)) {
							 | 
						||
| 
								 | 
							
								        edges.push(
							 | 
						||
| 
								 | 
							
								          { data: { id: `edge${i}-1`, source, target: compoundId, label: 'via compound' } },
							 | 
						||
| 
								 | 
							
								          { data: { id: `edge${i}-2`, source: compoundId, target, label: 'via compound' } }
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      if (entryMap.has(source) && entryMap.has(target)) {
							 | 
						||
| 
								 | 
							
								        edges.push({ data: { id: `edge${i}`, source, target, label: type } })
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const reactions = Array.from(xmlDoc.getElementsByTagName('reaction'))
							 | 
						||
| 
								 | 
							
								  reactions.forEach((reaction, i) => {
							 | 
						||
| 
								 | 
							
								    const reactionType = reaction.getAttribute('type')
							 | 
						||
| 
								 | 
							
								    const reactionLabel = reaction.getAttribute('name') || `reaction${i}`
							 | 
						||
| 
								 | 
							
								    const substrates = Array.from(reaction.getElementsByTagName('substrate'))
							 | 
						||
| 
								 | 
							
								    const products = Array.from(reaction.getElementsByTagName('product'))
							 | 
						||
| 
								 | 
							
								    substrates.forEach(substrate => {
							 | 
						||
| 
								 | 
							
								      const sid = resolveToRealNode(substrate.getAttribute('id'))
							 | 
						||
| 
								 | 
							
								      products.forEach(product => {
							 | 
						||
| 
								 | 
							
								        const pid = resolveToRealNode(product.getAttribute('id'))
							 | 
						||
| 
								 | 
							
								        if (entryMap.has(sid) && entryMap.has(pid)) {
							 | 
						||
| 
								 | 
							
								          edges.push({ data: { id: `reaction-${i}-${sid}-${pid}`, source: sid, target: pid, label: `${reactionLabel} (${reactionType})` } })
							 | 
						||
| 
								 | 
							
								          if (reactionType === 'reversible') {
							 | 
						||
| 
								 | 
							
								            edges.push({ data: { id: `reaction-${i}-${pid}-${sid}`, source: pid, target: sid, label: `${reactionLabel} (reversible)` } })
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      })
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const cy = cytoscape({
							 | 
						||
| 
								 | 
							
								    container: cyContainer.value,
							 | 
						||
| 
								 | 
							
								    elements: { nodes, edges },
							 | 
						||
| 
								 | 
							
								    style: [
							 | 
						||
| 
								 | 
							
								      { selector: 'node', style: { label: 'data(label)', 'text-valign': 'center', 'text-halign': 'center', shape: 'rectangle', 'font-size': 10, 'border-width': 1, 'border-color': '#333' } },
							 | 
						||
| 
								 | 
							
								      { selector: '.ortholog', style: { 'background-color': '#ccffcc', shape: 'round-rectangle', 'border-width': 2, 'border-color': '#339933' } },
							 | 
						||
| 
								 | 
							
								      { selector: '$node > node', style: { 'background-color': '#f3f3f3', 'border-width': 2, 'border-color': '#666', shape: 'roundrectangle' } },
							 | 
						||
| 
								 | 
							
								      { selector: '.compound', style: { 'background-color': '#ffe135', shape: 'ellipse' } },
							 | 
						||
| 
								 | 
							
								      { selector: 'edge', style: { width: 2, 'line-color': '#888', 'target-arrow-shape': 'triangle', 'label': 'data(label)', 'font-size': 8 } }
							 | 
						||
| 
								 | 
							
								    ],
							 | 
						||
| 
								 | 
							
								    layout: { name: 'preset' }
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  cy.ready(() => {
							 | 
						||
| 
								 | 
							
								    cy.fit(cy.elements(), 100)
							 | 
						||
| 
								 | 
							
								    createNodeOverlays(cy)
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  cy.on('render', () => {
							 | 
						||
| 
								 | 
							
								    requestAnimationFrame(() => updateOverlayPositions(cy))
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function createNodeOverlays(cy) {
							 | 
						||
| 
								 | 
							
								  cy.nodes().forEach(node => {
							 | 
						||
| 
								 | 
							
								    const pos = node.renderedPosition()
							 | 
						||
| 
								 | 
							
								    const id = node.id()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const wrapper = document.createElement('div')
							 | 
						||
| 
								 | 
							
								    wrapper.style.position = 'absolute'
							 | 
						||
| 
								 | 
							
								    wrapper.style.left = `${pos.x + 30}px`
							 | 
						||
| 
								 | 
							
								    wrapper.style.top = `${pos.y - 40}px`
							 | 
						||
| 
								 | 
							
								    wrapper.style.width = '20px'
							 | 
						||
| 
								 | 
							
								    wrapper.style.height = '80px'
							 | 
						||
| 
								 | 
							
								    wrapper.style.background = 'rgba(255,255,255,0.9)'
							 | 
						||
| 
								 | 
							
								    wrapper.style.fontSize = '6px'
							 | 
						||
| 
								 | 
							
								    wrapper.style.border = '1px solid #ccc'
							 | 
						||
| 
								 | 
							
								    wrapper.style.borderRadius = '2px'
							 | 
						||
| 
								 | 
							
								    wrapper.style.overflow = 'hidden'
							 | 
						||
| 
								 | 
							
								    wrapper.style.pointerEvents = 'none'
							 | 
						||
| 
								 | 
							
								    wrapper.style.zIndex = '9999' // 보장
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const table = document.createElement('table')
							 | 
						||
| 
								 | 
							
								    table.style.borderCollapse = 'collapse'
							 | 
						||
| 
								 | 
							
								    table.style.width = '100%'
							 | 
						||
| 
								 | 
							
								    for (let i = 0; i < 7; i++) {
							 | 
						||
| 
								 | 
							
								      const tr = document.createElement('tr')
							 | 
						||
| 
								 | 
							
								      for (let j = 0; j < 2; j++) {
							 | 
						||
| 
								 | 
							
								        const td = document.createElement('td')
							 | 
						||
| 
								 | 
							
								        td.textContent = '·'
							 | 
						||
| 
								 | 
							
								        td.style.padding = '0'
							 | 
						||
| 
								 | 
							
								        td.style.fontSize = '6px'
							 | 
						||
| 
								 | 
							
								        td.style.textAlign = 'center'
							 | 
						||
| 
								 | 
							
								        tr.appendChild(td)
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      table.appendChild(tr)
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const canvas = document.createElement('canvas')
							 | 
						||
| 
								 | 
							
								    canvas.width = 16
							 | 
						||
| 
								 | 
							
								    canvas.height = 16
							 | 
						||
| 
								 | 
							
								    canvas.style.margin = '2px auto 0'
							 | 
						||
| 
								 | 
							
								    canvas.style.display = 'block'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    wrapper.appendChild(table)
							 | 
						||
| 
								 | 
							
								    wrapper.appendChild(canvas)
							 | 
						||
| 
								 | 
							
								    overlayContainer.value.appendChild(wrapper)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const ctx = canvas.getContext('2d')
							 | 
						||
| 
								 | 
							
								    const chart = new Chart(ctx, {
							 | 
						||
| 
								 | 
							
								      type: 'doughnut',
							 | 
						||
| 
								 | 
							
								      data: {
							 | 
						||
| 
								 | 
							
								        labels: ['A', 'B'],
							 | 
						||
| 
								 | 
							
								        datasets: [{ data: [30, 70], backgroundColor: ['#ff6384', '#36a2eb'] }]
							 | 
						||
| 
								 | 
							
								      },
							 | 
						||
| 
								 | 
							
								      options: {
							 | 
						||
| 
								 | 
							
								        responsive: false,
							 | 
						||
| 
								 | 
							
								        plugins: { legend: { display: false }, tooltip: { enabled: false } },
							 | 
						||
| 
								 | 
							
								        cutout: '60%',
							 | 
						||
| 
								 | 
							
								        animation: false
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    chartMap.set(id, { wrapper, chart })
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function updateOverlayPositions(cy) {
							 | 
						||
| 
								 | 
							
								  cy.nodes().forEach(node => {
							 | 
						||
| 
								 | 
							
								    const entry = chartMap.get(node.id())
							 | 
						||
| 
								 | 
							
								    if (entry) {
							 | 
						||
| 
								 | 
							
								      const pos = node.renderedPosition()
							 | 
						||
| 
								 | 
							
								      entry.wrapper.style.left = `${pos.x + 30}px`
							 | 
						||
| 
								 | 
							
								      entry.wrapper.style.top = `${pos.y - 40}px`
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								</script>
							 |