LoginSignup
20
23

More than 3 years have passed since last update.

Cytoscape.jsを試してみた

Last updated at Posted at 2019-07-12

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文字列化します。

ノードの状態を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

ソース

cytoscape-1.html
<!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>


20
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
23