Cytoscape.jsとは
Cytoscape.js(http://js.cytoscape.org/):
グラフ理論(ネットワーク)の可視化と解析のためのライブラリです。ライセンスは MIT License なので、商用にも使えます。下記デモで利用している依存ライブラリもMIT Licenseです。
試してみた
次のURLをクリックすると、デモ用ページに遷移します。
Cytoscape.js demo(https://madilloar.github.io/pub-pages/cytoscape1.html)
使い方:
- 「Layout:」コンボボックスで、レイアウトを選択すると、グラフのノード配置が変わります。
- 「読込」ボタンは、その右にあるテキストエリアに書いてあるノードとエッヂの情報に従って、ノードのグラフを書きます。
- 「保存」ボタンは、その右にあるテキストエリアに、現在のグラフにあるノードの属性情報をJSON文字列で書き込みます。
- 「ノード」をクリックすると、そのクリックイベントを拾って、ノードの状態を変えます。
- 「ノード」を右クリックすると、その右クリックイベントを拾って、ノードの状態をalert表示します。
- 灰色の「グラフエリア」をクリックすると、新しいノードを作ります。
- ドラッグするとグラフが移動します。マウスホイールでグラフをズームイン/ズームアウトします。
- 左の方にある、丸っこい図形とスライダーはズームイン/ズームアウトするコントールです。
覚書
ノードやエッヂに任意の属性をつけられる
ノードに必須の属性は「id」。親ノードを示すときは「parent」属性に親の「id」を指定するのが必須。
それ以外は、任意に属性を加えられる。例えば下記例では、「name」属性を独自に入れてます。
このデモでは、ノードに「name」属性を表示するようにしていますが、下記例のように、「¥n」を文字列の途中に入れておいて、スタイルで「node.css("text-wrap", "wrap");
」と「wrap」するようにすると、ノード内の文字列が改行してくれます。
{ group: 'nodes', data: { id: 'n24', name: 'テキスト\n折り返しの\nテスト', parent: 'n39' } },
ノードのスタイルを動的に変える
ボタンをクリックしたイベントをトリガーにノードのスタイルを変えるコードの断片。
$("#IdBtnRead").click(function () {
// ボタンイベント内で、スタイルの設定
setStyles(cy.nodes(), cy.edges());
// スタイル設定の後にlayout().run();でグラフに反映。
});
下記例では、ノードの属性に任意に付け加えた「type
属性」で親か子かを判定しています。
var setStyles = function (nodes, edges) {
// ノードのスタイル
nodes.forEach(function (node) {
var data = node.json().data;
if (!data.type) {
// 子ノードのサイズとスタイル
} else if ((data.type).match(/^g/)) {
// 親ノードのスタイル
}
});
// エッヂのスタイル
edges.forEach(function (edge) {
});
};
グラフエリアの要素をセレクタで選択してイベントを拾う
cy.on("cxttap", "node", function (evt) {
の「"node"
」の部分がセレクタで、
この例では、全てのノードの中で、右クリック("cxttap"
)されたノードでイベントが発火します。
cy.on("cxttap", "node", function (evt) {
var tgt = evt.target;
var data = tgt.json().data;
var id = data["id"];
var m = "";
m += "ノードの右クリックイベントでアラートしてますが、GETとかPOSTするのもよいかも。";
m += "{id:" + id + ", x:" + tgt.position("x") + ", y:" + tgt.position("y") + "}";
alert(m);
});
## クリックした要素によって処理を分ける
下記例では、セレクタを使わないで、グラフエリアでクリックされた要素で処理を分けています。
「if (tgt === cy) {
」の様に、比較にオブジェクトを使うので、扱いにくいです。
cy.on("tap", function (evt) {
var tgt = evt.target;
if (tgt === cy) {
// cyをtapした場合は、ノードを追加
} else {
// ノードをタップした場合
});
要素の入れ替え
「var elements = cy.elements()
」で全要素(ノード、エッヂ)を取得して、「cy.remove(elementes)
」の引数に渡すことで、全要素を削除します。
「cy.add(eval(JSON文字列));
」で要素を追加します。
$("#IdBtnRead").click(function () {
var elements = cy.elements();
cy.remove(elements);
cy.add(eval($("#IdElements").val()));
setStyles(cy.nodes(), cy.edges());
CytLayout.setLeyout(cy, $("#IdLayout").val());
});
ノードの状態をJSON文字列で取得
「var nodes = cy.nodes();
」で全ノードを取得して、ループの中で「JSON.stringify(node.json());
」でJSON文字列化します。
$("#IdBtnSave").click(function () {
var s = "";
var nodes = cy.nodes();
nodes.forEach(function (node) {
s += JSON.stringify(node.json());
s += "\n";
});
$("#IdElementsPosition").val(s);
});
参考URL
Cytoscape.jsを用いてデータを可視化する
iVis-at-Bilkent/cytoscape.js-fcose
ソース
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cytoscape example</title>
<link rel="stylesheet" type="text/css"
href="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-panzoom/2.5.3/cytoscape.js-panzoom.css">
<link rel="stylesheet" type="text/css"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/all.min.css">
<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<script src="https://unpkg.com/cytoscape@3.8.1/dist/cytoscape.min.js"></script>
<script src="https://unpkg.com/numeric@1.2.6/numeric-1.2.6.js"></script>
<script src="https://unpkg.com/layout-base@1.0.1/layout-base.js"></script>
<script src="https://unpkg.com/cose-base@1.0.0/cose-base.js"></script>
<!--cytoscape-fcose:Dependencies
Cytoscape.js ^3.2.0
numeric.js ^1.2.6
cose-base ^1.0.0
-->
<script src="https://unpkg.com/cytoscape-fcose@1.0.0/cytoscape-fcose.js"></script>
<!-- cytoscape-panzoom:Dependencies
jQuery ^1.4 || ^2.0 || ^3.0
Cytoscape.js ^2.0.0 || ^3.0.0
Font Awesome 4 (for automatic icons), or you can specify your own class names for icons
-->
<script src="https://unpkg.com/cytoscape-panzoom@2.5.3/cytoscape-panzoom.js"></script>
<style>
body {
font-family: helvetica neue, helvetica, liberation sans, arial, sans-serif;
font-size: 10px;
}
#IdCytoscape {
position: absolute;
top: 150px;
left: 0px;
width: 100%;
height: 100%;
z-index: 999;
background-color: #e9e9e9;
}
</style>
<script>
var CytLayout = (function () {
var _setLayout = function (cy, layoutName) {
var layout = {
name: layoutName,
fit: true,
animate: true
};
cy.layout(layout).run();
return layout;
};
return {
setLeyout: _setLayout
};
})();
document.addEventListener("DOMContentLoaded", function () {
var setStyles = function (nodes, edges) {
// ノードのスタイル
nodes.forEach(function (node) {
var data = node.json().data;
if (!data.type) {
// 子ノードのサイズとスタイル
/*
// ノードのサイズをランダムで求めていますが、普通はノードの属性から値を取ると思います。
var width = [30, 70, 110];
var size = width[Math.floor(Math.random() * 3)];
node.css("width", size);
node.css("height", size);
*/
// ラベルの幅と高さのサイズにします。
node.css("width", "label");
node.css("height", "label");
node.css("padding", "20px");
node.css("content", data.name || data.id);
node.css("text-justification", "left")
node.css("text-valign", "center");
node.css("text-halign", "center");
node.css("text-wrap", "wrap");
node.css("shape", "round-rectangle");
node.css("background-color", "#ffcb4f");
} else if ((data.type).match(/^g/)) {
// 親ノードのスタイル
var colors = ["#B2EDCE", "#4eb7d9", "#ffdaf4"];
node.css("content", data.name);
node.css("text-valign", "top");
node.css("background-color", colors[Math.floor(Math.random() * 3)]);
}
});
// エッヂのスタイル
edges.forEach(function (edge) {
var data = edge.json().data;
edge.css("content", data.id);
edge.css("curve-style", "taxi");
edge.css("target-arrow-shape", "triangle");
});
};
var cy = cytoscape({
container: $("#IdCytoscape"),
ready: function () {
setStyles(this.nodes(), this.edges());
},
elements: eval($("#IdElements").val()),
});
// パン、ズームイン/ズームアウトコントロールの配置
cy.panzoom({});
CytLayout.setLeyout(cy, $("#IdLayout").val());
// cyのセレクタ"node"でイベントを拾う
cy.on("cxttap", "node", function (evt) {
var tgt = evt.target;
var data = tgt.json().data;
var id = data["id"];
var m = "";
m += "ノードの右クリックイベントでアラートしてますが、GETとかPOSTするのもよいかも。";
m += "{id:" + id + ", x:" + tgt.position("x") + ", y:" + tgt.position("y") + "}";
alert(m);
});
// cy要素自体でイベントを拾う
cy.on("tap", function (evt) {
var tgt = evt.target;
if (tgt === cy) {
// cyをtapした場合は、ノードを追加
cy.add({
data: { id: 'new' + Math.round(Math.random() * 100) },
position: {
x: evt.position.x,
y: evt.position.y
}
});
} else {
// ノードをタップした場合
var data = tgt.data();
var m = "";
m += "ノードのクリックイベントで属性を追加してます。\n";
m += "{name+id:" + data["id"] + ",\nx:" + tgt.position("x") + ", y:" + tgt.position("y") + "}";
data.name = m;
}
setStyles(cy.nodes(), cy.edges());
CytLayout.setLeyout(cy, $("#IdLayout").val());
});
$("#IdLayout").change(function () {
CytLayout.setLeyout(cy, $("#IdLayout").val());
});
$("#IdBtnRead").click(function () {
var elements = cy.elements();
cy.remove(elements);
cy.add(eval($("#IdElements").val()));
setStyles(cy.nodes(), cy.edges());
CytLayout.setLeyout(cy, $("#IdLayout").val());
});
$("#IdBtnSave").click(function () {
var s = "";
var nodes = cy.nodes();
nodes.forEach(function (node) {
s += JSON.stringify(node.json());
s += "\n";
});
$("#IdElementsPosition").val(s);
});
});
</script>
</head>
<body>
<h1>Cytoscape demo</h1>
<table border="1">
<tr>
<td>
<label for="IdLayout">Layout:</label>
<select name="NmLayout" id="IdLayout">
<option value="fcose" selected>fcose</option>
<option value="grid">grid</option>
<option value="random">random</option>
<option value="circle">circle</option>
<option value="concentric">concentric</option>
<option value="breadthfirst">breadthfirst</option>
<option value="cose">cose</option>
</select>
</td>
<td>
<button id="IdBtnRead">読込</button>
<textarea name="NmElements" id="IdElements" cols="30" rows="5">
[
{ group: 'nodes', data: { id: 'n0' } },
{ group: 'nodes', data: { id: 'n1' } },
{ group: 'nodes', data: { id: 'n2' } },
{ group: 'nodes', data: { id: 'n3' } },
{ group: 'nodes', data: { id: 'n4', parent: 'n37' } },
{ group: 'nodes', data: { id: 'n5' } },
{ group: 'nodes', data: { id: 'n6' } },
{ group: 'nodes', data: { id: 'n7', parent: 'n37' } },
{ group: 'nodes', data: { id: 'n8', parent: 'n37' } },
{ group: 'nodes', data: { id: 'n9', parent: 'n37' } },
{ group: 'nodes', data: { id: 'n10', parent: 'n38' } },
{ group: 'nodes', data: { id: 'n12' } },
{ group: 'nodes', data: { id: 'n13' } },
{ group: 'nodes', data: { id: 'n14' } },
{ group: 'nodes', data: { id: 'n15' } },
{ group: 'nodes', data: { id: 'n16' } },
{ group: 'nodes', data: { id: 'n17' } },
{ group: 'nodes', data: { id: 'n18' } },
{ group: 'nodes', data: { id: 'n19' } },
{ group: 'nodes', data: { id: 'n20' } },
{ group: 'nodes', data: { id: 'n21' } },
{ group: 'nodes', data: { id: 'n22' } },
{ group: 'nodes', data: { id: 'n23' } },
{ group: 'nodes', data: { id: 'n24', name: 'テキスト\n折り返しの\nテスト', parent: 'n39' } },
{ group: 'nodes', data: { id: 'n25', parent: 'n39' } },
{ group: 'nodes', data: { id: 'n26', parent: 'n42' } },
{ group: 'nodes', data: { id: 'n27', parent: 'n42' } },
{ group: 'nodes', data: { id: 'n28', parent: 'n42' } },
{ group: 'nodes', data: { id: 'n29', parent: 'n40' } },
{ group: 'nodes', data: { id: 'n31', parent: 'n41' } },
{ group: 'nodes', data: { id: 'n32', parent: 'n41' } },
{ group: 'nodes', data: { id: 'n33', parent: 'n41' } },
{ group: 'nodes', data: { id: 'n34', parent: 'n41' } },
{ group: 'nodes', data: { id: 'n35', parent: 'n41' } },
{ group: 'nodes', data: { id: 'n36', parent: 'n41' } },
{ group: 'nodes', data: { id: 'n37', type: 'g37', name: 'グループ37' } },
{ group: 'nodes', data: { id: 'n38', type: 'g38', name: 'グループ38' } },
{ group: 'nodes', data: { id: 'n39', type: 'g39', name: 'グループ39', parent: 'n43' } },
{ group: 'nodes', data: { id: 'n40', type: 'g40', name: 'グループ40', parent: 'n42' } },
{ group: 'nodes', data: { id: 'n41', type: 'g41', name: 'グループ41', parent: 'n42' } },
{ group: 'nodes', data: { id: 'n42', type: 'g42', name: 'グループ42', parent: 'n43' } },
{ group: 'nodes', data: { id: 'n43', type: 'g43', name: 'グループ43' } },
{ group: 'edges', data: { id: 'e0', source: 'n0', target: 'n1' } },
{ group: 'edges', data: { id: 'e1', source: 'n1', target: 'n2' } },
{ group: 'edges', data: { id: 'e2', source: 'n2', target: 'n3' } },
{ group: 'edges', data: { id: 'e3', source: 'n0', target: 'n3' } },
{ group: 'edges', data: { id: 'e4', source: 'n1', target: 'n4' } },
{ group: 'edges', data: { id: 'e5', source: 'n2', target: 'n4' } },
{ group: 'edges', data: { id: 'e6', source: 'n4', target: 'n5' } },
{ group: 'edges', data: { id: 'e7', source: 'n5', target: 'n6' } },
{ group: 'edges', data: { id: 'e8', source: 'n4', target: 'n6' } },
{ group: 'edges', data: { id: 'e9', source: 'n4', target: 'n7' } },
{ group: 'edges', data: { id: 'e10', source: 'n7', target: 'n8' } },
{ group: 'edges', data: { id: 'e11', source: 'n8', target: 'n9' } },
{ group: 'edges', data: { id: 'e12', source: 'n7', target: 'n9' } },
{ group: 'edges', data: { id: 'e13', source: 'n13', target: 'n14' } },
{ group: 'edges', data: { id: 'e14', source: 'n12', target: 'n14' } },
{ group: 'edges', data: { id: 'e15', source: 'n14', target: 'n15' } },
{ group: 'edges', data: { id: 'e16', source: 'n14', target: 'n16' } },
{ group: 'edges', data: { id: 'e17', source: 'n15', target: 'n17' } },
{ group: 'edges', data: { id: 'e18', source: 'n17', target: 'n18' } },
{ group: 'edges', data: { id: 'e19', source: 'n18', target: 'n19' } },
{ group: 'edges', data: { id: 'e20', source: 'n17', target: 'n20' } },
{ group: 'edges', data: { id: 'e21', source: 'n19', target: 'n20' } },
{ group: 'edges', data: { id: 'e22', source: 'n16', target: 'n20' } },
{ group: 'edges', data: { id: 'e23', source: 'n20', target: 'n21' } },
{ group: 'edges', data: { id: 'e25', source: 'n23', target: 'n24' } },
{ group: 'edges', data: { id: 'e26', source: 'n24', target: 'n25' } },
{ group: 'edges', data: { id: 'e27', source: 'n26', target: 'n38' } },
{ group: 'edges', data: { id: 'e29', source: 'n26', target: 'n39' } },
{ group: 'edges', data: { id: 'e30', source: 'n26', target: 'n27' } },
{ group: 'edges', data: { id: 'e31', source: 'n26', target: 'n28' } },
{ group: 'edges', data: { id: 'e33', source: 'n21', target: 'n31' } },
{ group: 'edges', data: { id: 'e35', source: 'n31', target: 'n33' } },
{ group: 'edges', data: { id: 'e36', source: 'n31', target: 'n34' } },
{ group: 'edges', data: { id: 'e37', source: 'n33', target: 'n34' } },
{ group: 'edges', data: { id: 'e38', source: 'n32', target: 'n35' } },
{ group: 'edges', data: { id: 'e39', source: 'n32', target: 'n36' } },
{ group: 'edges', data: { id: 'e40', source: 'n16', target: 'n40' } }
]
</textarea>
</td>
<td id="IdElementPositions">
<button id="IdBtnSave">保存</button>
<textarea name="NmElementsPosition" id="IdElementsPosition" cols="30" rows="5"></textarea>
</td>
</tr>
<tr>
<td colspan="3">
<div id="IdCytoscape"></div>
</td>
</tr>
</table>
</body>
</html>