図形をグリグリ動かすためのCytoscape+ Electron入門

図形をグリグリ動かすためのCytoscape + Electron入門

本記事は図形をグリグリ動かせるアプリケーションを作るために図形ライブラリ使ってみたCytoscape編です。

JavaScriptを使っているもののデスクトップアプリケーションを作りたかったのでElectronを使用してます。

図形をグリグリ動かせる!無料JS図形ライブラリ5選比較してみた

図形をグリグリ動かすためのSigma.js+ Electron入門

図形をグリグリ動かすためのJSPlumb+ Electron入門

本記事👉図形をグリグリ動かすためのCytoscape+ Electron入門

図形をグリグリ動かすためのmxGraph+ Electron入門

図形をグリグリ動かすためのD3.js+ Electron入門

今回お試しで作ったやつはこんな感じ

Cytoscapeの使い方

Node.jsのインストール

本記事では、electron上で目的のライブラリを動作するため、まずはelectronを動かすうえで必要となるnode.jsをインストールしましょう。

Nodejs – どこでもJavaScriptを使おう

ソースコード

package.json

{
  "name": "sample",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "electron ."
  },
  "license": "ISC",
  "devDependencies": {
    "electron": "^35.1.4"
  },
  "dependencies": {
    "cytoscape": "^3.28.1"
  }
}

main.js

const { app, BrowserWindow } = require('electron')
const path = require('path')
​
function createWindow () {
 const win = new BrowserWindow({
   width: 800,
   height: 600,
   webPreferences: {
     nodeIntegration: true
  }
})
​
 win.loadFile('index.html')
}
​
app.whenReady().then(() => {
 createWindow()
 
 app.on('activate', () => {
   if (BrowserWindow.getAllWindows().length === 0) {
     createWindow()
  }
})
})
​
app.on('window-all-closed', () => {
 if (process.platform !== 'darwin') {
   app.quit()
}
})

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Cytoscape Example</title>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
        }
        #cy {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
        }
        .controls {
            position: absolute;
            top: 10px;
            left: 10px;
            background: rgba(255, 255, 255, 0.8);
            padding: 10px;
            border-radius: 5px;
            z-index: 1000;
        }
        button {
            margin: 5px;
            padding: 5px 10px;
        }
    </style>
</head>
<body>
    <div class="controls">
        <button id="circleLayout">Circle Layout</button>
        <button id="gridLayout">Grid Layout</button>
        <button id="randomLayout">Random Layout</button>
    </div>
    <div id="cy"></div>
    <script src="node_modules/cytoscape/dist/cytoscape.min.js"></script>
    <script src="renderer.js"></script>
</body>
</html>

 

 

renderer.js

document.addEventListener('DOMContentLoaded', () => {
    let nodeCount = 0;
    const cy = cytoscape({
        container: document.getElementById('cy'),
        style: [
            {
                selector: 'node',
                style: {
                    'background-color': '#666',
                    'label': 'data(id)',
                    'width': 40,
                    'height': 40
                }
            },
            {
                selector: 'edge',
                style: {
                    'width': 3,
                    'line-color': '#ccc',
                    'target-arrow-color': '#ccc',
                    'target-arrow-shape': 'triangle'
                }
            }
        ],
        elements: {
            nodes: [
                { data: { id: 'a' } },
                { data: { id: 'b' } },
                { data: { id: 'c' } }
            ],
            edges: [
                { data: { source: 'a', target: 'b' } },
                { data: { source: 'b', target: 'c' } },
                { data: { source: 'c', target: 'a' } }
            ]
        },
        layout: {
            name: 'circle'
        }
    });

    // ノードの追加
    cy.on('tap', function(evt) {
        if (evt.target === cy) {
            const pos = evt.position;
            const newNodeId = 'node' + (++nodeCount);
            cy.add({
                group: 'nodes',
                data: { id: newNodeId },
                position: { x: pos.x, y: pos.y }
            });
        }
    });

    // ノードの削除
    cy.on('cxttap', 'node', function(evt) {
        const node = evt.target;
        cy.remove(node);
    });

    // レイアウトの切り替え
    document.getElementById('circleLayout').addEventListener('click', () => {
        cy.layout({ name: 'circle' }).run();
    });

    document.getElementById('gridLayout').addEventListener('click', () => {
        cy.layout({ name: 'grid' }).run();
    });

    document.getElementById('randomLayout').addEventListener('click', () => {
        cy.layout({ name: 'random' }).run();
    });

    // ズーム機能
    cy.on('mousewheel', function(evt) {
        const zoom = evt.originalEvent.wheelDeltaY > 0 ? 1.1 : 0.9;
        cy.zoom({
            level: cy.zoom() * zoom,
            renderedPosition: { x: evt.renderedPosition.x, y: evt.renderedPosition.y }
        });
    });

    // ノードのドラッグ&ドロップ
    cy.nodes().on('drag', function(evt) {
        const node = evt.target;
        node.style('background-color', '#ff6b6b');
    });

    cy.nodes().on('dragfree', function(evt) {
        const node = evt.target;
        node.style('background-color', '#666');
    });
}); 

ソースコード解説

基本的なセットアップ

const cy = cytoscape({
    container: document.getElementById('cy'),  // 描画するコンテナ要素
    style: [  // ノードとエッジのスタイル定義
        {
            selector: 'node',
            style: {
                'background-color': '#666',
                'label': 'data(id)',
                'width': 40,
                'height': 40
            }
        },
        {
            selector: 'edge',
            style: {
                'width': 3,
                'line-color': '#ccc',
                'target-arrow-color': '#ccc',
                'target-arrow-shape': 'triangle'
            }
        }
    ]
});

ノードとエッジの追加

elements: {
    nodes: [
        { data: { id: 'a' } },
        { data: { id: 'b' } },
        { data: { id: 'c' } }
    ],
    edges: [
        { data: { source: 'a', target: 'b' } },
        { data: { source: 'b', target: 'c' } },
        { data: { source: 'c', target: 'a' } }
    ]
}

クリックイベントによるノードの追加

cy.on('tap', function(evt) {
    if (evt.target === cy) {
        const pos = evt.position;
        const newNodeId = 'node' + (++nodeCount);
        cy.add({
            group: 'nodes',
            data: { id: newNodeId },
            position: { x: pos.x, y: pos.y }
        });
    }
});

クリックイベントによるノードの削除

cy.on('cxttap', 'node', function(evt) {
    const node = evt.target;
    cy.remove(node);
});

ドラッグ & ドロップ

cy.nodes().on('drag', function(evt) {
    const node = evt.target;
    node.style('background-color', '#ff6b6b');
});

cy.nodes().on('dragfree', function(evt) {
    const node = evt.target;
    node.style('background-color', '#666');
});

レイアウト変更

// サークルレイアウト
cy.layout({ name: 'circle' }).run();

// グリッドレイアウト
cy.layout({ name: 'grid' }).run();

// ランダムレイアウト
cy.layout({ name: 'random' }).run();

実行

ライブラリのインストール

npm install

アプリを起動!

npm start

終わりに

図形ライブラリって結構ソースコードの量が大きくなったり、複雑になってりすることが多いと思うんですがCytoscapeは比較的コード量が少なめで読みやすくていいですね!

デザインや機能的な面に関しても十分なライブラリといった感じで、だいぶ優秀なライブラリな印象です。

参考

Cytoscape

https://github.com/fujarenpaw/codeForBlog/tree/main/code/electron/Cytoscape

本記事で使用したソースコードはGithubにあります。

最新情報をチェックしよう!