見出し画像

Laravelからグラフを出力する

はじめに

本稿ではLaravelを利用してグラフ表示機能を作成する方法について解説していきます。

体重データを記録して増減を折れ線グラフで表示するためのサンプルについて解説します。グラフ表示はJavaScriptのライブラリであるChart.jsを利用するため、最小限のJavaScriptの知識が必要となります。

※ Laravel8で動作確認していますが、Laravel7.xでも動作する内容です。

最終的に表示されるグラフはこのようなものです。

スクリーンショット 2020-11-29 19.57.06


■ 課題

・グラフ表示を行うためのモデルを作成
・ダミーデータをSeederを使って登録する
・グラフを表示

■ 設計(体重データ)

体重データを記録するためのモデルとデーターベースは次の設計で行います。

・Model:WeightLog
・テーブル:weight_log
・カラム
 ・date_key 日付キー/文字列/YYYYMMDDの形式
 ・weight 体重/数値/小数点付の値

複数人の体重を記録する場合はuser_idなどを入れることがありますが、シンプルに体重のみをグラフにしたいと思います。

体重記録は毎日ない場合もあるので、無い場合も考えてグラフ表示をしないといけません。


# プロジェクトの準備

今回のサンプル用にプロジェクトの準備を行います。プロジェクトディレクトリは「graph_app」としていますが、自由に設定してください。


1. プロジェクトの作成

composer create-project --prefer-dist laravel/laravel graph_app
cd graph_app

composer create-projectでプロジェクトを初期化したらcdで作成したディレクトリに移動するのを忘れないようにしましょう。


2. .envを編集してデーターベースの設定

.envを編集してお使いの環境に合わせてデーターベースのセットアップを行ってください。


3. 開発サーバーの準備

php artisan serve


4. ブラウザで確認

ブラウザで「http://127.0.0.1:8000/」を開いてLaravelのスタートアップ画面が開くことを確認してください。

Laravel7とLaravel8で表示が異なります。

スクリーンショット 2020-11-29 20.07.57



# マイグレーションの作成(体重データ)

マイグレーションはartisanコマンドで作成します。「weight_log」テーブルを作成するため、マイグレーションのファイル名を「create_weight_log_table」とします。

php artisan make:migration create_weight_log_table

作成されたマイグレーションファイルを開いて編集します。

マイグレーションファイルは「database/migrations」ディレクトリに作成されます。マイグレーションファイルは作成タイミングでファイル名が変わります。

マイグレーションファイルで「weight_log」テーブルの作成を定義します。日付キーと体重を保持するカラムを追加しています。

database/migrations/2020_11_29_045857_create_weight_log_table.php


<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWeightLogTable extends Migration
{
   /**
    * Run the migrations.
    *
    * @return void
    */
   public function up()
   {
       Schema::create('weight_log', function (Blueprint $table) {
			$table->id();
			$table->string("date_key"); //日付キー
			$table->float("weight");	//体重
           $table->timestamps();
       });
   }
   /**
    * Reverse the migrations.
    *
    * @return void
    */
   public function down()
   {
       Schema::dropIfExists('weight_log');
   }
}

作成が終わりましたらマイグレーションを実行します。

php artisan migrate

マイグレーションのコードに問題がなければ次のように表示されます。

Migrating: 2020_11_29_045857_create_weight_log_table
Migrated: 2020_11_29_045857_create_weight_log_table (2.28ms)


# モデルの作成(体重データ)

作成したweight_logテーブルを読み書きするためのモデルの作成を行います。

体重データモデルのクラス名は「WeightLog」とします。

モデルクラスはartisanコマンドで作成します。

(Laravel8)
php artisan make:model WeightLog
(Laravle7)
php artisan make:model Models/WeightLog

作成されたモデルは「app/Models/WeightLog.php」に作成されます。Laravel7の場合は作成先が異なるためコマンドの引数が異なる点に注意してください。

WeightLogクラスとweight_logテーブルを関連付けるためにWeightLogテーブルを編集します。

app/Models/WeightLog.php

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class WeightLog extends Model
{
	use HasFactory;
	
	protected $table = "weight_log";
}


protected $table = "weight_log";

テーブルの関連付けには「protected $table」を利用します。


