具体例で学ぶ!情報可視化のテクニック

第4回ツリーマップによる木構造の可視化(後編)

前回に引き続き、ツリー構造のデータを可視化する「ツリーマップ」の手法を扱います。今回は、グラフィックスAPIのJava2Dを使用して、実際にツリーマップを描画し、画像ファイルへ出力を行います。

ソースコードのダウンロード

今回作成するプログラムのソースコードは、こちらから一括してダウンロードすることができます。ZIPファイルを展開して生成されるフォルダを、プロジェクトとしてNetBeansに読み込むことも可能です。

Java2Dによるグラフィックス

Javaには、Java2Dと呼ばれる2次元グラフィックスAPIが標準で付属しています。Java2Dの仕様は大変充実しており、例えば以下のような機能がサポートされています。

  • 基本図形の描画
  • テキストの描画
  • パスの作成
  • 交差判定
  • 座標系の拡大・縮小・回転
  • アンチエイリアス
  • グラデーション
  • 半透明描画
  • 画像ファイルの入出力

視覚的に美しい情報可視化を行うにあたっては、高機能なグラフィックライブラリの使用が必須となります。Java2Dは、その有力な選択肢の一つとなるでしょう。

色集合のツリーマップ

今回は、Java2Dを用いて、色データの集合を対象としたツリーマップの描画に挑戦します。ツリーマップは、以下の要件にしたがって作成することにします。

  • 第2回のアルゴリズムにしたがって、色データ集合のクラスタリングを実行し、クラスタツリーを作成する。
  • クラスタリングの距離関数には、第3回のWard法を使用する。
  • 第3回のアルゴリズムにしたがって、ツリーマップの領域分割を行う。
  • ツリーマップ領域の面積比率は、ランダムに決定する。
  • ツリーマップの各領域は、ツリーノードに対応する色で塗りつぶす。
  • ツリーマップの各領域は、ツリーノードの深さに応じた太さで輪郭を描画する。ルートノードを最も太く、末端ノードに近づくほど細く描画する。

この結果出力されるイメージは、例えば図1のような見た目になります。分割された各領域の面積、色、そして輪郭の太さによって、データの全体的な構造を直感的に理解することができます。

図1
図1 treemap

ツリーマップの描画

それでは、ツリーマップの描画ロジックを作成していきましょう。

まず始めに、TreeMapRendererインターフェイスを用意します。TreeMapRendererは、指定された長方形領域の内部にツリーマップを描画するrender()メソッドを持ちます。

リスト1 TreeMapRenderer.java
public interface TreeMapRenderer {
    void render(Graphics2D g, Node node, Rectangle2D bounds);
}

そして、このインターフェイスを実装したBinaryTreeMapRendererクラスを作成します。BinaryTreeMapRendererクラスは、次のような方針で描画を行います。

  • ノードが末端なら、領域を確定し、内部を塗りつぶす。
  • ノードが末端でなければ、領域を分割し、子ノードを再帰的に処理する。
  • 子ノードの処理が終了後、親ノードの輪郭を描画する。

上記の方針に対応する実際の部分コードを、順番に見ていきましょう。なお、これらの部分コードを含むBinaryTreeMapRendererクラスの完全なソースコードは、こちらをご覧ください。

末端ノードの塗りつぶし

以下は、ノードが末端の色データである場合の処理です。bounds変数に格納されている現在の長方形領域を、ノードの色を使って塗りつぶしています。

リスト2 BinaryTreeMapRenderer.java(部分)
    if (node instanceof ColorItem) {
        g.setPaint(((ColorItem) node).getColor());
        g.fill(bounds);
    }

ノード領域の分割

以下は、ノードがクラスタを示している場合の処理です。

リスト3 BinaryTreeMapRenderer.java(部分)
    } else if (node instanceof Cluster) {
        Cluster cluster = (Cluster) node;
        // 子ノードchild1とchild2を取得
        Node child1 = cluster.getLeft();
        Node child2 = cluster.getRight();

        // 子ノードの面積比を計算 (A)
        double area1 = child1.getArea();
        double area2 = child2.getArea();
        double ratio1 = area1 / (area1 + area2);
        double ratio2 = area2 / (area1 + area2);

        double x = bounds.getX();
        double y = bounds.getY();
        double w = bounds.getWidth();
        double h = bounds.getHeight();

        Rectangle2D rect1;
        Rectangle2D rect2;
        // 領域分割を実行 (B)
        if (w > h) {
            // boundsが横長の場合、左右に分割
            rect1 = new Rectangle2D.Double(x, y, ratio1 * w, h);
            rect2 = new Rectangle2D.Double(x + ratio1 * w, y, ratio2 * w, h);
        } else {
            // boundsが縦長の場合、上下に分割
            rect1 = new Rectangle2D.Double(x, y, w, ratio1 * h);
            rect2 = new Rectangle2D.Double(x, y + ratio1 * h, w, ratio2 * h);
        }
        // 子ノードを再帰的に処理 (C)
        doRender(g, child1, rect1, depth + 1);
        doRender(g, child2, rect2, depth + 1);
    }

