261 lines
7.1 KiB
Vue
261 lines
7.1 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>
|