【Js・vue.js】char.jsでのデータ加工のやり方

全般

記事作成から1年以上経過しています。
内容が古い可能性があります。


こんにちは!
個人的に開発しているvuejs × rails apiのアプリにてグラフを使ったデータ表示を実装した時に躓いたポイントがあったので記事にしたいと思います。

環境

  • rails 5.2.3
  • rails 5.2.3
  • vue.js 2.6.12

参考

躓いたポイント

chart.jsの使い方は参考記事がたくさんあったのですが、「じゃあ動的にデータ取得する時にどうやってグラフに渡すの?」ってなり、なかなか参考記事が見つからずに苦労したので…

完成

これがchart.jsを使って表示している棒グラフです。

やりたい事

  • 1週間の学習時間をDBから取得。
  • 1日に複数の学習時間を登録している場合は合計して1つの連想配列にまとめる。
  • 学習時間を登録していない日は学習時間を0とする。
  • 最終的に取得したデータをchart.jsに渡して棒グラフを表示する。

はじめにデータを取得

欲しいデータは以下のようなデータです

[
  {"id":9,"time":1.5,"user_id":1,"created_at":"2021-01-25T12:00:00.000+09:00","day_of_week":1},
  {"id":11,"time":2.0,"user_id":1,"created_at":"2021-01-27T09:39:30.000+09:00","day_of_week":3},
  {"id":14,"time":0.5,"user_id":1,"created_at":"2021-01-28T07:52:24.000+09:00","day_of_week":4}
]

railsのactiveRecordを使って取得します。
まず今週の日〜土で期間指定します。

# models/study.rb
def self.get_week_chart_data
@this_day = Time.now
@range = @this_day.all_week(:sunday)
self.where(created_at: @range)
end

これでcreated_atが今週2021/01/24~2021/01/30で期間が指定できます
次にフロントで使うデータを指定して取得します

def self.get_week_chart_data
@this_day = Time.now
@range = @this_day.all_week(:sunday)
self.where(created_at: @range)
.select(:id, "sum(time) as time", :user_id, :created_at, "dayofweek(created_at) - 1 as day_of_week")
end

不要なデータも取ってきてますが気にしない笑
ポイントは、

  • sum(time) as timeとして1日に複数回の学習時間(time)を登録している場合もあるので、その合計値を取得する。
  • dayofweek(created_at) -1 as day_of_weekを使って曜日(この場合は曜日の添字である0~6)を取得する。

mysqlだとdayofweekで取得できる添字が1~7になるので注意。
JsではgetDay()を使うと0~6が取得できる。
なんか揃ってないのが気になるので-1して揃えているだけ。

最後に日付ごとにまとめる。

def self.get_week_chart_data
@this_day = Time.now
@range = @this_day.all_week(:sunday)
self.where(created_at: @range)
.select(:id, "sum(time) as time", :user_id, :created_at, "dayofweek(created_at) - 1 as day_of_week")
.group("date(created_at)")
end

これで曜日ごとにグループ化できたので当初取得したかったデータが取得できる。
このモデルメソッドをコントローラに渡す. ついでにルーティングも設定。

# controllers/studies_controller.rb
def history
histories = current_user.studies.get_week_chart_data // current_userの説明ははしょります. すいません!
render json: histories
end

# config/routes.rb
namespace :api, {format: 'json'} do
namespace :v1 do
get 'histories', controller: :studies, action: :history // RESTfulじゃないのは許して
end
end

本題

これで下準備が完了. chart.jsにデータを渡す。

# components/Chart.vue
# これは公式の書き方のまま
<script>
import { Bar, mixins } from 'vue-chartjs';
const { reactiveProp } = mixins

export default {
  extends: Bar,
  mixins: [reactiveProp], // reactiveProp使わないとデータ更新できないので注意
  props: ['options'],
  mounted () {
    this.renderChart(this.chartData, this.options)
  }
}
</script>
#History.vue
<template>
<div>
<Chart class="chart" v-if="loaded" :chartData="chartData" :options="options"/>
</div>
</template>
<script>
import Chart from '../components/Chart';

