前回から選択項目別の金額推移グラフページを実装しています。
前編では、ホームページ円グラフ横の表からパラメータを渡して遷移する実装と、月別金額推移グラフのページの検索フォームを実装するまで行いました。
今回は後編として渡ってきたデータをもとにグラフを表示する実装と、グラフ下に詳細データを表示する実装を行います。
月別金額推移グラフ
・円グラフ横表の項目<td>にリンクを追加(不明以外)
・検索フォーム実装
・棒グラフを表示
・グラフ下に詳細データを表示
棒グラフを表示
前回、グラフを表示するために必要なデータを渡す実装を行いました。
今回はこの渡ってきたデータを用いて、chart.jsプラグインで棒グラフを表示します。
まずはグラフを描画する要素を追加します。
以下を前回実装した検索フォームの下に追記します。
<div class="bar-graph" id="barGraph">
<canvas id="canvas"></canvas>
</div>
続いてchart.js本体とプラグインをCDN形式で読み込ませます。
以下をfunctions.jsを読み込んでいる記述の下に追記します。
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
以上でグラフ描画の準備が完了しました。
続いてはグラフにするデータを生成します。
データ生成は以前円グラフを実装したときと同様に、今回もPHPで抽出したデータをJSON配列にしてJavaScriptに渡しグラフを描画します。
以下を先程準備で追加した<canvas>の上に追記します。
<?php
//月配列
$month_list = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
//データを入れる多次元配列の用意
$data = array();
//1月~12月まですべて0円の初期多次元配列データを生成
//初期化データを用意しないと、データのない月が抽出されずグラフに0円と表示されないため
for ($i = 0; $i < count($month_list); $i++) :
$data[] = [
'year_month' => $year . '-' . $month_list[$i],
'amount' => '0'
];
endfor;
//LEFT(date, 7) as monthでdateカラムの頭から7文字分「YYYY-mm」を切り出した値とamountカラムの合計値を抽出
//・user_idが一致する
//・渡ってきたテーブル情報と同名のrecordsテーブル内カラムが渡ってきた項目idと一致する
//・dateカラムが渡ってきた年の文字列を含む
//上記3点を条件に置く
$sql = "SELECT LEFT(date, 7) as month, SUM(amount)
FROM records
WHERE user_id=? AND {$item_table}=? AND date LIKE ?
//テーブル名と同名のrecordsカラムと年月カラムでグループ化
GROUP BY {$item_table}, month
ORDER BY month ASC";
$stmt = $db->prepare($sql);
$year_param = $year . '%'; //WHERE date LIKE ?の?に入れるワイルドカードを含む文字列生成
$stmt->bind_param('iis', $user_id, $item, $year_param);
$stmt->execute();
$stmt->bind_result($month, $sum);
while ($stmt->fetch()) :
//データを入れている初期多次元配列から年月「YYYY-mm」が一致する配列番号を取得
$key = array_search($month, array_column($data, 'year_month'));
//上記で取得した$data[番目]のkeym名:amountの値を抽出したデータに書き換え
$data[$key]['amount'] = $sum;
endwhile;
//JSON形式にする配列を用意
$year_month = [];
$sum = [];
//連想配列をfor文で回し、それぞれの配列にセットする
for ($i = 0; $i < count($month_list); $i++) :
//「YYYY-mm」形式を「YYYY年mm月」形式に変換して配列に追加
$year_month[] = str_replace('-', '年', $data[$i]['year_month']) . '月';
//合計金額を配列に追加
$sum[] = $data[$i]['amount'];
endfor;
//JSONに変換
$json_year_month = json_encode($year_month);
$json_amount = json_encode($sum);
?>
少し複雑なプログラムなので多次元配列の中がどの様になっているのか説明します。
まず最初のfor文で初期の多次元配列を生成している部分の中身です。
はじめのfor文では上記のように、year_monthには渡ってきた年と$month_listを「-」で結合させた文字列が入り、amountには全て「0」が入っています。
続いてのSQLでは下記のようなデータが取り出されます。(登録しているデータによってデータ数や値は変わります)
データの抽出ができたら、生成していた$data多次元配列の中で、year_monthに上記画像monthカラムの値が一致するものを探し、その配列のamountに上記画像SUM(amount)カラムの値を上書きします。
データを抽出した際に、データがある月のamountにはその金額が入り、データがない月は「0」のままの多次元配列が出来上がります。
多次元配列完成後のJSON式に変換する箇所は、ほぼ円グラフを出力したときと同様なので説明は割愛します。
ここでrecordsテーブルのカラム名「credit」を「creditcard」に変更する必要があります。
これはSQLで$item_tableを使用しており、ここにクレジットカードで入る値が「creditcard」のため、既存のカラム名ではグラフにするデータが正しく抽出されません。
カラム名を変えたので、既存SQL抽出プログラムで「credit」となっている部分を「creditcard」に修正します。
以上で棒グラフにするデータが揃ったのでJavaScriptで棒グラフを描画するプログラムを記述します。
以下を先程作成したonChangeItem関数の下に追記します。
const canvas = document.getElementById('canvas'); //グラフを描画するcanvasを取得
window.onload = function() { //画面が読み込まれたら
//棒グラフを描画
new Chart(canvas.getContext('2d'), {
type: 'bar', //グラフのタイプ、bar -> 棒グラフ
data: {
labels: <?php echo $json_year_month; ?>, //X軸のラベル、PHPで抽出した年月配列をセット
datasets: [{
data: <?php echo $json_amount; ?>, //棒グラフにするデータ、PHPで抽出した金額配列をセット
backgroundColor: ['#28a7e0'], //棒グラフの色
datalabels: { //金額データラベル設定
color: '#28a7e0', //文字色
font: { //フォント設定
size: '11px',
weight: 'bold'
},
anchor: 'end', // 金額データラベルの位置('end' は上端)
align: 'end', // 金額データラベルの位置('end'は上記アンカーポイントの後ろ)
formatter: function(value, context) { // 金額データラベルを「,」で区切り「円」を付け足す
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' 円';
},
},
}],
},
plugins: [ChartDataLabels], //データラベルのプラグインを読み込む
options: {
responsive: false, //レスポンシブOFF
scales: {
y: { //y軸の設定
ticks: {
callback: function(tick) { //y軸ラベルを「,」で区切り「円」を付け足す
return tick.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' 円';
},
}
},
},
plugins: {
legend: { //グラフの凡例OFF
display: false,
},
tooltip: { //ツールチップOFF
enabled: false,
},
},
},
})
}
ラベルの表示形式や色などを細かく設定したので、少し複雑なプログラムになりました。
他の各設定は公式サイトに記載があるので参考にしてみてください。
上記を記述すると以下のようにデータがある項目では棒グラフが描画されるようになります。
現在はcanvasのスタイルを設定していないので上記の幅になっています。
これをPCの際には100%に、スマホの際には850pxの固定値にして横スクロールで閲覧できるように修正します。
以下のプログラムを new Chart(canvas.getContext(‘2d’), { の上に追記します。
//canvasの高さと幅指定
const containerWidth = document.getElementById('barGraph').clientWidth; //canvasを囲っているdiv要素の幅を取得
if (window.outerWidth < 600) { //画面幅が600px未満のとき
canvas.style.width = '850px'; //canvasの幅を850pxに
} else { //それ以外(PCのとき)
canvas.style.width = containerWidth + 'px'; //取得したdiv要素の幅と同値を幅に設定
}
canvas.style.height = '400px'; //canvasの高さは400pxで固定
上記を追記すると以下のようにグラフが見やすくなります。
このままでも見やすいのですが、現状このグラフは自動でY軸目盛の間隔や最大値を出力しているため、以下のような場合に見づらくなります。
・項目として登録されているものの、1年通して使われていない時(全て0円のとき)
・1年の最大金額がy軸の最大値になっているとき(グラフ上部に余白がなくなる)
そこで配列の中を調べてから、y軸目盛の間隔と最大値を計算する処理を追加します。
以下を new Chart(canvas.getContext(‘2d’), { の上に追記します。
//y軸目盛の間隔と最大値計算処理
const aryMax = (a, b) => { //配列の頭から二つずつ比較して最大値を返す関数
return Math.max(a, b);
}
//金額配列に対して上記関数を実行し最大値を格納
const maxVal = <?php echo $json_amount; ?>.reduce(aryMax);
//y軸目盛間隔、最大値切り上げのための割る数、y軸の最大値変数定義
let stepVal, roundVal, ymax;
//最大値によってy軸目盛間隔、最大値切り上げのための割る数を設定
if (maxVal < 1000) { //0-999円
stepVal = 100;
roundVal = 100; //100で割る
} else if (maxVal < 10000) { //1,000-9,999円
stepVal = 1000;
roundVal = 1000; //1000で割る
} else if (maxVal < 50000) { //10,000-49,999円
stepVal = 5000;
roundVal = 5000; //5000で割る
} else if (maxVal < 100000) { //50,000-99,999円
stepVal = 10000;
roundVal = 10000; //10000で割る
} else { //100,000円以上
stepVal = 100000;
roundVal = 100000; //10000で割る
}
//金額の最大値によってy軸の最大値を設定
//一度最大金額を切り上げたい桁数が小数点以下になるように割ってから切り上げ→割った数をかけて桁数を戻す
let ymax = Math.ceil(maxVal / roundVal) * roundVal;
if (maxVal === 0) { //0円のとき
ymax = 1000; //強制的にy軸の最大値は1000に設定
} else if (maxVal == ymax) { //金額最大値とy軸最大値が同じ場合
ymax = ymax + stepVal; //y軸最大値に目盛間隔分を足す
}
上記で計算した結果をグラフに反映させます。
new Chart(canvas.getContext(‘2d’), { の中にある、options: → scales: →y: に最大値を設定する記述を追加し、ticks: にy軸目盛の間隔を設定する記述を追加します。
y: {
max: ymax, //追加
ticks: {
stepSize: stepVal, //追加
callback: function(tick) {
return tick.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' 円';
},
}
},
以上を追加すると以下のようにグラフが見やすくなります。
グラフの表示は完了しましたが、スマホ表示でスクロールできることがわかるようにします。
canvasタグの下にspanタグを追加します。
<canvas id="canvas"></canvas>
<span><i class="fa-regular fa-hand-pointer"></i></span> <!--追加-->
そしてCSSファイルprojectディレクトリにある、「_sections.scss」のコメントアウト部分を解除します。
//コメントアウト部分
&::before {
@include m.mq(sp) {
content: "";
position: absolute;
top: 0;
left: 0;
width: 850px;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0;
animation: fade-out 3s;
}
}
&::after {
@include m.mq(sp) {
content: "スクロールして見られます";
position: absolute;
top: 50%;
left: 50%;
width: 100%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 1.6rem;
font-weight: bold;
text-align: center;
opacity: 0;
animation: fade-out 3s;
}
}
span {
display: none;
position: absolute;
top: 60%;
left: 70%;
animation: scroll-finger 3s;
opacity: 0;
z-index: 10;
@include m.mq(sp) {
display: block;
}
i {
color: #fff;
font-size: 3rem;
}
}
ユーザーにスクロールできることがわかりやすく伝わるようになりました。
グラフ下に詳細データを表示
最後にグラフの下に月ごとにまとめた詳細データを表示させます。
処理はスマホのデータ一覧と同じものを使います。以下をグラフ要素を囲っている<section>の下に追記します。
<section class="p-section p-section__item-report-datalist">
<?php
$month_list = array();
$count_list = array();
$sql = "SELECT LEFT(date, 7) as month, COUNT(*) FROM records WHERE user_id=? AND {$item_table} = ? AND date LIKE ? GROUP BY month";
$stmt = $db->prepare($sql);
$stmt->bind_param('iis', $user_id, $item, $year_param);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows();
$stmt->bind_result($month_item, $count_item);
while ($stmt->fetch()) :
$month_list[] = $month_item;
$count_list[] = $count_item;
endwhile;
?>
<?php if ($count > 0) : ?>
<div id="groupView" class="p-sp-data-box__groupview">
<?php for ($i = 0; $i < count($month_list); $i++) :
$search_month = $month_list[$i];
?>
<div class="p-toggledate-tab js-toggle" id="date<?php echo h($search_month); ?>" onclick="onClickDataBanner('<?php echo $search_month; ?>');">
<p class="date">
<?php echo date('y年n月', strtotime($month_list[$i])); ?>
</p>
<p class="count">( <?php echo h($count_list[$i]); ?>件 )</p>
</div>
<?php
$sql_output = "SELECT records.id, records.date, records.title, records.amount, spending_category.name, income_category.name, records.type, payment_method.name, creditcard.name, qr.name, records.memo
FROM records
LEFT JOIN spending_category ON records.spending_category = spending_category.id
LEFT JOIN income_category ON records.income_category = income_category.id
LEFT JOIN payment_method ON records.payment_method = payment_method.id
LEFT JOIN creditcard ON records.creditcard = creditcard.id
LEFT JOIN qr ON records.qr = qr.id
WHERE records.date LIKE ? AND records.user_id = ? AND records.{$item_table} =?
ORDER BY date DESC, records.input_time DESC";
$stmt_output = $db->prepare($sql_output);
$month_param = $search_month . '%';
$stmt_output->bind_param('sii', $month_param, $user_id, $item);
$stmt_output->execute();
$stmt_output->bind_result(
$id,
$date,
$title,
$amount,
$spending_category,
$income_category,
$type,
$paymentmethod,
$credit,
$qr,
$memo,
);
?>
<div class="p-sp-data-box__frame hide" id="item<?php echo h($search_month); ?>">
<?php while ($stmt_output->fetch()) : ?>
<div class="p-sp-data-box item<?php echo h($id); ?>">
<div class="u-flex-box p-sp-data-box__overview <?php echo $memo !== '' ? 'hasmemo' : ''; ?>">
<p> <?php echo h($title); ?>
<span>
<?php
if ($type === 0 && $spending_category !== null) {
echo '(' . h($spending_category) . ')';
} else if ($type === 1 && $income_category !== null) {
echo '(' . h($income_category) . ')';
} else {
echo "(カテゴリー不明)";
}
?>
<i class="fa-regular fa-message" onclick="showMemo('<?php echo h($memo); ?>')"></i> </span>
</p>
<p class="<?php echo $type === 0 ? 'text-red' : 'text-blue' ?>">
<?php echo h($type) === "0" ? '-¥' . number_format($amount) : ''; ?>
<?php echo h($type) === "1" ? '+¥' . number_format($amount) : ''; ?>
</p>
</div>
<div class="p-sp-data-box__detail">
<p>
<?php
//支払い方法の出力
if ($type === 0 && $paymentmethod !== null) {
echo '支払い方法:' . h($paymentmethod);
} else if ($type === 1) {
echo "";
} else {
echo "支払い方法:不明";
}
?>
</p>
<?php if ($paymentmethod === "クレジット" || $paymentmethod === "スマホ決済") : ?>
<p>
<?php
//クレジット、スマホ決済の詳細出力
if ($paymentmethod === "クレジット") {
if ($credit !== null) {
echo 'カード種類:' . h($credit);
} else {
echo "カード種類:不明";
}
} else if ($paymentmethod === "スマホ決済") {
if ($qr !== null) {
echo 'スマホ決済種類:' . h($qr);
} else {
echo "スマホ決済種類:不明";
}
}
?>
</p>
<?php endif; ?>
</div>
<div class="u-flex-box p-sp-data-box__button">
<form action="./record-edit.php" method="post">
<input type="hidden" name="record_id" value="<?php echo h($id); ?>">
<input type="submit" class="c-button c-button--bg-green edit" id="" value="編 集">
</form>
<a class="c-button c-button--bg-red delete" id="delete<?php echo h($id); ?>sp" href='./delete.php?id=<?php echo h($id); ?>;' onclick="deleteConfirm('<?php echo h($title); ?>', 'delete<?php echo h($id); ?>sp');">削 除</a>
</div>
</div>
<?php endwhile; ?>
</div>
<?php endfor; ?>
</div>
<?php else : ?>
<div class="p-sp-data-box nodata">
<p>データがありません</p>
</div>
<?php endif; ?>
</section>
以前スマホのデータ一覧表示のプログラムと多少異なる部分はありますが、ここでは詳細な説明は割愛します。
以上を追加するとグラフの下にデータリストが表示されます。
以上で月別金額推移グラフのページの実装は完了です。
最後に
前回から2回に分けて、月別金額推移グラフのページを実装しました。
次回はデータ入力部分をより使いやすくするために修正をします。
最後までお読みいただきありがとうございました。
コメント