Files
bio_frontend/pages/test/pathway2.vue

264 lines
7.1 KiB
Vue
Raw Normal View History

2025-09-12 11:10:43 +09:00
<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>