245 lines
7.3 KiB
Vue
245 lines
7.3 KiB
Vue
|
|
<template>
|
||
|
|
<div ref="cyContainer" class="cy-container"></div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { ref, onMounted, nextTick } from 'vue'
|
||
|
|
import cytoscape from 'cytoscape'
|
||
|
|
|
||
|
|
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('/expanded_pathway21600.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>
|
||
|
|
|