mearge
This commit is contained in:
263
pages/test/pathway2.vue
Normal file
263
pages/test/pathway2.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user