# Seederの作成と実行(体重データ)

ダミーの体重データを入力するためにSeederを作成します。

Seederはartisanコマンドで作成します。サンプルデータを作成するためのSeederなので、そのことがわかるようにクラス名は「WeightLogSampleSeeder」とします。

マイグレーションと異なり、Seederはクラス名表記(単語の先頭が大文字)になる点注意してください。

Seederは「database/seeders/WeightLogSampleSeeder.php」に作成されます。

作成されたSeederを編集し、半年分の体重データを生成していきます。

<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\WeightLog;
class WeightLogSampleSeeder extends Seeder
{
   /**
    * Run the database seeds.
    *
    * @return void
    */
   public function run()
   {
		
		//開始日を6ヶ月前にする
		$start = strtotime("-6 month");
		//作成する日数(180日分)
		$days = 180;
		//初期体重(50.0kg)
		$weight = 50.0;
		for($i = 0; $i < $days; $i++){
			//作成する日
			$date = $start + $i * 24 * 60 * 60;
			//体重をランダムで作成する
			//-200g〜200gで増減するようにする
			$weight += 0.1 * (2 - rand(0, 4));
			
			//保存実行
			$log = new WeightLog();
			$log->date_key = date("Ymd", $date);
			$log->weight = $weight;
			$log->save();
		}
   }
}

use App\Models\WeightLog;

WeightLogモデルを使って保存するため、use句を追加しています。

$date = $start + $i * 24 * 60 * 60;

180日分ループさせるため、作成する日を作っています。$dateの値は秒なので、開始日+何日目x24時間x60分x60秒で指定の日を作ることができます。

$weight += 0.1 * (2 - rand(0, 4));

体重を前日からランダムで-0.2kg〜+0.2kgするようにしています。
rand(0, 4)でランダムに0, 1, 2, 3, 4の値が作成されます。

rand(0,4)で計算されたランダムな値と、体重の差は次のようになります。

0の時: 0.1 * (2-0) = + 0.2
1の時: 0.1 * (2-1) = + 0.1
2の時: 0.1 * (2-2) = 0
3の時: 0.1 * (2-3) = - 0.2
4の時: 0.1 * (2-4) = - 0.2

rand()関数は正の値しか作ることが出来ませんが、このように差を使ってマイナスを含めた乱数を作成することができます。

Seederが作成されたら実行します。実行はartisanコマンドで行います。

php artisan db:seed --class=WeightLogSampleSeeder

--classでクラス名を指定します。「Database seeding completed successfully.」と表示されたら成功です。

これで体重表示用のデータを作成する準備ができました。


# 体重表示画面の作成

次に体重表示画面を作成していきます。

Laravelで画面の作成は次の3つの作業を行います。独立しているため順番は入れ替え可能ですが、今回は番号通りの順に行います。

1. Controllerの作成
2. ルーティングの設定
3. Viewの作成

事前に作成するファイル名やURLを決めておくとスムーズに行えます。今回は下記のように定めました。

- クラス名: WeightGraphController
- 体重画面表示用の関数名: show()
- 表示用URL: /graph
- テンプレートのファイル名: weight_graph.blade.php

Controllerをartisanコマンドで作成します。

php artisan make:controller WeightGraphController

作成されたWeightGraphControllerを開き、show()関数を作成します。

show()関数の役割は2つです。

1. 体重ログデータを取り出す
2. Viewにログデータを渡す


app/Http/Controllers/WeightGraphController.php

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\WeightLog;
class WeightGraphController extends Controller
{
   function show(Request $request){
		//体重ログデータを取り出す
		//今年の体重データを取り出す
		$log_list = WeightLog::where("date_key","like",date("Y") . "%")->get();
		
		//Viewにログデータを渡す
		return view("weight_graph",[
			"log_list" => $log_list
		]);
	}
}

$log_list = WeightLog::where("date_key","like",date("Y") . "%")->get();

date_keyには「YYYYMMDD」の形式の文字列でデータが保存されています。2020年11月11日であれば「20201111」という形です。

date_keyをwhere関数を利用して絞り込みを行うことが出来ます。

- 2020年11月のデータを取り出す場合は「202011から始まるデータ」
- 2020年のデータをすべて取り出す場合は「2020から始まるデータ」

