cola.jsによるデータビジュアライゼーション

この記事は1年以上前に書かれました。
内容が古くなっている可能性がありますのでご注意下さい。


LODチャレンジ2016のキックオフイベントで、「LOD × ジオ」というテーマのアンカンファレンスでファシリテータを務めました。テーマ説明において、「緯度経度によらない地理表現」の例として、隣接関係だけを用いて描いた東京23区のグラフを提示しました。

その際に用いたデータビジュアライゼーションのライブラリcola.jsについて解説します。

cola.jsの特長は、グラフ描画において制約(Constraint)を指定できることです。D3.jsのForce Layoutのような力学モデルの描画アルゴリズムに対して制約を追加することにより、より意図した通りのグラフを描画することができます。
今回作成する東京23区のグラフは、区をノードで、区と区の隣接関係をリンクで表すものです。

D3.jsのForce Layoutを用いて作成した上のグラフでは、隣接関係は正しく表現されているのでしょうが、リンクが交差し、地図で見られる区と区の位置関係とはかけ離れたものになってしまいました。
cola.jsでは「リンクが交差しない」という制約は指定できませんが、「ノードが重ならない」という制約を指定できるので、試してみることにします。

データの準備
プログラムに先立ち、各区の隣接関係のデータを作成します。
DBpedia JapaneseSPARQLエンドポイントで以下のSPARQLクエリ

PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
PREFIX prop-ja: <http://ja.dbpedia.org/property/>
PREFIX category-ja: <http://ja.dbpedia.org/resource/Category:>

select distinct ?name ?tonari_name where {
?s prop-ja:name ?name ;
dbpedia-owl:wikiPageWikiLink category-ja:特別区 ;
prop-ja:隣接自治体 ?tonari .
?tonari dbpedia-owl:wikiPageWikiLink category-ja:特別区 ;
prop-ja:name ?tonari_name .
}

を実行すると、区と区の隣接関係のリストを得ることができます。

プログラムでは名前よりもコードの方が扱いやすいので、以下のSPARQLにより、隣接する区のコードの対を取得します。

PREFIX dbpedia-owl:  <http://dbpedia.org/ontology/>
PREFIX prop-ja: <http://ja.dbpedia.org/property/>
PREFIX category-ja: <http://ja.dbpedia.org/resource/Category:>

SELECT DISTINCT ?scode - 13101 AS ?source ?tcode - 13101 AS ?target WHERE {
?s dbpedia-owl:wikiPageWikiLink category-ja:特別区 ;
prop-ja:code ?scode ;
prop-ja:隣接自治体 ?t .
?t dbpedia-owl:wikiPageWikiLink category-ja:特別区 ;
prop-ja:code ?tcode .
FILTER (?scode < ?tcode)
}
ORDER BY ?scode ?tcode

コードは13101をマイナスして0から始まる数字に変換し、双方向のリンクは必要ないのでFILTERを使用して片方向のみ(リンク元のコードが小さい対のみ)を取得しています。

ノードのラベルに表示する文字列は以下のSPARQLクエリで取得します。

PREFIX dbpedia-owl:  <http://dbpedia.org/ontology/>
PREFIX prop-ja: <http://ja.dbpedia.org/property/>
PREFIX category-ja: <http://ja.dbpedia.org/resource/Category:>

SELECT DISTINCT ?name WHERE {
?s dbpedia-owl:wikiPageWikiLink category-ja:特別区 ;
prop-ja:name ?name ;
prop-ja:code ?code .
}
ORDER BY ?code

データは以下のようなJSON形式にします。

var dataset = {
  nodes: [
    { name : "千代田区" },
    { name : "中央区" },
    { name : "港区" },
    { name : "新宿区" },
    { name : "文京区" },
    { name : "台東区" },
    { name : "墨田区" },
    { name : "江東区" },
    { name : "品川区" },
    { name : "目黒区" },
    { name : "大田区" },
    { name : "世田谷区" },
    { name : "渋谷区" },
    { name : "中野区" },
    { name : "杉並区" },
    { name : "豊島区" },
    { name : "北区" },
    { name : "荒川区" },
    { name : "板橋区" },
    { name : "練馬区" },
    { name : "足立区" },
    { name : "葛飾区" },
    { name : "江戸川区" }
  ],
  edges: [
    { source: 0, target: 1 },
    { source: 0, target: 2 },
    { source: 0, target: 3 },
    { source: 0, target: 4 },
    { source: 0, target: 5 },
    { source: 1, target: 2 },
    { source: 1, target: 5 },
    { source: 1, target: 6 },
    { source: 1, target: 7 },
    { source: 2, target: 3 },
    { source: 2, target: 7 },
    { source: 2, target: 8 },
    { source: 2, target: 12 },
    { source: 3, target: 4 },
    { source: 3, target: 12 },
    { source: 3, target: 13 },
    { source: 3, target: 15 },
    { source: 4, target: 5 },
    { source: 4, target: 15 },
    { source: 4, target: 16 },
    { source: 4, target: 17 },
    { source: 5, target: 6 },
    { source: 5, target: 17 },
    { source: 6, target: 7 },
    { source: 6, target: 17 },
    { source: 6, target: 20 },
    { source: 6, target: 21 },
    { source: 6, target: 22 },
    { source: 7, target: 8 },
    { source: 7, target: 10 },
    { source: 7, target: 22 },
    { source: 8, target: 9 },
    { source: 8, target: 10 },
    { source: 8, target: 12 },
    { source: 9, target: 10 },
    { source: 9, target: 11 },
    { source: 9, target: 12 },
    { source: 10, target: 11 },
    { source: 11, target: 12 },
    { source: 11, target: 14 },
    { source: 12, target: 13 },
    { source: 12, target: 14 },
    { source: 13, target: 14 },
    { source: 13, target: 15 },
    { source: 13, target: 19 },
    { source: 14, target: 19 },
    { source: 15, target: 16 },
    { source: 15, target: 18 },
    { source: 15, target: 19 },
    { source: 16, target: 17 },
    { source: 16, target: 18 },
    { source: 16, target: 20 },
    { source: 17, target: 20 },
    { source: 18, target: 19 },
    { source: 20, target: 21 },
    { source: 21, target: 22 }
  ]
};

