254 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
		
		
			
		
	
	
			254 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| 
								 | 
							
								<template>
							 | 
						||
| 
								 | 
							
								    <div ref="cyContainer" class="cy-container"></div>
							 | 
						||
| 
								 | 
							
								  </template>
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  <script setup>
							 | 
						||
| 
								 | 
							
								  import { ref, onMounted, nextTick } from 'vue'
							 | 
						||
| 
								 | 
							
								  import cytoscape from 'cytoscape'
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  if (typeof document !== 'undefined') {
							 | 
						||
| 
								 | 
							
								    const script = document.createElement('script');
							 | 
						||
| 
								 | 
							
								    script.src = '/dist/cy_custom.js';
							 | 
						||
| 
								 | 
							
								    script.onload = () => {
							 | 
						||
| 
								 | 
							
								      window.igvCustomLoaded = true;
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								    document.head.appendChild(script);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let CytoscapeOverlays = null
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (import.meta.client) {
							 | 
						||
| 
								 | 
							
								    const useOverlay = (await import('@/composables/useOverlay')).default
							 | 
						||
| 
								 | 
							
								    CytoscapeOverlays = await useOverlay()
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const cyContainer = ref(null)
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  onMounted(async () => {
							 | 
						||
| 
								 | 
							
								    await nextTick()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    cytoscape.use(CytoscapeOverlays.default)
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								    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'))
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								    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) {
							 | 
						||
| 
								 | 
							
								          parentMap.set(comp.getAttribute('id'), 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 parent = parentMap.get(id)
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								      const valueA = Math.floor(Math.random() * 50)
							 | 
						||
| 
								 | 
							
								      const valueB = 100 - valueA
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								      const node = {
							 | 
						||
| 
								 | 
							
								        data: {
							 | 
						||
| 
								 | 
							
								          id,
							 | 
						||
| 
								 | 
							
								          label,
							 | 
						||
| 
								 | 
							
								          link: entry.getAttribute('link') || null,
							 | 
						||
| 
								 | 
							
								          reaction: entry.getAttribute('reaction') || null,
							 | 
						||
| 
								 | 
							
								          chartData: [valueA, valueB]
							 | 
						||
| 
								 | 
							
								        },
							 | 
						||
| 
								 | 
							
								        position: { x, y },
							 | 
						||
| 
								 | 
							
								        classes: entry.getAttribute('type'),
							 | 
						||
| 
								 | 
							
								        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) => {
							 | 
						||
| 
								 | 
							
								      const source = resolveToRealNode(rel.getAttribute('entry1'))
							 | 
						||
| 
								 | 
							
								      const 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)) {
							 | 
						||
| 
								 | 
							
								          const sourceConst = source;
							 | 
						||
| 
								 | 
							
								          const targetConst = target;
							 | 
						||
| 
								 | 
							
								          edges.push(
							 | 
						||
| 
								 | 
							
								            {
							 | 
						||
| 
								 | 
							
								              data: {
							 | 
						||
| 
								 | 
							
								                id: `edge${i}-1`,
							 | 
						||
| 
								 | 
							
								                source: sourceConst,
							 | 
						||
| 
								 | 
							
								                target: compoundId,
							 | 
						||
| 
								 | 
							
								                label: 'via compound'
							 | 
						||
| 
								 | 
							
								              }
							 | 
						||
| 
								 | 
							
								            },
							 | 
						||
| 
								 | 
							
								            {
							 | 
						||
| 
								 | 
							
								              data: {
							 | 
						||
| 
								 | 
							
								                id: `edge${i}-2`,
							 | 
						||
| 
								 | 
							
								                source: compoundId,
							 | 
						||
| 
								 | 
							
								                target: targetConst,
							 | 
						||
| 
								 | 
							
								                label: 'via compound'
							 | 
						||
| 
								 | 
							
								              }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								          )
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        if (entryMap.has(source) && entryMap.has(target)) {
							 | 
						||
| 
								 | 
							
								          const sourceConst = source;
							 | 
						||
| 
								 | 
							
								          const targetConst = target;
							 | 
						||
| 
								 | 
							
								          edges.push({
							 | 
						||
| 
								 | 
							
								            data: {
							 | 
						||
| 
								 | 
							
								              id: `edge${i}`,
							 | 
						||
| 
								 | 
							
								              source: sourceConst,
							 | 
						||
| 
								 | 
							
								              target: targetConst,
							 | 
						||
| 
								 | 
							
								              label: type
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								          })
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								    const reactions = Array.from(xmlDoc.getElementsByTagName('reaction'))
							 | 
						||
| 
								 | 
							
								    reactions.forEach((reaction, i) => {
							 | 
						||
| 
								 | 
							
								      const reactionType = reaction.getAttribute('type')
							 | 
						||
| 
								 | 
							
								      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: `${reactionType}`
							 | 
						||
| 
								 | 
							
								              }
							 | 
						||
| 
								 | 
							
								            })
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								      })
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								    // 원형(도넛/파이) 차트 overlay 생성 (흰색 라인 없이)
							 | 
						||
| 
								 | 
							
								    const pieOverlay = CytoscapeOverlays.renderSymbol({
							 | 
						||
| 
								 | 
							
								      symbol: node => {
							 | 
						||
| 
								 | 
							
								        const data = node.data('chartData') || [50, 50]
							 | 
						||
| 
								 | 
							
								        return {
							 | 
						||
| 
								 | 
							
								          draw: (ctx, size) => {
							 | 
						||
| 
								 | 
							
								            const total = data[0] + data[1]
							 | 
						||
| 
								 | 
							
								            const r = Math.sqrt(size / Math.PI)
							 | 
						||
| 
								 | 
							
								            const innerR = r * 0.5
							 | 
						||
| 
								 | 
							
								            const startAngle = 0
							 | 
						||
| 
								 | 
							
								            const angleA = (data[0] / total) * 2 * Math.PI
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            // A 영역
							 | 
						||
| 
								 | 
							
								            ctx.beginPath()
							 | 
						||
| 
								 | 
							
								            ctx.arc(0, 0, r, startAngle, startAngle + angleA)
							 | 
						||
| 
								 | 
							
								            ctx.arc(0, 0, innerR, startAngle + angleA, startAngle, true)
							 | 
						||
| 
								 | 
							
								            ctx.closePath()
							 | 
						||
| 
								 | 
							
								            ctx.fillStyle = '#36a2eb'
							 | 
						||
| 
								 | 
							
								            ctx.fill()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            // B 영역
							 | 
						||
| 
								 | 
							
								            ctx.beginPath()
							 | 
						||
| 
								 | 
							
								            ctx.arc(0, 0, r, startAngle + angleA, startAngle + 2 * Math.PI)
							 | 
						||
| 
								 | 
							
								            ctx.arc(0, 0, innerR, startAngle + 2 * Math.PI, startAngle + angleA, true)
							 | 
						||
| 
								 | 
							
								            ctx.closePath()
							 | 
						||
| 
								 | 
							
								            ctx.fillStyle = '#ff6384'
							 | 
						||
| 
								 | 
							
								            ctx.fill()
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      },
							 | 
						||
| 
								 | 
							
								      color: '',
							 | 
						||
| 
								 | 
							
								      width: 32,
							 | 
						||
| 
								 | 
							
								      height: 32,
							 | 
						||
| 
								 | 
							
								      borderColor: '#333'
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const cy = cytoscape({
							 | 
						||
| 
								 | 
							
								      container: cyContainer.value,
							 | 
						||
| 
								 | 
							
								      elements: { nodes, edges },
							 | 
						||
| 
								 | 
							
								      style: [
							 | 
						||
| 
								 | 
							
								        { selector: 'node', style: {
							 | 
						||
| 
								 | 
							
								            label: 'data(label)',
							 | 
						||
| 
								 | 
							
								            'background-color': '#eee',
							 | 
						||
| 
								 | 
							
								            'text-valign': 'center',
							 | 
						||
| 
								 | 
							
								            'text-halign': 'center',
							 | 
						||
| 
								 | 
							
								            'border-width': 2,
							 | 
						||
| 
								 | 
							
								            'border-color': '#333',
							 | 
						||
| 
								 | 
							
								            'font-size': 8,
							 | 
						||
| 
								 | 
							
								            'padding': '6px',
							 | 
						||
| 
								 | 
							
								            'width': 32,
							 | 
						||
| 
								 | 
							
								            'height': 32,
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        },
							 | 
						||
| 
								 | 
							
								        { selector: 'edge', style: { 'curve-style': 'bezier', 'target-arrow-shape': 'triangle', 'line-color': '#888' } },
							 | 
						||
| 
								 | 
							
								      ],
							 | 
						||
| 
								 | 
							
								      layout: { name: 'preset', padding: 100 }
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    cy.overlays(
							 | 
						||
| 
								 | 
							
								      [
							 | 
						||
| 
								 | 
							
								        { position: 'right', vis: pieOverlay }
							 | 
						||
| 
								 | 
							
								      ],
							 | 
						||
| 
								 | 
							
								      {
							 | 
						||
| 
								 | 
							
								        updateOn: 'render',
							 | 
						||
| 
								 | 
							
								        backgroundColor: 'white'
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    applyCustomZoomHandling(cy);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								});
							 | 
						||
| 
								 | 
							
								  </script>
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  <style>
							 | 
						||
| 
								 | 
							
								  .cy-container {
							 | 
						||
| 
								 | 
							
								    width: 100vw !important;
							 | 
						||
| 
								 | 
							
								    max-width: 100vw !important;
							 | 
						||
| 
								 | 
							
								    min-width: 0 !important;
							 | 
						||
| 
								 | 
							
								    height: 800px !important;
							 | 
						||
| 
								 | 
							
								    overflow: hidden !important;
							 | 
						||
| 
								 | 
							
								    position: relative;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  </style>
							 | 
						||
| 
								 | 
							
								  
							 |