264 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			264 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<template>
 | 
						|
  <div>
 | 
						|
    <h2>KEGG Pathway Viewer (Cytoscape.js)</h2>
 | 
						|
    <div ref="cyContainer" style="width: 100%; height: 800px; border: 1px solid #ccc;"></div>
 | 
						|
  </div>
 | 
						|
</template>
 | 
						|
 | 
						|
<script setup>
 | 
						|
import { ref, onMounted, nextTick } from 'vue'
 | 
						|
import cytoscape from 'cytoscape'
 | 
						|
 | 
						|
const cyContainer = ref(null)
 | 
						|
 | 
						|
onMounted(async () => {
 | 
						|
  await nextTick()
 | 
						|
 | 
						|
  const res = await fetch('/pon00061.xml')
 | 
						|
  const xmlText = await res.text()
 | 
						|
  const parser = new DOMParser()
 | 
						|
  const xmlDoc = parser.parseFromString(xmlText, 'application/xml')
 | 
						|
 | 
						|
  const scale = 3
 | 
						|
 | 
						|
  const entryMap = new Map()
 | 
						|
  const entryDataMap = new Map()
 | 
						|
  const parentMap = new Map()
 | 
						|
 | 
						|
  const entries = Array.from(xmlDoc.getElementsByTagName('entry'))
 | 
						|
 | 
						|
  // 1. 부모-자식 관계 정리
 | 
						|
  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)
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // 2. 노드 생성
 | 
						|
  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 shapeType = graphics?.getAttribute('type') === 'circle' ? 'compound' : '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
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // parent 설정
 | 
						|
    if (parent) node.data.parent = parent
 | 
						|
 | 
						|
    // ortholog 스타일 구분
 | 
						|
    if (entry.getAttribute('type') === 'ortholog') {
 | 
						|
      node.classes += ' ortholog'
 | 
						|
    }
 | 
						|
 | 
						|
    entryMap.set(id, true)
 | 
						|
    return node
 | 
						|
  })
 | 
						|
 | 
						|
  // 3. group 노드가 edge에 등장할 경우 첫 자식으로 대체
 | 
						|
  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 = []
 | 
						|
 | 
						|
  // 4. relation 기반 edge 처리
 | 
						|
  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
 | 
						|
          }
 | 
						|
        })
 | 
						|
      }
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  // 5. reaction 기반 edge 처리
 | 
						|
  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)`
 | 
						|
              }
 | 
						|
            })
 | 
						|
          }
 | 
						|
        }
 | 
						|
      })
 | 
						|
    })
 | 
						|
  })
 | 
						|
 | 
						|
  // 6. Cytoscape 초기화
 | 
						|
  const cy = cytoscape({
 | 
						|
    container: cyContainer.value,
 | 
						|
    elements: { nodes, edges },
 | 
						|
    style: [
 | 
						|
      {
 | 
						|
        selector: 'node',
 | 
						|
        style: {
 | 
						|
          label: 'data(label)',
 | 
						|
          'text-valign': 'center',
 | 
						|
          'text-halign': 'center',
 | 
						|
          'shape': 'rectangle',
 | 
						|
          'padding': '6px',
 | 
						|
          'text-wrap': 'wrap',
 | 
						|
          'color': '#000',
 | 
						|
          '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',
 | 
						|
          'text-valign': 'top',
 | 
						|
          'text-halign': 'center',
 | 
						|
          'font-weight': 'bold',
 | 
						|
          'padding': '20px'
 | 
						|
        }
 | 
						|
      },
 | 
						|
      {
 | 
						|
        selector: '.compound',
 | 
						|
        style: {
 | 
						|
          'background-color': '#ffe135',
 | 
						|
          'shape': 'ellipse'
 | 
						|
        }
 | 
						|
      },
 | 
						|
      {
 | 
						|
        selector: 'edge',
 | 
						|
        style: {
 | 
						|
          width: 2,
 | 
						|
          'line-color': '#888',
 | 
						|
          'target-arrow-color': '#888',
 | 
						|
          'target-arrow-shape': 'triangle',
 | 
						|
          'curve-style': 'bezier',
 | 
						|
          'label': 'data(label)',
 | 
						|
          'font-size': 8,
 | 
						|
          'text-background-opacity': 1,
 | 
						|
          'text-background-color': '#fff',
 | 
						|
          'text-background-shape': 'roundrectangle',
 | 
						|
          'text-rotation': 'autorotate'
 | 
						|
        }
 | 
						|
      }
 | 
						|
    ],
 | 
						|
    layout: { name: 'preset' }
 | 
						|
  })
 | 
						|
 | 
						|
  cy.ready(() => {
 | 
						|
    cy.fit(cy.elements(), 100)
 | 
						|
  })
 | 
						|
 | 
						|
  // 7. 노드 클릭 시 KEGG 링크 열기
 | 
						|
  cy.on('tap', 'node', (evt) => {
 | 
						|
    const node = evt.target
 | 
						|
    const link = node.data('link')
 | 
						|
    if (link) {
 | 
						|
      window.open(link, '_blank')
 | 
						|
    }
 | 
						|
  })
 | 
						|
})
 | 
						|
</script>
 |