〜から始まるという形で絞り込む場合where()関数と「like」演算子が利用できます。

このやり方では2020年中に実行した場合「2020から始まるデータ」をすべて取り出すことができます。

実際には取得範囲に合わせて絞り込む方がサーバーの負荷が下がるため、一月単位で取得するのが良いでしょう。


続いてルーティングを設定します。

routes/web.php

use App\Http\Controllers\WeightGraphController;
Route::get('/graph', [WeightGraphController::class,"show"])
   ->name("show_graph");

use句で利用するControllerを書くこと、定義に合わせて「/graph」とWeightGraphControllerのshow()関数の関連付けを行うことの2点を行っています。

今回は利用しませんが、ルーティングはname()関数を使って名前をつけるようにした方がよいため、"show_graph"という名前をつけています。

最後にViewを作成します。

Viewのテンプレートは自分で作る必要があるため、定義に合わせて下記ファイルを作成します。内容はひとまず仮でViewから渡されたログ一覧が表示されることを確認します。

resources/views/weight_graph.blade.php

<h1>graph</h1>
ここにグラフを表示
{{ $log_list }}

ブラウザで「http://127.0.0.1:8000/graph」を開き、次のように表示されていることを確認してください。

スクリーンショット 2020-11-29 14.58.58

表示されない、エラーが出る場合はファイル名や設定が間違っている可能性があります。


# ChartJSの組み込み

ChartJSはJavaScriptでグラフを簡単に書くことの出来るライブラリです。

ChartJSは様々なグラフを書くことが出来ますが、今回は一番簡単な線グラフを書くことにします。

ChartJSでグラフを書くには「ChartJS用のデータをLaravelで作成する」という一手間が発生します。まずはダミーのデータでグラフが表示される状態にします。

weight_graph.blade.phpを次のように書きかえてダミーデータでグラフを描画します。

resources/views/weight_graph.blade.php

<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="utf-8">
 <title>グラフ</title> 
</head>
 <body>
		<h1>グラフ</h1>
   	<canvas id="myChart"></canvas>
		<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"></script>
	<!-- グラフを描画 -->
   <script>
	//ラベル
	var labels = [
		"2020年1月",
		"2020年2月",
		"2020年3月",
		"2020年4月",
		"2020年5月",
		"2020年6月",
	];
	//平均体重ログ
	var average_weight_log = [
		50.0,	//1月のデータ
		51.0,	//2月のデータ
		52.0,	//3月のデータ
		53.0,	//4月のデータ
		54.0,	//5月のデータ
		49.0	//6月のデータ
	];
	//最大体重ログ
	var max_weight_log = [
		52.0,	//1月のデータ
		54.0,	//2月のデータ
		55.0,	//3月のデータ
		51.0,	//4月のデータ
		57.0,	//5月のデータ
		48.0	//6月のデータ
	];
	//最小体重ログ
	var min_weight_log = [
		48.0,	//1月のデータ
		47.0,	//2月のデータ
		45.0,	//3月のデータ
		44.0,	//4月のデータ
		43.0,	//5月のデータ
		49.0	//6月のデータ
	];

	//グラフを描画
   var ctx = document.getElementById("myChart");
   var myChart = new Chart(ctx, {
		type: 'line',
		data : {
			labels: labels,
			datasets: [
				{
					label: '平均体重',
					data: average_weight_log,
					borderColor: "rgba(0,0,255,1)",
         			backgroundColor: "rgba(0,0,0,0)"
				},
				{
					label: '最大',
					data: max_weight_log,
					borderColor: "rgba(0,255,0,1)",
         			backgroundColor: "rgba(0,0,0,0)"
				},
				{
					label: '最小',
					data: min_weight_log,
					borderColor: "rgba(255,0,0,1)",
         			backgroundColor: "rgba(0,0,0,0)"
				}
			]
		},
		options: {
			title: {
				display: true,
				text: '体重ログ(6ヶ月平均)'
			}
		}
   });
   </script>
   <!-- グラフを描画ここまで -->
 </body>
</html>

<canvas id="myChart"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"></script>

グラフを描画するための<canvas>タグと、ChartJSを読み込むスクリプトタグです。最小限この2個のタグが必要となります。

