Laravel + Chart.jsで売上統計の円グラフをつくる

こんにちは❗フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、先日記事が300を超えたので「何か漏れている内容がないかな?」と昔の記事を見返してみることにしました。

やはり300記事もあるといろんな記事があるのですが、その中で ”やろうと思ってたのに忘れてた” ことを思い出しました。

それは・・・

データを集計して円グラフをつくる

というものです。

・・・というのも、ご依頼をいただく案件の中に「ログイン直後のページで現状をグラフ化したい」というご要望があるからなんです。

例えば、会社別の販売実績がどうなっているのか?をひと目で分かるようにしたいというご要望ですね。

そこで!

今回は、以前公開した「ログイン日時の統計をとって棒グラフにしてみる」に続いてChart.jsでの円グラフをつくる方法を紹介したいと思います。

ぜひ楽しみながらやっていきましょう❗

「chart.jsって見た目がキレイですよね」

開発環境: Laravel 7.x、Vue 2.6.11、Chart.js 2.9.3、lodash 4.17.15

この記事を見たらできること

以下のように「年ごとの会社別・販売実績」円グラフがつくれます。
また、セレクトボックスの年を切り替えると自動的に円グラフが更新されます。

では実際にやっていきましょう❗

モデル&マイグレーションをつくる

まずデータベース関連からはじめます。
以下のコマンドを実行してください。

php artisan make:model Sale -m

すると、モデルとマイグレーションが作成されるので、マイグレーションの中身を次のようにしてください。

