LGTM

Looks Good To Me

inet-henge で、好みのネットワーク図を描くヒント

2020-02-25追記: SVG DOM が変更になったため、この記事のCSS ではスタイルが壊れるかもしれません。こちら も参考にしてください。


「構成管理DB から自動でネットワーク図を描く」というコンセプトで、inet-henge というライブラリを開発しています。

ネットワークは日々変化しますが、ネットワーク図の更新が面倒です。「このデバイスはあのデバイスとつながっている」くらいの接続情報の集まりから、オートレイアウトでネットワーク図を描きたい。これがモチベーションです。

ここでは、最近入った機能を使って 好みのネットワーク図を描くためのヒントを紹介します。

計算結果のキャッシュ

描画のたびに計算結果である位置情報をキャッシュし、それをヒントにすることで2回目以降の表示が高速になりました。

  • キャッシュはブラウザに保存される
  • 入力データが変わった場合はキャッシュを使わない

キャッシュを参照せず常に計算を行うには、次のように positionCache オプションを false にします。

<script>
  var diagram = new Diagram('#diagram', 'bar.json', {positionCache: false});
  diagram.init('bandwidth', 'interface');
</script>

計算ステップ数を調整する

2回目以降はキャッシュを使えるという前提のもと、計算ステップを増やすことで よりよい配置を得つつ (2回目以降の) 表示速度を落とさない、ということが可能になりました。

このオプションを理解するために、内部で利用している D3.js のforce layout について説明します。

force layout はノード間に引力や斥力が働く場を想定し、初期配置から小刻みに時間ステップを進めながらノード配置を計算して収束させるD3.js モジュールです。ノード数やリンク数が増えるにつれ、一般的には収束までに必要な時間ステップ数が増加します。よりよいネットワーク図を得る代わりに表示時間が長くなるトレードオフがあったわけです。

これについて、過去のinet-henge では 最大時間ステップ数 = 1000 を決め打っていました 💦
表示のたびに再計算する仕様 & スピード優先 でした。

ここが可変になり、ネットワーク規模に合わせたステップ数を設定できます。デフォルトは今まで通りの 1000 です。

<script>
  var diagram = new Diagram('#diagram', 'bar.json', {
    ticks: 2000  // 2000 ステップまで計算する
  });
  diagram.init('bandwidth', 'interface');
</script>

CSS スタイリング

次のようなJSONデータを

{
  "nodes": [
    { "name": "POP1-A" },
    { "name": "POP1-B" },
    { "name": "POP2-A", "icon": "https://inet-henge.herokuapp.com/images/router.png" },
    { "name": "POP2-B", "icon": "https://inet-henge.herokuapp.com/images/router.png" },
    { "name": "POP3-A", "icon": "https://inet-henge.herokuapp.com/images/router.png" }
  ],

  "links": [
    { "source": "POP1-A", "target": "POP1-B",
      "meta": { "interface": { "source": "ge-0/{0,1}/0", "target": "Te0/{0,1}/0/0" }, "bandwidth": "20G" } },
    { "source": "POP2-A", "target": "POP2-B",
      "meta": { "interface": { "source": "ge-0/{0,1}/0", "target": "Te0/0/{0,1}/0" }, "bandwidth": "20G" } },
    { "source": "POP1-A", "target": "POP2-A",
      "meta": { "interface": { "source": "ge-0/0/1", "target": "Te0/0/0/1" }, "bandwidth": "10G" } },
    { "source": "POP1-B", "target": "POP2-B",
      "meta": { "interface": { "source": "ge-0/0/1", "target": "Te0/0/0/1" }, "bandwidth": "10G" } },
    { "source": "POP3-A", "target": "POP2-B"}
  ]
}

次のHTMLのように描画するとします。

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
  <script src="https://inet-henge.herokuapp.com/js/cola.min.js"></script>
  <script src="https://inet-henge.herokuapp.com/js/inet-henge.js"></script>
</head>

<body>
  <div id="diagram"></div>
</body>

<script>
  var diagram = new Diagram('#diagram', 'bar.json', { pop: /^([^\s-]+)-/ });
  diagram.init('bandwidth', 'interface');
</script>

</html>