プログラムの作成
cola.jsを用いたプログラムは以下の通りです。

var w = 800;
var h = 800;

var cola = cola.d3adaptor()
    .nodes(dataset.nodes)
    .links(dataset.edges)
    .linkDistance(100)
    .avoidOverlaps(true)
    .symmetricDiffLinkLengths(20)
    .size([w, h])
    .start();

var svg = 
  d3.select("body")
    .append("svg")
    .attr("width", w)
    .attr("height", h);

var edges = 
  svg.selectAll("line")
    .data(dataset.edges)
    .enter()
    .append("line")
    .style("stroke", "#ccc")
    .style("stroke-width", 1);

var nodes = 
  svg.selectAll("circle")
    .data(dataset.nodes)
    .enter()
    .append("circle")
    .attr("r", 15)
    .style("fill", "#ccc")
    .call(cola.drag);

var labels = 
  svg.selectAll("text")
    .data(dataset.nodes)
    .enter()
    .append("text")
    .text( function(d) {return d.name;} )
    .call(cola.drag);

cola.on("tick", function() {
  edges.attr("x1", function(d) { return d.source.x; })
       .attr("y1", function(d) { return d.source.y; })
       .attr("x2", function(d) { return d.target.x; })
       .attr("y2", function(d) { return d.target.y; });

  nodes.attr("cx", function(d) { return d.x; })
       .attr("cy", function(d) { return d.y; });

  labels.attr("x", function(d) { return d.x; })
        .attr("y", function(d) { return d.y; });

});

初めに、colaのd3adaptorオプジェクトを作成します。

var cola = cola.d3adaptor()
    .nodes(dataset.nodes)
    .links(dataset.edges)
    .linkDistance(100)
    .avoidOverlaps(true)
    .symmetricDiffLinkLengths(20)
    .size([w, h])
    .start();

d3adaptorはD3.jsのd3.layout.forceを代替するものです。
avoidOverlaps(true)がミソです。

さらに、D3.jsを使用してSVGのオプジェクトを作成し、

var svg = 
  d3.select("body")
    .append("svg")
    .attr("width", w)
    .attr("height", h);

SVGにリンク線と

var edges = 
  svg.selectAll("line")
    .data(dataset.edges)
    .enter()
    .append("line")
    .style("stroke", "#ccc")
    .style("stroke-width", 1);

ノードを描き、

var nodes = 
  svg.selectAll("circle")
    .data(dataset.nodes)
    .enter()
    .append("circle")
    .attr("r", 15)
    .style("fill", "#ccc")
    .call(cola.drag);

ノードにラベルを付けます。

var labels = 
  svg.selectAll("text")
    .data(dataset.nodes)
    .enter()
    .append("text")
    .text( function(d) {return d.name;} )
    .call(cola.drag);

以下のコードは、ノードをドラッグした時の処理です。

cola.on("tick", function() {
  edges.attr("x1", function(d) { return d.source.x; })
       .attr("y1", function(d) { return d.source.y; })
       .attr("x2", function(d) { return d.target.x; })
       .attr("y2", function(d) { return d.target.y; });

  nodes.attr("cx", function(d) { return d.x; })
       .attr("cy", function(d) { return d.y; });

  labels.attr("x", function(d) { return d.x; })
        .attr("y", function(d) { return d.y; });
});

データとプログラムを含む完全なhtmlファイルはこちらです。実行結果はこちらで見ることができます。

実行直後のグラフは、残念ながら期待とは少し違ったものでした。
北区をドラッグして上に移動すると分かりますが、下町と山の手が東西逆になっています。また、リンク線も一部交差してしまいました。ノードをうまく移動すると、冒頭で示した、地図で見られる区と区の位置関係に近いグラフにすることができます。

隣接関係のみを使用すると、鏡像反転したグラフが得られてしまう可能性があることが分かりました。
区によっては、<http://ja.dbpedia.org/property/north>や<http://ja.dbpedia.org/property/south>等のプロパティによって方角付きで隣接自治体の情報が得られる場合があります。現在のところ、残念ながらすべての区がこのプロパティを持っているとは限りませんが、このプロパティを使用して、cola.jsで制約を追加すると、より実際の位置関係に近いグラフを描くことができるかもしれません。