<!-- グラフを描画 -->
<script>
...
</script>
<!-- グラフを描画ここまで -->

上記<script>タグの内部にChartJSでグラフを書くためのコードを書いていきます。

ChartJSでグラフを書くには次のデータが必要です。

1. ラベル
2. データ

ラベルはテキストで用意します。グラフの横軸に表示するためのデータです。データをポイントする個数分だけ用意します。

データは数値の配列で用意します。ラベルの個数に対応した数だけ用意します。

今回は2020年6月〜11月までの平均体重、最小体重、最大体重のグラフを一つのグラフに描画していきます。

実際の出力はこのような形になります。ブラウザをリロードして確認してみましょう。

スクリーンショット 2020-11-29 19.27.25

適当に作ったダミーデータなので最大と平均が入れ替わっていました。


ラベルと平均体重のデータだけ見ると対応していることがわかると思います。

//ラベル
var labels = [
	"2020年6月",
	"2020年7月",
	"2020年8月",
	"2020年9月",
	"2020年10月",
	"2020年11月",
];
//平均体重ログ
var average_weight_log = [
	50.0,	//6月のデータ
	51.0,	//7月のデータ
	52.0,	//8月のデータ
	53.0,	//9月のデータ
	54.0,	//10月のデータ
	49.0	//11月のデータ
];

この部分をダミーではなくLaravelから出せるようにするのが目標となります。

また、詳しい内容については省略しますが、ChartJSでグラフを描画するコードは次のような形式です。

var ctx = document.getElementById("【canvasタグのID】");
var myChart = new Chart(ctx, {
		type: 【種別】
		data : {
			labels: 【ラベル】,
			datasets: 【データ】
		},
		options : 【オプション】
});

データはdatasetsの中に複数書くことが出来ます。今回は平均、最大、最小の3個のコードが書かれています。

各データは次のような形で記載します。

{
	label: '平均体重',
	data: average_weight_log,
	borderColor: "rgba(0,0,255,1)",
	backgroundColor: "rgba(0,0,0,0)"
}

各データはラベル(label)、実際の数値(data)、線の色(borderColor)、背景色(backgroundColor)を指定しています。

それ以外にも様々な項目がありカスタマイズすることができます。

ChartJSの公式サイトに様々なサンプルがあるので興味のある方はご確認ください。

https://www.chartjs.org/samples/latest/



# LaravelからChartJS用にデータを作成する

続いて、Laravelから「ラベル」「平均体重データ」「最大体重データ」「最小体重データ」を作成してViewに渡すようにします。

処理を綺麗に記述するためWeightGraphControllerに「X年Y月の平均、最大、最小を計算する関数」を追加していきます。関数名は「getWeightLogData()」としています。


function getWeightLogData($date_key){
	$sum = 0;
	$max = 0;
	$min = 500;
	$logs = WeightLog::where("date_key","like",$date_key . "%")->get();

	foreach($logs as $log){
		$weight = $log->weight;
		$sum += $weight;
		$max = max($max, $weight);
		$min = min($min, $weight);
	}

	$avg = ($logs->count() > 0) ? $sum / $logs->count() : 0;

	return [
		$avg,
		$max,
		$min
	];
}

$logs = WeightLog::where("date_key","like",$date_key . "%")->get();

where()関数とlike演算を使って、$date_keyで始まったデータをすべて取り出しています。2020年06月のデータをすべて取り出す=date_keyが202006という形に読み替えてください。

$sum += $weight;

平均を出すため一度$sumに体重を合計しています。

$max = max($max, $weight);

最大を出すためにmax()関数で比較して大きい方を取得しています。

$min = min($min, $weight);

最小を出すためにmin()関数で比較して小さい方を取得しています。


PHPで全データから平均、最大、最小を計算していますが、これはPHPで計算せずSQLで作ることができます。

関数としては「平均、最大、最小」を取得できれば良いので手法は自由に決めることができます。本来のプログラムであれば速度を検討してSQLで作る方が望ましいです。余力があればチャレンジしてみてください。

show()関数を次のように書き換えてデータを構築し、Viewに取得したデータを渡すようにしましょう。