/database/migrations/****_**_**_******_create_sales_table.php

// 省略

public function up()
{
    Schema::create('sales', function (Blueprint $table) {
        $table->id();
        $table->string('company_name')->comment('会社名');
        $table->integer('amount')->comment('販売金額');
        $table->integer('year')->comment('販売した年');
        $table->timestamps();
    });
}

// 省略

テストデータ(Seeder)をつくる

続いて、salesテーブルにテストデータを用意します。
以下のコマンドを実行してください。

php artisan make:seed SalesTableSeeder

すると、Seederファイルが作成されるのでrun()の中身を次のようにします。

/database/seeds/SalesTableSeeder.php

// 省略

public function run()
{
    $companies = [
        '株式会社 恵比寿',
        '株式会社 大黒天',
        '株式会社 毘沙門天',
        '株式会社 弁財天',
        '株式会社 福禄寿',
        '株式会社 寿老人',
        '株式会社 布袋',
    ];

    for($i = 0 ; $i < 100 ; $i++) {

        $sale = new \App\Sale();
        $sale->company = Arr::random($companies);
        $sale->amount = rand(1000, 10000) * 10;
        $sale->year = rand(2018, 2020);
        $sale->save();

    }
}

// 省略

変更が完了したら、このSalesTableSeederLaravelに登録しましょう。

/database/seeds/DatabaseSeeder.php

// 省略

public function run()
{
     $this->call(SalesTableSeeder::class); // 👈 追加
}

// 省略

最後にマイグレーションを実行します。

php artisan migrate:fresh --seed

するとテーブルはこうなります。

コントローラーをつくる

次にコントローラーを2つ作成します。

まずはブラウザからアクセスするためのコントローラーです。
以下のコマンドを実行してください。

php artisan make:controller SalesController

作成したら、中にindex()を追加します。

/app/Http/Controllers/SaleController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SaleController extends Controller
{
    public function index() { // 👈 追加

        return view('sale.index');

    }
}

そして、Ajax通信するためのコントローラーです。
同じくコマンドを実行してください。

php artisan make:controller Ajax/SalesController

こちらも中にindex()years()を追加します。

/app/Http/Controllers/Ajax/SaleController.php

<?php

namespace App\Http\Controllers\Ajax;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class SaleController extends Controller
{
    public function index(Request $request) { // 👈 追加

        return \App\Sale::select('company', 'amount')
            ->where('year', $request->year)
            ->get();

    }

    public function years() { // 👈 追加

        return \App\Sale::select('year')
            ->groupBy('year')
            ->pluck('year');

    }
}

ルートをつくる

では、ルートをつくってアクセス可能なURLをつくります。

Route::get('sales', 'SaleController@index'); // 👈 ブラウザでアクセス
Route::get('ajax/sales', 'Ajax\SaleController@index'); // 👈 売上データ取得
Route::get('ajax/sales/years', 'Ajax\SaleController@years'); // 👈 年データ取得(セレクトボックス用)

まず1行目のルートは、実際にブラウザからアクセスする部分です。(そのため、SaleControllerではビューを返すようにしました)

2行目はAjaxで販売実績データを取得するURLです。

そして最後の行は、存在する販売年のリストを取得するURLになります。
これは次のようなセレクトボックスを用意するために使います。

ビューをつくる

では、今回メインになるビューです。

/resources/views/sale/index.blade.php

<html>
<head>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div id="app" class="container p-3">
        <div class="row">
            <div class="col-md-6">

                <!-- 👇 円グラフを表示するキャンバス -->
                <canvas id="chart" width="400" height="400"></canvas>

                <!-- 👇 年を選択するセレクトボックス -->
                <div class="form-group">
                    <label>販売年</label>
                    <select class="form-control" v-model="year" @change="getSales">
                        <option v-for="year in years" :value="year">@{{ year }} 年</option>
                    </select>
                </div>

            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                sales: [],
                year: '{{ date('Y') }}',
                years: [],
                chart: null
            },
            methods: {
                getYears() {

                    // 👇 販売年リストを取得 ・・・ ①
                    fetch('/ajax/sales/years')
                        .then(response => response.json())
                        .then(data => this.years = data);

                },
                getSales() {

                    // 👇 販売実績データを取得 ・・・ ②
                    fetch('/ajax/sales?year='+ this.year)
                        .then(response => response.json())
                        .then(data => {

                            if(this.chart) { // チャートが存在していれば初期化

                                this.chart.destroy();

                            }

                            // 👇 lodashでデータを加工 ・・・ ③
                            const groupedSales = _.groupBy(data, 'company'); // 会社ごとにグループ化
                            const amounts = _.map(groupedSales, companySales => {

                                return _.sumBy(companySales, 'amount'); // 金額合計

                            });
                            const companyNames = _.keys(groupedSales); // 会社名

                            // 👇 円グラフを描画 ・・・ ④
                            const ctx = document.getElementById('chart').getContext('2d');
                            this.chart = new Chart(ctx, {
                                type: 'pie',
                                data: {
                                    datasets: [{
                                        data: amounts,
                                        backgroundColor: [
                                            'rgb(255, 99, 132)',
                                            'rgb(255, 159, 64)',
                                            'rgb(255, 205, 86)',
                                            'rgb(75, 192, 192)',
                                            'rgb(54, 162, 235)',
                                            'rgb(153, 102, 255)',
                                            'rgb(201, 203, 207)'
                                        ]
                                    }],
                                    labels: companyNames
                                },
                                options: {
                                    title: {
                                        display: true,
                                        fontSize: 45,
                                        text: '売上統計'
                                    },
                                    tooltips: {
                                        callbacks: {
                                            label(tooltipItem, data) {

                                                const datasetIndex = tooltipItem.datasetIndex;
                                                const index = tooltipItem.index;
                                                const amount = data.datasets[datasetIndex].data[index];
                                                const amountText = amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
                                                const company = data.labels[index];
                                                return ' '+ company +' '+amountText +' 円';

                                            }
                                        }
                                    }
                                }
                            });

                        });

                }
            },
            mounted() {

                this.getYears();
                this.getSales();

            }
        });

    </script>
</body>
</html>

少しコードが長いので各ブロックで紹介していきます。

① 販売年リストを取得

先ほど作ったAjax\SaleControlleryears()から「販売年リスト」を取得します。

これは、例えば販売実績が「2018年」「2019年」「2020年」に発生している場合は、その3つの数字が配列で返ることなります。

[2018,2019,2020] // 👈 販売年リスト

② 販売実績データを取得

Ajax\SaleControllerindex()から「該当する年の販売実績データ」を取得します。

例えば、2020年に発生した販売データ全てを取得することができます。

③ lodashでデータを加工

今回はせっかくデータの集計をするお話ですので、データ加工が楽にできるlodashを使った例をご用意しました。

(なお、loadashLaravelでも採用されています)

1つずつ見ていきましょう。

まず、_.groupBy()の部分ですがここでは、会社名ごとに販売データをグループ化しています。

つまり、次のような販売実績データがあったとして、

[
  { amount: 1000, company: '株式会社 弁財天'},
  { amount: 2500, company: '株式会社 弁財天'},
  { amount: 3000, company: '株式会社 毘沙門天'},
  { amount: 5500, company: '株式会社 毘沙門天'},
  { amount: 1500, company: '株式会社 毘沙門天'}
]

グループ化すると、こうなります。

{
  '株式会社 弁財天': [
    { amount: 1000, company: '株式会社 弁財天'}, 
    { amount: 2500, company: '株式会社 弁財天'},
  ],
  '株式会社 毘沙門天': [
    { amount: 3000, company: '株式会社 毘沙門天'}, 
    { amount: 5500, company: '株式会社 毘沙門天'}, 
    { amount: 1500, company: '株式会社 毘沙門天'}
  ],
}

次に_.map()の部分はこのグループ化した各会社の販売金額を合計しています。
そのため、例えば次のようなデータが取得できます。

[114910, 170930, 161800, 440800, 103110, 428900, 141670] // 👈 販売金額の合計

最後に_.keys()では、先ほどグループ化した販売実績データのキー(つまり、会社名)を取得しています。

④ 円グラフを描画

ここがChart.jsを使っている部分になります。データのセット方法は見ていただいたとおりですが、少し変則なのは、options.tooltips.callbacksの部分です。

ここでは、グラフにマウスを合わせたときに表示される内容をカスタマイズしています。

つまり、初期状態では単なる数字だけが表示されますが、これを「会社名 + 3桁カンマ区切りの数字 + 円」で表示しています。(例:株式会社 毘沙門天 1,234,567 円)

なお、セレクトボックスが変更される度にこのコードが実行されますので、チャートがすでに存在している場合を考慮して、初期化を忘れないでください。

if(this.chart) {

    this.chart.destroy();

}

こうしないと、年を変更した際に上手くグラフが描画されません。

テストしてみる

では実際にブラウザからアクセスしてみましょう❗

うまく円グラフが表示されていますね😊

次に、マウスを合わせてみます。

カスタマイズした内容が表示されました😊

最後にセレクトボックスの年を変更してみます。

円グラフが自動的に変更されました。
成功です😊✨

ダウンロードする

今回実際の開発したソースコード一式をダウンロードできます。

Laravel + Chart.jsで売上統計の円グラフをつくる

JavaScriptのライブラリもCDNで読み込んでいるので、すぐ試すことができますよ❗

ちなみに

円グラフにラベル(今回の例で言うと会社名)を表示したい場合は、以下のページを参考にしてみてください。

コピペでOK!Chart.js円グラフ外側にテキスト表示する方法(Chart PieceLabel)

おわりに

ということで今回はLaravel + Chart.jsを使って売上実績の円グラフをつくってみました。

この方法を使えば、売上実績だけでなく営業成績やクレームの分布などを円グラフにすることができます。

なお、今回の開発で感じたのは「なるほど、こういうときにGraphQLは強い❗」ということでした。この間「Laravel + Vue + GraphQLでデータ取得」という記事でも書きましたが、今回の内容をGraphQLで作ると、たった一回のAjaxで全てのデータが取得できます。

やはり開発業務は「どのテクノロジーをどう組み合わせるか」が重要になってきますね。

ぜひ皆さんもいろんなパターンを考えてみてくださいね。

ではでは〜❗

「またいつか円グラフ以外もやってみますね👍」

開発のご依頼お待ちしております 😊✨
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly  

開発効率を上げるための機材・まとめ