GD::Graphと積み重ね棒グラフ

Linux

はじめに

データを加工してグラフ画像を作成する必要がありました。
棒グラフを積み重ねる必要があったので、当初はMATLABを使う予定でした。
しかし、色々調べるとPerlの「GD::Graph」モジュールも綺麗そうだと分かったので使ってみました。
フォントさえ用意すればLinux、Windows関係なく日本語も表示出来ます。

環境1

「Perl ver.5.12」では「Statistics::Lite」がインストール出来なかったので「ver.5.10」を使用した。

  • WindowsXP Professional SP3 32bit
  • Strawberry Perl ver.5.10.1.5
  • GDGraph ver.1.44_01

環境2

  • Debian squeeze 32bit
  • Perl ver.5.10.1
  • libgd-graph-perl(GDGraph ver.1.44-3)

データ

テキストデータだけでPerlのスクリプトは含まれません。
wp.zip

日本語フォント Migu 1P

「MigMix-1P-20110610」に含まれる「MigMix-1P-bold.ttf」を使いました。
http://mix-mplus-ipa.sourceforge.jp/migmix/

自分用のメモ

X軸は「control.txt」の目盛りを基準とするため、「control.txt」の平均値を使う。
このときにX軸データのMin、Maxが必要なので、予め、全てのデータを連結したファイルを作成して読み込む。

GDモジュールで画像サイズを小さくすると目盛り値が重なってしまうので、ある程度大きいサイズを設定する。

Y軸の値は0~Nの整数値なので、それぞれの値毎に配列を準備(二次元配列で対応)して、各々が別の棒グラフになるようにする。
それを「GD::Graph」を使って棒グラフを積み重ねることで色分けする。

「control.txt」の平均値を計算してX軸の中心とする。
X軸のデータの最小値、最大値を求めて、それらが含まれるようにX軸の目盛りの数を設定する。
一目盛り幅は、平均値の20%とする。
その目盛りの幅がBIN幅となり、その範囲に含まれるデータを加算して棒グラフで表示する。
BIN幅の左の目盛りは~以上、右の目盛りは~未満とする。

グラフ図

ソース

UTF8で保存。

#!/usr/bin/perl

use strict;
use warnings;
use utf8;
use Encode;
#use GD::Graph::mixed;
use GD::Graph::bars;
#use GD::Graph::hbars;
use Statistics::Lite qw( :all );

#use POSIX; # ceil
#use Math::Round; # 四捨五入