function show(Request $request){
	$avg_weihgt_log = [];
	$max_weihgt_log = [];
	$min_weihgt_log = [];

	//取り出す対象
	$target_days = [
		"202006",
		"202007",
		"202008",
		"202009",
		"202010",
		"202011",
	];

	foreach($target_days as $date_key){
		list($avg, $max, $min) = $this->getWeightLogData($date_key);
		$avg_weihgt_log[] = $avg;
		$max_weihgt_log[] = $max;
		$min_weihgt_log[] = $min;
	}

	return view("weight_graph",[
		"label" => [
			"2020年6月",
			"2020年7月",
			"2020年8月",
			"2020年9月",
			"2020年10月",
			"2020年11月",
		],
		"avg_weight_log" => $avg_weihgt_log,
		"max_weight_log" => $max_weihgt_log,
		"min_weight_log" => $min_weihgt_log,
	]);
}

$target_daysに取り出す対象のdate_keyの前半部分を直書きしています。これは本来プログラムで書くべき部分ですが、今回は分かりやすいようにこのように書いています。

コードは順に読めばわかるように書いていますが、202006〜202011までループしながらgetWeightLogData()関数を呼び出してデータを取り出し、CharJSで利用しやすいように平均、最大、最小の3つの配列に格納しています。

Viewに渡す部分でlabelを直接書いていますが、これも本来は開始月と終了月を渡してループで作るべき部分です。


続いてViewを書き換え、直接データを書いていた部分をControllerから渡された値を使うようにします。

Controllerから渡されたデータをJavaScriptに変換するにはどのようにすればよいでしょうか。Laravelには非常に便利な関数が用意されています。

resources/views/weight_graph.blade.php

//ラベル
var labels = @json($label);

//平均体重ログ
var average_weight_log = @json($avg_weight_log);

//最大体重ログ
var max_weight_log = @json($max_weight_log);

//最小体重ログ
var min_weight_log = @json($min_weight_log);

@json関数を利用して、ControllerからJavaScriptへデータを渡しています。

Bladeの@jsonを利用するとJavaScriptで読み書きしやすいJSON形式に変換して出力できます。

実際に実行すると次のようなコードに変換されます。

//ラベル
var labels = ["2020\u5e746\u6708","2020\u5e747\u6708","2020\u5e748\u6708","2020\u5e749\u6708","2020\u5e7410\u6708","2020\u5e7411\u6708"];

//平均体重ログ
var average_weight_log = [50.28666666666666,52.15161290322581,51.89354838709677,51.72666666666667,51.829032258064515,50.383333333333326];

//最大体重ログ
var max_weight_log = ["51.3","53.0","52.3","52.1","52.5","51.0"];

//最小体重ログ
var min_weight_log = ["49.6","51.5","51.5","51.4","50.9","49.9"];

このようにControllerで値を作って@jsonでJavaScriptに渡す手法は様々なパターンに応用することが出来ます。

例えば緯度経度の情報をControllerから渡せばGoogle Map上にマーカーを表示することも出来ます。

地図から探す、といった機能はこのような作り方をしています。

ブラウザで開いてみて次のように最大、平均、最小が綺麗に出ていることを確認してください。



# まとめ

この手法はControllerで作るデータを組み合わせることで様々な応用が可能です。

- 月ごとの平均ではなく週毎の平均にして3ヶ月のグラフを表示する、
- 目標値を別途表示して目標に近づいているかどうか見せる
- 1ヶ月単位で表示する

などの表示形式が考えられます。

今回はシンプルな形ですが、データをSeederで生成し、集計して表示するサンプルアプリを作成しました。

実際のアプリケーションで行う場合、データが無い日も考えられます。その場合は前の値をそのまま引き継ぐなど、ChartJSに合わせたデータをController側で作る必要があります。

また、ChartJSは円グラフを作ることが出来ます。円グラフを利用してアンケートのデータを円グラフにして表示するといったやり方も可能です。

統計データを様々なグラフにして表示する機能は色々な場所で使われています。「Controller
で作ったデータを@jsonでJavaScriptに渡す」という手法を使いこなしてください。


完成まで突っ走る意気込みです。サポートしていただけると非常に嬉しいです。応援よろしくお願いします。