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>
							 |