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