デフォルトで左のような図になりますが、右のようにCSS で要素ごとにスタイルを変えることが可能です。

  • グループ、ノード、リンク全体にスタイルを当てる
  • 特定グループにスタイルを当てる (POP1)
  • 特定ノードにスタイルを当てる (POP1-A)
  • 特定リンクにスタイルを当てる (POP1-A - POP1-B 間)

下のCSSは、右の図を出力するサンプルです。
配色を変えているのに加え、リンクにマウスオーバーすると強調表示するようになっています。

(リンクラベルが重なって読めませんが、後ほど調節します)

.group rect { opacity: 0.5; }
.node, .group rect { cursor: move; }

/* 特定 node, link のみスタイルを当てる */
.group.pop1 rect { fill: #4ecdc4 !important; }
.node.pop1-a rect { fill: #c7f464 !important; }
.link.pop1-a-pop1-b { stroke: #c44d58; }

/* リンクにマウスオーバーすると、リンクを太く & ラベル表示 */
.link:hover { stroke-width: 5px; }
.link:hover ~ .path-label {
   font-weight: bold;
   visibility: visible !important;
}

リンク長さの調節

先ほどの例で、マウスオーバーしたときのリンクラベルが重なっていました。

これは、次のようにリンク長さを調節することで解決できます。

<script>
  var diagram = new Diagram('#diagram', 'bar.json', {
    pop: /^([^\s-]+)-/,
    distance: function(force) { force.jaccardLinkLengths(100, 2); }
  });
  diagram.init('bandwidth', 'interface');
</script>
  • ジャッカード距離を計算に使う
  • 共通ノードを持たないノード間の、ベースとなる長さを 100
  • ジャッカード距離に対する係数を 2

としています。

描画したい ノード数、リンク数 によっていい感じのパラメーターが変わってきますので、「見づらいな」と感じたら時々調節してみることをオススメします。

(調節はコンセプトに反するので、ラベルの重なり検知は課題のひとつ💪)

ネットワークステータスの反映

下のように、障害のあるルーターやリンクを強調表示できます。

データ:

{
  "nodes": [
    { "name": "POP1-A" },
    { "name": "POP1-B" },
    { "name": "POP2-A", "icon": "https://inet-henge.herokuapp.com/images/router.png" },
    { "name": "POP2-B", "icon": "https://inet-henge.herokuapp.com/images/faulty_router.png" },  // アイコン差し替え
    { "name": "POP3-A", "icon": "https://inet-henge.herokuapp.com/images/router.png" }
  ],

  "links": [
    { "source": "POP1-A", "target": "POP1-B",
      "meta": { "interface": { "source": "ge-0/{0,1}/0", "target": "Te0/{0,1}/0/0" }, "bandwidth": "20G" } },
    { "source": "POP2-A", "target": "POP2-B",
      "meta": { "interface": { "source": "ge-0/{0,1}/0", "target": "Te0/0/{0,1}/0" }, "bandwidth": "20G" } },
    { "source": "POP1-A", "target": "POP2-A",
      "meta": { "interface": { "source": "ge-0/0/1", "target": "Te0/0/0/1" }, "bandwidth": "10G" } },
    { "source": "POP1-B", "target": "POP2-B",
      "meta": { "interface": { "source": "ge-0/0/1", "target": "Te0/0/0/1" }, "bandwidth": "10G" } },
    { "source": "POP3-A", "target": "POP2-B", "class": "fault" }  // クラス指定
  ]
}

CSS:

.link.fault {
  stroke: #c44d58;
  stroke-dasharray: 5, 5;
}

出力:

ノードはアイコンの差し替えを、リンクは class 属性の追加によりスタイルを操作します。

補足: ほかのCSS ヒント

CSS スタイリングについて、別の記事も書きました。こちらも参考にしてみてください。

codeout.hatenablog.com

補足: JANOG41 で登壇しました

ずいぶん経ってしまいましたが、JANOG41 でinet-henge について話してきました。
LT で、事前投票 上位のトークが採択されるシステムでしたが…なんと一位 🎉🎉🎉

会期中やその後に「使ってみました」「ここ、こうなりませんか?」などなど多数のフィードバックを頂きまして、めちゃくちゃ有意義なミーティングでした。ありがとうございました!

今回紹介したヒントのほとんどが、そのときの話を参考に実装したものです。
まだまだフィードバックお待ちしてます!