[php+sql+chart.js]ajaxでデータを拾ってchart.jsに反映させる方法

非同期通信ってやつですね。

やりたいこと

phpで作ったページがある。

ページを読み込むとchart.jsによりグラフが表示される。

グラフはmySQLからデータを拾ってきてる。

任意の条件を指定してグラフの内容を更新する、をやりたい。

仕組みについて

グラフはchart.jsで作る。

グラフの内容はjsで設定、mySQLにあるものを拾ってくる、ということなのでphpを絡めてjsを書いていくことになる。

そこまではいい、珍しいことじゃない。

グラフの更新をしたいっていうのが、今回の肝。

後付でデータを取ってくるためには非同期通信をしなきゃいけないのでajaxのやつを作らなきゃいけない。

jsを上書きしただけじゃ意味がないから、ちゃんと出力した内容が反映されるようにchart.jsが発火するようにもする。

そういうやつ。

下地づくり

まずは基本形。

ページを読み込んだらグラフが出るようにする。

<head>
︙
<script src="https://code.jquery.com/jquery-3.7.1.js" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
</head>
<body>
<?php

    $first_date = date("Y-m-01");
    $last_date = date("Y-m-d");
    $where_firstdate = "date >= '$first_date' ";
    $where_lastdate = "date <= '$last_date' ";

    $days = array();
    for($i=date('Ymd', strtotime($first_date)); $i<=date('Ymd', strtotime($last_date)); $i++){
       $year = substr($i, 0,4);
       $month = substr($i, 4,2);
       $day = substr($i, 6,2);
        if(checkdate ( $month , $day , $year )){
            $days[date('Y-m-d', strtotime($i))] = 0;
        }
    }
    $dataListPrice = $days;

    $whereArray = array();
    array_push($whereArray, $where_firstdate);
    array_push($whereArray, $where_lastdate);
    foreach($whereArray as $row){
        if ($row === reset($whereArray)) {
            $sqlWhere = " WHERE 1";
        }
        $sqlWhere .= " AND ( ".$row.")";
    }
    $dbh = pdo略
    $sql = "SELECT * FROM sample_table{$sqlWhere}";
    $stmt = $dbh->prepare($sql);
    $stmt->execute();

    $dataListPriceGraphAll = 0;
    foreach ($stmt as $row) {
        $date = $row['date'];
        $price = $row['price'];
        $dataListPrice["{$date}"] = $dataListPrice[$date] + intval($price);
        $dataListPriceGraphAll += intval($price);
    }

    $dataList = '';
    foreach($dataListPrice as $key => $val){
        $dataList .= '{x:"'.$key.'",y:'.$val.'}';
        if ($key !== array_key_last($dataListPrice)) {
            $dataList .= ',';
        }
    }

?>
<input type="date" name="price_start" value="<?php print $first_date; ?>">
<span>~</span>
<input type="date" name="price_end" value="<?php print $last_date; ?>">
<button type="button" id="changeCanvas" class="btn_common">click</button>
<canvas id="chart"></canvas>
期間内合計:<input type="text" readonly name="priceGraphAll" value="<?php print $dataListPriceGraphAll; ?>">

<script>
var values =[<?php print $dataList; ?>];
drawChart();
function drawChart() {
    ctx = document.getElementById('chart');
    config = {
        type: "line",
        data: {
            datasets: [{
                data: values,
            }],
        },
        options: {
            spanGaps: true,
            responsive: true,
            maintainAspectRatio: false,
            plugins: {
                legend: {
                    display: false,
                }
            }
        }
    }
    window.myChart = new Chart(ctx, config);
}
</script>
</body>

めっちゃ長いから漏れがあったら嫌だな―って思いつつ。

噛み砕く

chart.jsとjQueryを読ませるところから始まる。ajaxはjQueryのを使うしね。

<script src="https://code.jquery.com/jquery-3.7.1.js" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>

で、php。

今回は期間を指定して、期間中で登録されたデータの中の金額を日毎に出してグラフにするとする。

要するに売上表ですね。

