2026/02/09

Cytoscape.js

Cytoscape.js 是一個處理資料視覺化的 javascript library,當我們要對資料關係進行可視化顯示時,例如社交網路關係或網路拓樸圖時,Cytoscape.js 是個不錯的選擇。

Cytoscape 和 Cytoscape.js 是兩個完全獨立不同的軟體

  • Cytoscape

    • 使用 Java 語言編寫的用於網絡可視化的桌面應用程序

    • 需要安裝 Java SDK 才能使用

    • 用於大型網絡分析和可視化的高性能應用程序

  • Cytoscape.js

    • 用於網絡可視化的 javascript library,本身不是一個完整的 Web Application

    • 可以在大多數瀏覽器上使用

    • 不需要 plugin 即可運行

    • 需要編寫程式來建構 Web Application

    • 支援 Extensions

    • 基於 CSS 將資料映射到元件屬性

sample1

建立一個 圓形排列 的 4 個節點 (A, B, C, D),節點之間有箭頭連線,點擊節點會有事件

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>test1</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
            display: block;
        }
    </style>
</head>
<body>
    <h2>test1</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'a', label: '節點 A' } },
                { data: { id: 'b', label: '節點 B' } },
                { data: { id: 'c', label: '節點 C' } },
                { data: { id: 'd', label: '節點 D' } },
                { data: { id: 'ab', source: 'a', target: 'b' } },
                { data: { id: 'bc', source: 'b', target: 'c' } },
                { data: { id: 'cd', source: 'c', target: 'd' } },
                { data: { id: 'da', source: 'd', target: 'a' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 3,
                        'line-color': '#AAAAAA',
                        'target-arrow-color': '#AAAAAA',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'circle'
            }
        });

        // 點擊事件
        cy.on('tap', 'node', function(evt) {
            let node = evt.target;
            console.log('你點了節點: ' + node.id());
        });
    </script>
</body>
</html>

sample2

流程圖,layout 調整為 dagre extension。

使用時要引用 dagre library 及 Cytoscape.js 的 extension

dagre 正是 Cytoscape.js 常用來畫flowchart或 directed graph 的 layout。適合做 flowchart, network topology, workflow

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Flowchart</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <script src="https://unpkg.com/dagre/dist/dagre.min.js"></script>
    <script src="https://unpkg.com/cytoscape-dagre/cytoscape-dagre.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>flowchart</h2>
    <div id="cy"></div>

    <script>
        cytoscape.use(cytoscapeDagre);

        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'start', label: '開始' } },
                { data: { id: 'step1', label: '步驟 1' } },
                { data: { id: 'step2', label: '步驟 2' } },
                { data: { id: 'decision', label: '判斷 ?' } },
                { data: { id: 'end', label: '結束' } },
                { data: { id: 's1', source: 'start', target: 'step1' } },
                { data: { id: 's2', source: 'step1', target: 'step2' } },
                { data: { id: 's3', source: 'step2', target: 'decision' } },
                { data: { id: 's4', source: 'decision', target: 'end' } },
                { data: { id: 's5', source: 'decision', target: 'step1' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#28a745',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#28a745'
                    }
                },
                {
                    selector: 'node[id="decision"]',
                    style: {
                        'shape': 'diamond',
                        'background-color': '#ffc107',
                        'text-outline-color': '#ffc107'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'dagre',
                // rankDir: 'TB'  // top-to-bottom
                rankDir: 'LR' // 由左到右 排列
            }
        });
    </script>
</body>
</html>

sample3

鐵路模擬,增加火車在鐵軌上移動的動畫

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Railway</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>Railway</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'station1', label: '車站 1' } },
                { data: { id: 'station2', label: '車站 2' } },
                { data: { id: 'station3', label: '車站 3' } },
                { data: { id: 'checkpoint4', label: '檢查點 4' } },
                { data: { id: 'checkpoint5', label: '檢查點 5' } },
                { data: { id: 'checkpoint6', label: '檢查點 6' } },
                { data: { id: 's1', source: 'station1', target: 'station2' } },
                { data: { id: 's2', source: 'station2', target: 'station3' } },
                { data: { id: 's3', source: 'station2', target: 'checkpoint4' } },
                { data: { id: 's4', source: 'checkpoint4', target: 'checkpoint5' } },
                { data: { id: 's5', source: 'checkpoint5', target: 'station3' } },
                { data: { id: 's6', source: 'checkpoint5', target: 'checkpoint6' } },

                // 列車節點
                { data: { id: 'train1', label: '🚆' }, classes: 'train' }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'ellipse',
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'node[id^="station"]',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#17a2b8',
                        'text-outline-color': '#17a2b8'
                    }
                },
                {
                    selector: 'node.train',
                    style: {
                        'background-color': 'red',
                        'shape': 'ellipse',
                        'label': 'data(label)',
                        'font-size': 24,
                        'width': 30,
                        'height': 30
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle'
                    }
                }
            ],

            layout: {
                name: 'breadthfirst',
                directed: true,
                padding: 20
            }
        });

        function moveAlongEdge(train, fromNode, toNode, duration, callback) {
            const start = fromNode.position();
            const end = toNode.position();
            const startTime = performance.now();

            function animate(now) {
                const elapsed = now - startTime;
                const t = Math.min(elapsed / duration, 1); // 0~1
                const x = start.x + (end.x - start.x) * t;
                const y = start.y + (end.y - start.y) * t;
                train.position({ x, y });

                if (t < 1) {
                    requestAnimationFrame(animate);
                } else if (callback) {
                    callback();
                }
            }

            requestAnimationFrame(animate);
        }

        function moveTrain(path) {
            let i = 0;
            const train = cy.getElementById('train1');

            function step() {
                if (i >= path.length - 1) return;
                const fromNode = cy.getElementById(path[i]);
                const toNode = cy.getElementById(path[i + 1]);

                moveAlongEdge(train, fromNode, toNode, 2000, () => {
                    i++;
                    step();
                });
            }

            step();
        }

        // 定義路徑
        const route = ['station1', 'station2', 'checkpoint4', 'checkpoint5', 'station3'];

        // 初始化列車位置
        cy.getElementById('train1').position(cy.getElementById(route[0]).position());

        // 2 秒後啟動列車
        setTimeout(() => moveTrain(route), 2000);
    </script>
</body>
</html>

沒有留言:

張貼留言