06/14(Sun)

PHPで折れ線グラフをつくるまで[4]

PHPで折れ線グラフをつくるまで[3]

先日、折れ線グラフ作成奴を公開しました。
内部的にはPHPとjavascriptで動いているのですが、その仕組みについての備忘録も兼ねて書いていこうと思います。
前回の続きです。

機能概略【再掲】

  1. パラメータを指定してAjaxで送信する
  2. PHPでパラメータを受け取り画像をつくる
  3. 受け取った画像を表示する

2に入ります

PHPでパラメータを受け取り画像をつくる

パラメータを受け取る


        // 画像サイズ
        $param['width'] = $_POST['width'];
        $param['height'] = $_POST['height'];
        $param['margin'] = [$_POST['margin0'], $_POST['margin1'], $_POST['margin2'], $_POST['margin3']];
        
        // 座標系
        $param['min'] = min($_POST['min'], $_POST['max']);
        $param['max'] = max($_POST['min'], $_POST['max']);
        $param['step'] = $_POST['step'];
        
        // 色
        $param['color']['BG_COLOR']   = cnvColor($_POST['BG_COLOR']);
        $param['color']['AXIS_COLOR'] = cnvColor($_POST['AXIS_COLOR']);
        $param['color']['GRID_COLOR'] = cnvColor($_POST['GRID_COLOR']);
        $param['color']['LINE_COLOR'] = cnvColor($_POST['LINE_COLOR']);
        $param['color']['TEXT_COLOR'] = cnvColor($_POST['TEXT_COLOR']);
        
        // テキスト
        $param['ttfpath'] = 'font.ttf';
        $param['fontsize'] = $_POST['fsize'];
        
        // データ
        $param['separator'] = [];
        $param['value'] = explode(',', $_POST['value']);
        $param['label'] = explode(',', $_POST['label']);
      

送られてきたパラメータをまとめて配列に入れます。
色の部分はなんか関数を通してますが次で説明します。
データの部分はカンマ区切りの文字列になっているので分割して配列にしています。

16進数色表記を10進数に変換


        // 16進数色表記を変換
        function cnvColor($s){
          $r = hexdec(substr($s, 1, 2));
          $g = hexdec(substr($s, 3, 2));
          $b = hexdec(substr($s, 5, 2));
          return [$r, $g, $b];
        }
      

色のパラメータは「#RRGGBB」の16進数表記になってるので
各成分ごとに切り出して10進数に直してやります。
返り値は3つの整数値を含む配列です。

イメージリソースの作成


        // イメージリソースの作成
        $img = imagecreatetruecolor($param['width'], $param['height']);
        imageantialias($img, true);
      

image-create-truecolorという名前の長い関数でイメージリソースを作成します。
返り値はあちこちで使います。
キャンバスを設定して次からこのキャンバスを指定する感じでしょうか。
アルファチャンネルを犠牲にしてアンチエイリアスも設定しておきます。

使いまわす値の定数化


        // 座標系
        define('X0', 0); 
        define('X1', count($param['value'])); 
        define('Y0', $param['min']); 
        define('Y1', $param['max']); 
        
        // 色
        foreach ($param['color'] as $name => $rgb){
          define($name, setColor($rgb));
        } unset($rgb);
        
        // テキスト
        define('FPATH', $param['ttfpath']);
        define('FSIZE', $param['fontsize']);
      

何回か使うことになる各種の値を定数として宣言しておきます。
X0,X1,Y0,Y1はそれぞれ右左下上端の計算上の値 です。
上から何px、などの描画座標とは異なります。
9行目のsetColor()は次で説明します。

色設定


        // 色設定 @[R, G, B]
        function setColor($p){
          global $img;
          return imagecolorallocate($img, $p[0], $p[1], $p[2]);
        }
      

RGBそれぞれの値を受け取って、GDで利用できる色のリソースを返します。
描画時にこの色設定を渡すことになります。

描画(背景)


        // 描画
        imagefill($img, 0, 0, BG_COLOR);
      

背景を描画します。塗りつぶすだけです。

座標変換関数


        // 座標変換 @[x, y]
        function cl($p){
          global $param;
          $m = $param['margin'];
          
          // 係数
          $cx = ($param['width'] - ($m[1] + $m[3])) / X1;
          $cy = ($param['height'] - ($m[0] + $m[2])) / (Y1 - Y0);
          
          // 座標変換
          $x = $p[0] * $cx + $m[3];
          $y = (Y1 - $p[1]) * $cy + $m[0];
          
          return [$x, $y];
        }
      

計算上の座標は右図のようになっています。
x座標は左端を0として、データの境界を1,2,3,...としているので
グラフを始める座標は中央の0.5からになります。
縦軸はデータの値をそのまま使用しています。
描画処理を行う場合はこれを「上から*px」「左から*px」に直さないといけないので
その変換を行う関数を準備してやります。

描画関数(直線)


        // 直線 @[x1, y1], [x2, y2], color, double
        // double = true で 0.5pxずらして3本重ねる
        function drawLine($f, $t, $c, $d){
          global $img;
          imageline($img, $f[0], $f[1], $t[0], $t[1], $c);
          if ($d){
            $x = $t[0] - $f[0];
            $y = $t[1] - $f[1];
            $cc = sqrt(pow($x, 2) + pow($y, 2));
            $dx = ($y / $cc) * 0.5;
            $dy = ($x / $cc) * 0.5;
            
            imageline($img, $f[0]-$dx, $f[1]+$dy, $t[0]-$dx, $t[1]+$dy, $c);
            imageline($img, $f[0]+$dx, $f[1]-$dy, $t[0]+$dx, $t[1]-$dy, $c);
          }
        }
      