export default {
components: {
Chart
},
data() {
histories: [],
loaded: false,
chartData: { labels: [], datasets: [] }, // これがchart.jsのデータの定義
options: {
// ここの書き方はググるとたくさん出てくるので省略
}
},
mounted () {
this.$http.secured.get('/api/v1/histories')
.then(response => {
this.histories = response.data
this.fillData()
// エラー処理は省略
},
methods: {
// ここでchartにデータを渡す処理書きます
fillData () {
this.loaded = false
var data = this.arrayData() // ここでデータを加工するメソッドを呼び出しています
this.chartData = {
labels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
datasets: [{ data: data }] // 配列の形でデータを渡す必要があります
}
this.loaded = true
}
}
}
</script>

これで先程apiで取得したデータをchartに渡せるようになります。
ただ、まだjsonデータを加工してないのでarrayData()メソッドを書いていきます。
datasets:[{ data: }]に渡す時は配列にしてデータを渡します。
なので、jsonでは連想配列を配列にまとめた形でデータを受け取っているのでmapを使って配列に加工します。

~省略~
methods: {
arrayData () {
var getData = this.histories;
const times = getData.map(item => item.time)
return times
}
~省略~
}

mapは連想配列のキーを指定する事でvalueだけ取り出してループ処理し配列を作ってくれます。

const times = getData.map(item => item.time)
console.log(times)
=> [1.5, 2.0, 0.5]

あれ?
本来のデータ通りだと

1/25(月) => 1.5
1/27(水)=> 2.0
1/28(木)=> 0.5

にならなければいけないのに、mapで作ったデータでは

1/24(日) => 1.5
1/25(月) => 2.0
1/26(火) => 0.5

になっちゃってます。
確かに配列データを作れたのでdatasets:[{ data: }]に渡せるデータにできましたが、1週間は7日なので、これだと日・月・火のデータになってしまっています…。

現状の状態をまとめると、
[0, 1.5, 0, 0.5, 0, 0, 0]という配列を作らなくてはいけないのに、[1.5, 2.0, 0.5]という配列ができている状態です。
mapで作った配列はindex[0]から値を埋めていくので、3つの連想配列から取り出したプロパティはindex[0]~[2]に詰めてセットされてしまっています。

この問題を解消する為にやりたい事の3つ目学習時間を登録していない日は学習時間を0とする処理をarrayData()メソッドに書きます。

考え方

  • (A) jsonデータのday_of_weekを使って比較用の配列を作成する。
  • (B) 1週間は7日なので比較対象の配列を用意する。
  • (A)と(B)を比較して(A)が持っていない数字(曜日)を求める。
  • 持っていない数字(曜日)番目に{ time: 0 }を突っ込んでやる(無理やり感…)

果たしてこれが良いやり方なのかは甚だ疑問だが、やってみよう!

データの成形

~省略~
methods: {
arrayData () {
var getData = this.histories;
// getDataの連想配列数が7つ未満の場合
if(getData.length < 7) {
// (A)比較用の配列を作成する
// 各連想配列created_atの曜日(添字)を抽出して配列を作成
const arrayDayOfWeek = getData.map(item => item.day_of_week)
=> [1, 3, 4]

// (B)比較対象の配列を用意する
// 1週間7日分の曜日(添字)の数値を格納した配列を作成
const arraySeven = [0, 1, 2, 3, 4, 5, 6]

// (A)と(B)を比較して差分を求める
// filterメソッドを使って、arrayDay0fWeekが持っていない値を抽出
var resultArray = arraySeven.filter(i => arrayDayOfWeek.indexOf(i) == -1)
=> [0, 2, 5, 6]がresultArrayに格納

// このケースだとresultArrayには4つデータが格納されているので4回ループされる
// spliceメソッドを使ってgetDataの配列に格納されている[0, 2, 5, 6]番目に{time: 0}を追加
for(var i = 0; i < resultArray.length; i++) {
getData.splice(resultArray[i], 0, {time: 0})
};
↓
結果
↓
// 配列index番号の0, 2, 5, 6番目に{time: 0}が追加された(プロパティの数が違うのは気にしない)
//
getData =
[ {time: 0}
{id: 9, time: 1.5, user_id: 1, created_at: "2021-01-25T12:00:00.000+09:00", day_of_week: 1}
{time: 0}
{id: 11, time: 2, user_id: 1, created_at: "2021-01-27T09:39:30.000+09:00", day_of_week: 3}
{id: 14, time: 0.5, user_id: 1, created_at: "2021-01-28T07:52:24.000+09:00", day_of_week: 4}
{time: 0}
{time: 0} ]
//
}

const times = getData.map(item => item.time)
return times
// chartに渡したい配列を作成できた
=> [0, 1.5, 0, 0.5, 0, 0, 0]
}
~省略~
}
for(var i = 0; i < resultArray.length; i++) {
getData.splice(resultArray[i], 0, {time: 0})
};

この処理では、

resultArray[0]
=> 配列0番目に格納されている値は[0]
getData0番目にspliceメソッドによって{time: 0}が追加
resultArray[1]
=> 配列1番目に格納されている値は[2]
getData2番目にspliceメソッドによって{time: 0}が追加

をループ処理でresultArray.length分、繰り返します。

これで目的のデータの成形が完了したのでchartに渡せます。

chart.jsに成形したデータを渡す

// 必要箇所だけ
<template>
<div>
<Chart class="chart" v-if="loaded" :chartData="chartData" :options="options"/>
=> chartDataにfillDataメソッドで生成されたデータが渡される
=> optionsにdata() {}内で定義したoptionsデータを渡せる(この記事では省略してます)
</div>
</template>
<script>
export default {
data() {
histories: [],
loaded: false,
chartData: { labels: [], datasets: [] },
},
mounted() {
this.$http.secured.get('/api/v1/histories')
.then(response => {
this.histories = response.data //jsonデータを受け取る
this.fillData() // fillDataメソッドを呼び出す
})
},
methods: {
arrayData () {
~省略~
return times // 返り値 => [0, 1.5, 0, 0.5, 0, 0, 0]
},
fillData () {
this.loaded = false
var data = this.arrayData() // [0, 1.5, 0, 0.5, 0, 0, 0]をdataに格納
this.chartData = {
labels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
datasets: [ { data: data } ]
} // data() {}内で定義したchartData { labels: [], datasets: [] }に生成したデータが格納され, templateタグ内の<Chart ~~/>へマウントされる
this.loaded = true
}
},
}
</script>

これで記事の最初に貼ったキャプチャの棒グラフが完成しました。

最後に

もっと効率的な書き方あれば教えて下さい!
ちなみに、dayofweek()はmysqlで用意されているメソッドなのでpostgresqlだとエラーします…。
herokuでデプロイしたらエラーで萎えました…。

おわり

コメント