始めに、クラスタが持つ2つの子ノードのgetArea()メソッドを呼び出し、面積比率を求めます(A⁠⁠。次に、その面積比率にしたがって、長方形を左右または上下に分割します(B⁠⁠。最後に、それぞれの子ノードを再帰的に処理します(C⁠⁠。

ノードの輪郭の描画

ノードの塗りつぶしと、輪郭の描画の実行順序には注意が必要です。なぜなら、先に描画した輪郭が、その後の塗りつぶしによって上書きされてしまう可能性があるからです。この問題を避けるには、全ての子ノードの処理が終わった後に、輪郭を描画するようにします。

以下が、実際の輪郭の描画コードになります。depth変数で与えられるノードの深さを元にしてストロークを作成し、輪郭線の幅を指定しています。

リスト4 BinaryTreeMapRenderer.java(部分)
    float borderWidth = Math.max(8f - depth, 1f);
    g.setStroke(new BasicStroke(borderWidth));
    g.setPaint(Color.BLACK);
    g.draw(bounds);

デモプログラムの実行

それではいよいよ、このBinaryTreeMapRendererクラスを使用して、ツリーマップの描画を実践してみましょう。

以下が、ツリーマップを生成し、画像ファイルに出力するDemoクラスになります。このクラスは第2回のDemoクラスと似ていますが、色データが100個に増えていること、階層的クラスタリングの距離関数を最短距離法からWard法に変更していること、そしてクラスタリング結果をコンソールではなくツリーマップに書き出していることが異なります。

Demoクラスでは、OUTPUT_FILE_NAME定数で指定されたファイル名で画像を保存します。定数は、実行環境に応じて適宜書き換えてください。

リスト5 Demo.java
public class Demo {
    // 出力ファイル名
    private static final String OUTPUT_FILE_NAME = "C:/visualization/treemap.png";

    public static void main(String[] args) {
        new Demo().run();
    }

    public void run() {
        List<Item> input = new ArrayList<Item>();
        // ランラムな色データを100個作成
        Random rand = new Random();
        for (int i = 0; i < 100; i++) {
            Color color = new Color(rand.nextFloat(), rand.nextFloat(), rand
                    .nextFloat());
            double area = 0.2 + 0.8 * rand.nextDouble();
            input.add(new ColorItem(color, area));
        }

        // Ward法に基づく階層的クラスタリングを準備
        DistanceEvaluator evaluator = new WardDistanceEvaluator();
        ClusterBuilder builder = new ClusterBuilder(evaluator);

        // クラスタリングを実行
        Node result = builder.build(input);

        // クラスタリング結果を表示
        output(result);
    }

    private void output(Node node) {
        // 400x400ピクセルの画像を作成
        BufferedImage image = new BufferedImage(400, 400,
                BufferedImage.TYPE_INT_RGB);

        // グラフィックオブジェクトを作成
        Graphics2D g = image.createGraphics();

        // 背景を白で塗りつぶす
        g.setPaint(Color.WHITE);
        g.fillRect(0, 0, image.getWidth(), image.getHeight());

        // ツリーマップの描画を実行
        TreeMapRenderer renderer = new BinaryTreeMapRenderer();
        Rectangle2D bounds = new Rectangle2D.Double(20, 20, 360, 360);
        renderer.render(g, node, bounds);

        // 画像をPNGファイルに保存
        try {
            ImageIO.write(image, "png", new File(OUTPUT_FILE_NAME));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Demoクラスを実行すると、図2の画像ファイルが出力されます。まるでステンドグラスのように、美しい画像を作成することができました。このイメージからは、クラスタリングによって似た色同士が階層状に集まっている様子も、大変分かりやすく把握することができます。

図2
図2 treemap

入力データはランダムに生成しているため、出力される画像は実行するたびに異なります。いろいろなパターンの画像が得られますので、イメージをじっくり観察してみてください。

また、以下の観点から考察や実験をしてみると、さらに面白いかもしれません。

  • 入力データの数をさらに増やすと、ツリーマップはどうなるか?
  • 階層的クラスタリングの距離関数をWard法から最短距離法に戻すと、ツリーマップはどうなるか?

まとめと次回予告

今回は、グラフィックスAPIのJava2Dを利用して、ツリーマップをグラフィカルに描画し、画像ファイルに出力するプログラムを作成しました。この実践を通して、ツリーマップが非常に効果的な情報可視化手法であることがお分かりいただけたかと思います。

さて、これまで作成したプログラムの動作確認では、人為的に用意したデータを入力していました。しかし、情報可視化は現実のデータに適用してこそ価値を持つものです。

次回からは、はてなブックマークが提供しているWeb APIを利用し、はてなブックマークのデータを可視化する方法を考えていきます。

おすすめ記事

記事・ニュース一覧