{
    my $infile = "control.txt";
    # my $infile = "data1.txt";
    # my $infile = "data2.txt";

    # control.txt、data1-2を合体させたもの。
    my $allfile = "data_all.txt";

    # GDで使用するフォント
    my $font_name = "MigMix-1P-bold.ttf";

    # 平均値の20%を一目盛の幅とする
    my $ratio = 0.2;

    # $allfileの読み込み
    open my $h_inall, "<", $allfile or die "$!: $allfile";
    my @data_all = <$h_inall>;
    close( $h_inall );

    my $ct_all     = 0;
    my @data_x_all = ();
    my $tmp;
    foreach my $i ( @data_all )
    {
        ( $data_x_all[ $ct_all ], $tmp ) = split(/\t/, $i);
        chomp( $data_x_all[ $ct_all ] );
        $ct_all++;
    }

    # 個別のデータ読み込み
    open my $h_in, "<", $infile or die "$!: $infile";
    my @data = <$h_in>;
    close( $h_in );

    my $ct     = 0;
    my @data_x = ();
    my @data_y = ();
    foreach my $i ( @data )
    {
        ( $data_x[ $ct ], $data_y[ $ct ] ) = split(/\t/, $i);
        chomp( $data_x[ $ct ] );
        chomp( $data_y[ $ct ] );
        $ct++;
    }

    my $mean_x   = mean @data_x;
    # my $stddev_x = stddev @data_x;
    # print "mean x org: $mean_x \n";
    # print "width org: " . ($mean_x * $ratio) . "\n";
    # print "min x: " . (min @data_x) . "\n";

    # X軸の目盛りをcontrol.txtのデータに合わせる
    $mean_x = 0.894063333333333 if ( $infile !~ /control/ );

    # データの最小値・最大値
    # my $min_x = min @data_x;
    # my $max_x = max @data_x;
    my $min_x = min @data_x_all;
    my $max_x = max @data_x_all;

    # print "min x: $min_x \n";
    # print "max x: $max_x \n";

    # X軸の目盛りの分割数(プラス側・マイナス側)
    # 小数点以下を切り捨てる。
    my $div_x_nega = abs( $mean_x - $min_x ) / ( $ratio * $mean_x );
    my $div_x_plus = abs( $mean_x - $max_x ) / ( $ratio * $mean_x );
    $div_x_nega = sprintf( "%d", $div_x_nega );
    $div_x_plus = sprintf( "%d", $div_x_plus );
    $div_x_nega += 1;
    $div_x_plus += 1;

    # X軸のマイナス側のBIN幅(二度手間だが、出力用に使う)
    my $r1;
    my $r2;
    my @bin_width_nega = ();
    my @bin_width_plus = ();
    my @bin_width      = ();
    for ( my $i = 0; $i < $div_x_nega; $i++ )
    {
        $r1 = $mean_x * ( 1 - ( 1 + $i ) * $ratio ) ;
        $r2 = $mean_x * ( 1 - $i * $ratio ) ;
        $bin_width_nega[ $i ] = $r1 . "\t" . $r2;
        # print "- $i $i: $data_x[ $i ] $range1 $r2 \n";
    }
    @bin_width_nega = reverse @bin_width_nega;

    # X軸のプラス側のBIN幅(二度手間だが、出力用に使う)
    for ( my $i = 0; $i < $div_x_plus; $i++ )
    {
        $r1 = $mean_x * ( 1 + $i * $ratio );
        $r2 = $mean_x * ( 1 + ( 1 + $i ) * $ratio );
        $bin_width_plus[ $i ] = $r1 . "\t" . $r2;
        # print "+ $i $i: $data_x[ $i ] $range1 $r2 \n";
    }
    @bin_width = ( @bin_width_nega, @bin_width_plus );

    # 実際の目盛(X軸のマイナス側)
    my @scale_x_nega = ();
    for ( my $i = 0; $i < $div_x_nega; $i++ )
    {
        $scale_x_nega[ $i ] = $mean_x - ( $ratio * $mean_x  ) / 2 - $i * $ratio * $mean_x
    }
    @scale_x_nega = reverse @scale_x_nega;

    # 実際の目盛(X軸のプラス側)
    my @scale_x_plus = ();
    for ( my $i = 0; $i < $div_x_plus; $i++ )
    {
        $scale_x_plus[ $i ] = $mean_x + ( $ratio * $mean_x ) / 2 + $i * $ratio * $mean_x
    }

    my @scale_x = ( @scale_x_nega, @scale_x_plus );

    # X軸の目盛りの桁を統一する
    foreach my $i ( @scale_x )
    {
        $i = sprintf( "%0.3f", $i );
    }
    # print "@scale_x \n\n";

    # 結果を格納する配列の初期化
    my @result = ();
    for ( my $i = 0; $i <= @data_y; $i++ )
    {
        for ( my $j = 0; $j < $div_x_nega + $div_x_plus; $j++ )
        {
            $result[ $i ][ $j ] = 0;
        }
    }

    # BIN幅に合わせたデータの仕分け
    my $range1;
    my $range2;
    my $range_found;
    for ( my $i = 0; $i < @data_x; $i++ )
    {
        # print "$i: $data_x[ $i ] $data_y[ $i ] \n";
        $range_found = 0;

        # X軸のマイナス側
        for ( my $k = 0; $k < $div_x_nega; $k++ )
        {
            $range1 = $mean_x * ( 1 - ( 1 + $k ) * $ratio ) ;
            $range2 = $mean_x * ( 1 - $k * $ratio ) ;
            if ( $range1 <= $data_x[ $i ] &amp;amp;&amp;amp; $data_x[ $i ] < $range2 )
            {
                # print "- found $i $k: $data_x[ $i ] $data_y[ $i ] $range1 $range2 \n";
                $result[ $data_y[ $i ] ][ $div_x_nega - 1 - $k ] += $data_y[ $i ]; # 加算する
                $range_found = 1;
                last;
            }
        }

        next if ( $range_found == 1 );

        # X軸のプラス側
        for ( my $j = 0; $j < $div_x_plus; $j++ )
        {
            $range1 = $mean_x * ( 1 + $j * $ratio );
            $range2 = $mean_x * ( 1 + ( 1 + $j ) * $ratio );
            if ( $range1 <= $data_x[ $i ] &amp;amp;&amp;amp; $data_x[ $i ] < $range2 )
            {
                # print "+ found $i $j: $data_x[ $i ] $data_y[ $i ] $range1 $range2 \n";
                $result[ $data_y[ $i ] ][ $j + $div_x_nega ] += $data_y[ $i ]; # 加算する
                last;
            }
        }
    }

    # Y値の重複データを削除、数値の昇順でソートする
    my %count       = ();
    my @data_y_uniq = grep( !$count{$_}++, @data_y );
    @data_y_uniq    = sort {$a <=> $b} @data_y_uniq;

    # GDに渡す配列に、X軸の目盛りを格納した配列を代入する
    my @plot_data = ();
    push @plot_data, \@scale_x;

    # データ整理
    foreach my $i ( @data_y_uniq )
    {
        my @data = ();
        for ( my $j = 0; $j < $div_x_nega + $div_x_plus; $j++ )
        {
            print "Error: $i $j \n" if ! defined $result[ $i ][ $j ];
            push @data, $result[ $i ][ $j ];
        }
        push @plot_data, \@data;
    }

    # データ範囲毎に標準出力に出力する(確認用)
    for ( my $j = 0; $j < $div_x_nega + $div_x_plus; $j++ )
    {
        my @data = ();
        foreach my $i ( @data_y_uniq )
        {
            print "Error: $i $j \n" if ! defined $result[ $i ][ $j ];
            push @data, $result[ $i ][ $j ];
        }
        my $sum = sum @data;
        # print $scale_x[ $j ] . "\t" . $sum . "\n";
        print $bin_width[ $j ] . "\t" . $sum . "\n";
    }

    # グラフ描画
    # my $gd = GD::Graph::mixed->new( 1280, 1024 );
    my $gd = GD::Graph::bars->new( 1280, 1024 );

    # 全てのグラフを棒グラフにする
    my @gd_types = ();
    for ( my $i = 0; $i < @data_y_uniq; $i++ )
    {
        $gd_types[ $i ] = 'bars';
    }

    # 「GD::Graph::mixed」のオプション
    $gd->set(
        title            => encode('utf-8', $infile),
        t_margin         => 10,
        b_margin         => 10,
        l_margin         => 10,
        r_margin         => 10,
        x_label          => encode('utf-8', "横軸"),
        x_label_position => 0.5,
        y_label          => encode('utf-8', "縦軸"),
        types            => [ @gd_types ],
        y_tick_number    => 10,
        y_label_skip     => 2,
        bar_width        => 20,
        bar_spacing      => 3,
        # overwrite      => 1, # 「$gd = GD::Graph::mixed」の場合は不要。
        cumulate         => 1 # 棒グラフを縦に積み重ねる場合は「1」
    );

    # フォントの設定
    GD::Text->font_path("./");
    $gd->set_title_font( $font_name, 14 );
    $gd->set_legend_font( $font_name, 8 );
    $gd->set_x_axis_font( $font_name, 8 );
    $gd->set_x_label_font( $font_name, 10 );
    $gd->set_y_axis_font( $font_name, 8 );
    $gd->set_y_label_font( $font_name, 8 );

    # $gd->set_legend( encode(utf-8, "xxx"),
    # encode(utf-8, "yyy"),
    # encode(utf-8, "zzz");

    my $img = $gd->plot( \@plot_data ) or die( "Cannot create image" );

    # ファイルに保存する
    my $imagefile = "gd-" . $infile . ".jpg";
    open my $h_gd, ">", "$imagefile" or die "$!: $imagefile";
    binmode $h_gd;
    print $h_gd $img->jpeg();
    close $h_gd;

    exit;
}

Comments