直線を描画する関数を準備します。
引数は座標1[x,y]、座標2[x,y]、色、太線フラグの4つです。
座標1から座標2まで指定された色で線を引きます。
太線フラグがtrueのときは両側0.5pxずつずらした位置にさらに2本引きます。
結果として太さが2倍の線に見えます。

描画関数(テキスト)


        // 文字 @[x, y], text, align(左上0, Z方向1-8)
        function drawText($l, $t, $a){
          global $img;

          // バウンディングボックス
          $b = imagettfbbox(FSIZE, 0, FPATH, $t);
          $w = $b[2] - $b[6];
          $h = $b[3] - $b[7];
          
          // 文字揃え
          $ah = $a % 3;
          $av = floor($a / 3);
          if ($ah == 0) $x = $l['0'];
          if ($ah == 1) $x = $l['0'] - ($w / 2);
          if ($ah == 2) $x = $l['0'] - $w;
          if ($av == 0) $y = $l['1'] + $h;
          if ($av == 1) $y = $l['1'] + ($h / 2);
          if ($av == 2) $y = $l['1'];
          
          imagettftext($img, FSIZE, 0, $x, $y, TEXT_COLOR, FPATH, $t);
        }
      

テキストを書きこむ関数を準備します。
引数は座標[x,y]、文字列、基準点の3つです。
あらかじめ縦横の長さを測っておき、基準点によって座標を決めます。
左上を基準点とする場合は0、中央上が1、右上が2、左中央が3、……という感じで
0-8の数字で指定します。

描画関数(横目盛線)


        // X方向グリッドの描画
        function drawGridX(){
          global $param;
          $step = $param['step'];
          
          // 最下グリッド位置
          $y = ceil(Y0 / $step) * $step;
          
          while ($y <= Y1){
            $l = [X0, $y];
            $r = [X1, $y];
            drawLine(cl($l), cl($r), GRID_COLOR, false);
            
            $y += $step;
          }
        }
      

横方向の目盛りを描画する関数を準備します。
step間隔で下から線を引いていきます。

描画関数(グラフ)


        // 折れ線グラフの描画
        function drawLineGraph(){
          global $param;
          
          // 左端位置
          $x = 0.5;
          
          foreach ($param['value'] as $v){
            $n = [$x, $v];
            if (isset($p)){
              drawLine(cl($p), cl($n), LINE_COLOR, 2);
            }
            
            $p = $n;
            $x += 1;
          } unset($v);
        }
      

グラフを描画する関数を準備します。
一つ前の座標から新しい点の座標までの直線を引くことになります。
実際の座標に変換してdrawLine関数につっこみます。

描画関数(軸)


        // 軸の描画
        function drawAxis(){
          $b = [0, Y0];
          $t = [0, Y1];
          $l = [X0, Y0];
          $r = [X1, Y0];
          drawLine(cl($t), cl($b), AXIS_COLOR, true);
          drawLine(cl($l), cl($r), AXIS_COLOR, true);
        }
      

軸を描画する関数を準備します。
縦横1本ずつ引いてやります。

描画関数(Y軸数値)


        // Y軸数値の描画
        function drawNumText(){
          global $param;
          $step = $param['step'];
          
          // 最下グリッド位置
          $y = ceil(Y0 / $step) * $step;
          if ($y == 0) $y = 0;
          
          while ($y <= Y1){
            $l = cl([X0, $y]);
            $l[0] -= 10;
            drawText($l, $y, 5);
            
            $y += $step;
          }
        }
      

Y軸の数値を書き込む関数を準備します。
右揃えにしたいのでalignは5を指定します。
10行目からのループは横目盛線と同様です。

描画関数(X軸ラベル)


        // X軸ラベルの描画
        function drawLabel(){
          global $param;
          
          // 左端位置
          $i = 0;
          $x = 0.5;
          
          foreach ($param['label'] as $v){
            
            // 複数選択 '$\d+'を含む
            if (strpos($v, '$') > 0){
              $a = explode('$', $v);
              $p = ($x * 2 + $a[1] - 1) / 2;
              $t = $a[0];
              
              $i += $a[1];
              $x += $a[1];
            }else if($v != ''){
              $p = $x;
              $t = $v;
              
              $i += 1;
              $x += 1;
            }else{
              continue;
            }
            
            $l = cl([$p, Y0]);
            $l[1] += 10;
            drawSeparatorX($i);
            drawText($l, $t, 1);
            
          } unset($v);
        }
      

X軸のラベルと区切りを書き込む関数を準備します。
ラベルに「$+整数値」がある場合はその数だけ結合して真ん中にラベルを表示させます。
変数iが区切りの位置を、xがラベルの中央位置を表します。
テキストは上中央揃えにしたいのでalignは1を指定します。
31行目のdrawSeparatorX関数は次に説明します。

描画関数(X軸区切り)


        // X軸セパレータの描画
        function drawSeparatorX($i){
          $p1 = cl([$i, Y0]);
          $p2 = cl([$i, Y0]);
          $p2[1] -= 10;
          drawLine($p1, $p2, AXIS_COLOR, false);
        }
      

x座標を受け取って軸上に区切りを描画する関数です。

長くなってしまったのでここらへんで切ります