<?php
    //期間の指定(月初から本日まで)
    $first_date = date("Y-m-01");
    $last_date = date("Y-m-d");
    //sql文のWHEREのパーツ作成(日付照合カラム名:date)
    $where_firstdate = "date >= '$first_date' ";
    $where_lastdate = "date <= '$last_date' ";

    //月初から本日まで(yyyymmdd)をkeyにした連想配列作成。値は0。
    $days = array();
    for($i=date('Ymd', strtotime($first_date)); $i<=date('Ymd', strtotime($last_date)); $i++){
       $year = substr($i, 0,4);
       $month = substr($i, 4,2);
       $day = substr($i, 6,2);
        if(checkdate ( $month , $day , $year )){
            $days[date('Y-m-d', strtotime($i))] = 0;
        }
    }
    //配列を使い回せるように別の変数名を作成
    $dataListPrice = $days;

    //sql文のWHERE部分を作成
    $whereArray = array();
    array_push($whereArray, $where_firstdate);
    array_push($whereArray, $where_lastdate);
    foreach($whereArray as $row){
        if ($row === reset($whereArray)) {
            $sqlWhere = " WHERE 1";
        }
        $sqlWhere .= " AND ( ".$row.")";
    }
    //SQL文発火、データを引っ張ってくる(テーブル名:sample_table)
    $dbh = pdo略
    $sql = "SELECT * FROM sample_table{$sqlWhere}";
    $stmt = $dbh->prepare($sql);
    $stmt->execute();

    $dataListPriceGraphAll = 0;
    foreach ($stmt as $row) {
        $date = $row['date'];
        $price = $row['price'];
        //dataが配列のkeyと等しいとき、値にpriceを加算する
        $dataListPrice["{$date}"] = $dataListPrice[$date] + intval($price);
        //無条件にpriceを加算する
        $dataListPriceGraphAll += intval($price);
    }

    //配列をchart.jsのdata形式に合わせて展開
    $dataList = '';
    foreach($dataListPrice as $key => $val){
        $dataList .= '{x:"'.$key.'",y:'.$val.'}';
        if ($key !== array_key_last($dataListPrice)) {
            $dataList .= ',';
        }
    }

?>

個別に書いたら長すぎてしんどいかったのでコメントアウト参照。

「$dataList」はChart.jsのjs部分でprintする。

次は表示部分。

<input type="date" name="price_start" value="<?php print $first_date; ?>">
<span>~</span>
<input type="date" name="price_end" value="<?php print $last_date; ?>">
<button type="button" id="changeCanvas" class="btn_common">click</button>
<canvas id="chart"></canvas>
期間内合計:<input type="text" readonly name="priceGraphAll" value="¥ <?php print $dataListPriceGraphAll; ?>">

いつからいつまで、をinputで表示。
phpの「date(“Y-m-d”)」形式はinputのdateにそのまま放り込める。

更新用にbuttonを設置、差別化のためにidを振っておく。

canvasはchart.js用のやつ。

最後にjs。

<script>
var values =[<?php print $dataList; ?>];
drawChart();
function drawChart() {
    ctx = document.getElementById('chart');
    config = {
        type: "line",
        data: {
            datasets: [{
                data: values,
            }],
        },
        options: {
            spanGaps: true,
            responsive: true,
            maintainAspectRatio: false,
            plugins: {
                legend: {
                    display: false,
                }
            }
        }
    }
    window.myChart = new Chart(ctx, config);
}
</script>

chart.jsを更新する(=また発火させる)という目的があるので、使い回せるようにchart()を関数化して発火させる書き方を採用。関数発火に合わせてdataを指定してるのもポイント。

ここではページ読み込み時に合わせて前述のphpでグラフのデータを流し込んで発火させてある。

グラフの内容を更新する

前項の内容も地味に大事だけど、ようやく本題。

やっていくのはjsの追記とphpファイルの作成。

js

いつからいつまで、を送信するとその期間の情報が返ってくる、ブラウザ上に反映するものを作る。
関数とかを使い回すので、前項のjsに追記する形になる。

<script>
var values =[<?php print $dataList; ?>];
    ︙
}

$(function(){
    $('#changeCanvas').on('click',function(){
        first_date = $('input[name="first_date"]').val();
        last_date = $('input[name="last_date"]').val();
        $.post({
            url: 'ajax.php',
            data:{
                'first_date' : first_date,
                'last_date' : last_date
            },
            dataType: 'json',
        }).done(function(data){
            $('input[name="priceGraphAll"]').val('\xA5 '+data.total_num);
            if (myChart) {
                myChart.destroy();
            }
            values = data.canvas_data;
            drawChart();
        }).fail(function(XMLHttpRequest, textStatus, errorThrown){
            console.log(errorThrown);
        })
    })
})
</script>

.ajax()より.post()のほうが良いらしいってことで採用。書式自体は大差ないですね。

まず、.post()でajax.phpへ期間指定の情報を送る。urlは相対パス。階層が違ったら調整する。

.done()ではajax.phpから受け取った情報の処理。変数をdataにしてある。
受け取った情報はjson形式だから、つまり連想配列みたいなもんだから変数名の後でkeyを指定して出力(data.***)。

