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