chart.jsの更新と合計値の更新はここでやってる。
chart.jsを更新する場合、一旦destroy()しておくのがルールなので忘れないように。

ここで注意すべきはvalues部分。
ajax.phpの方でchart.jsに合わせた連想配列を作るので、そのまま流し込む形になる。
ページ読み込み時のvaluesはphpで直接作ったけど、こっちは受け取りと出力をjsで書いてるので違う書き方になる。ここを間違うと詰む。実際詰みかけたのでまじで注意。

.fail()はエラー処理のやつ。

php

完成すればこんなものかって話なんだけどね。

毎度形にするまでめちゃくちゃ時間くってる。

<?php
//文字コード指定
header("Content-type: application/json; charset=UTF-8");
//.post()のdata受け取り+変数化
$first_date = filter_input(INPUT_POST,'first_date');
$last_date = filter_input(INPUT_POST,'last_date');

//合計値用変数初期化
$totalNum = intval(0);

//月初から現在までをkeyにした連想配列作成、値は0。
$dataList = array();
for($i=date('Ymd', strtotime($first_date)); $i<=date('Ymd', strtotime($last_date)); $i++){
   $year = substr($i, 0,4);
   $month = substr($i, 4,2);
   $day = substr($i, 6,2);
    if(checkdate ( $month , $day , $year )){
        $dataList[date('Y-m-d', strtotime($i))] = 0;
    }
}

    //sql文作成とデータ取得
    $whereArray = array();
    array_push($whereArray, "date >= '$first_date' ");
    array_push($whereArray, "date <= '$last_date' ");
    foreach($whereArray as $row){
        if ($row === reset($whereArray)) {
            $sqlWhere = " WHERE 1";
        }
        $sqlWhere .= " AND ( ".$row.")";
    }
    $dbh = pdo略
    $sql = "SELECT * FROM sample_table{$sqlWhere}";
    $stmt = $dbh->prepare($sql);
    $stmt->execute();
    $dbh = null;

    while($row = $stmt->fetch(PDO::FETCH_ASSOC)){
        $date = $row['date'];
        $price = $row['price'];
        //カラム名date同じ連想配列のkeyの値にpriceを加算
        $dataList[$date] += intval($price);
        //無条件で加算
        $totalNum += intval($price);
    }
    //chart.js用データ作成
    $dataListResult = array();
    foreach($dataList as $key => $val){
        array_push($dataListResult,['x' => $key, 'y'=> $val]);
    }

//受け渡し用変数
$result = array();
//chart.js用データ格納
$result['canvas_data'] = $dataListResult;
//合計値用データ格納
$result['total_num'] = $totalNum;

//json形式に変換(確認用)
echo json_encode($result);
exit;

内容はページ読み込み時のそれとほぼ一緒。

jsonの受け渡し用に最初の宣言があったり、$resultに連想配列で格納したりがある。

まとめ

こういうのって、そのまま同じように使えたら良いんだけどテンプレにできる部分はかなり限られる。
作例を見て自分の環境に合わせて応用するしかないんですよね。

自分が今回大変だったのは、やっぱりajax部分。エラーらしいエラーが出ないので、薄いヒントしか分からないんで、思いつく限りの書き直しで何回もトライアンドエラーを繰り返した。無知って大変ですね。

地味にchart.jsのdataが連想配列っぽいなにかじゃなくて連想配列そのものだったということが理解できてなかったのもしんどかった。php側で同じ形の文字列を作ってそれをjsで放り込んだ場合、文字列だから書き出したら前後を「’」で囲うことになる。そうすると配列として破綻するので通らない。みたいなこととか。基礎知識がないからこういうことになる。

でもまあ、今までは作りながら理解する感じのことばっかりだったのが、次に繋がる感じで書けるようになってきた。余力というか事前知識があれば思いつけることは多い。

sql文のWHERE辺り、$whereArray部分の作り方は視覚的に分かりやすい。あとから見返しやすいから、良い書き方ができたと思う。

多分今回、難しいことは別にやってないと思う。
その代わりに複雑というか、他方に絡む要素が多い。単純に膨大。記述ミスがあった場合のエラー修正がどこまで戻ればいいか、ひたすら辿っていかなきゃいけないことも珍しくなくて、だから詰まった時に焦りと相まって混乱したりして、そういうのがしんどかった。

やってることは同じ処理なのに違う場所でそれぞれforeach()とwhile()を使ってたりして、疲れが垣間見える。

お金払ってパッケージを活用するほうが賢いと思っちゃいますね。どうなんでしょうね。

コメント

タイトルとURLをコピーしました