にじさんじ甲子園の育成パートを可視化したい
動機
過去の大会のさまざまなまとめを見ていて、選手の成長の過程がわかる情報を見つけられなかったので、自分で調べて可視化したくなりました。
方針
記録するデータの項目と記録するタイミングについて
経験値に影響しうるデータのみを記録することにしました。従って特殊能力の獲得状況は無視します。
サブポジの獲得も経験値が必要なのですが、スキーマ設計上面倒にしてしまったため、グラフの描画は見送っています。
タイミングについては、月初の練習指示画面の情報を記録していくことにしました。他のタイミングで選手データが画面に表示されていても無視することにしました。同じゲーム内日付で全選手を比較できるようにするためです。
また、試合の有無が経験値獲得量に大きく影響するため、試合データも記録することにしました。
方法
データのフォーマット
TOMLで記述することにしました。
異なる配列の要素を順番ごちゃ混ぜに書くことができる点がよかったです。
例えばこのようなJSONを最終的に欲しいとします(JSONC)。
{
"name": "名門高校",
"liveStreams": [
{
"videoId": "youtube-video-id-1",
"startDate": "2022-04-08",
"endDate": "2022-09-01",
},
{
"videoId": "youtube-video-id-2",
"startDate": "2022-09-01",
"endDate": "2022-12-31",
},
],
"games": [
{ "date": "2022-06-11", "type": "練習試合" },
{ "date": "2022-07-02", "type": "夏の都道府県大会" },
{ "date": "2022-07-06", "type": "夏の都道府県大会" },
{ "date": "2022-09-13", "type": "秋の都道府県大会" },
{ "date": "2022-10-11", "type": "練習試合" },
],
}
2回配信があって、1回目は「練習試合」1回と「夏の都道府県大会」2回戦い、2回目の配信では「秋の都道府県大会」と「練習試合」をそれぞれ1回ずつ戦ったことがわかります。
JSONやYAMLだと、 liveStreams
と games
は別々のところに分けて書かなければいけないため、gitの差分が2箇所にできてしまいます。データの数が増えるとエディタでの移動範囲が大きくなりファイル編集が大変です。
一方でTOMLの場合はこのように書けます。
name = '名門高校'
[[liveStreams]]
videoId = 'youtube-video-id-1'
startDate = '2022-04-08'
endDate = '2022-09-01'
[[games]]
date = 2022-06-11
type ='練習試合'
[[games]]
date = 2022-07-02
type ='夏の都道府県大会'
[[games]]
date = 2022-07-06
type ='夏の都道府県大会'
[[liveStreams]]
videoId = 'youtube-video-id-2'
startDate = 2022-09-01
endDate = 2022-12-31
[[games]]
date = 2022-09-13
type ='夏の都道府県大会'
[[games]]
date = 2022-10-11
type ='練習試合'
このように、時系列順(date
フィールド)に書くことができ、差分もファイル末尾の1箇所で済みます。また 日付用のフォーマットも規定されていて、引用符で囲う必要がなくありがたかったです。
TOMLのパーサーは @iarna/toml
を使いました。日付のみのフォーマットに対応していることが選択理由です。
ウェブサイト作成
Next.jsを使いました。
リクエストに応じて表示内容を変えるということがないので、getStaticPaths
と getStaticProps
を使って静的にページを生成しました。
データ取得
GraphQL.jsを使いました。
TOMLから変換したJavaScriptオブジェクトをリゾルバに渡し、データの加工を行いました。オーバーフェッチングを防いだり、ランタイムで型チェックしてくれるので便利です。
Webサーバーを立てるわけではなく、 graphql
関数を呼び出すだけです。
import { graphql } from 'graphql'
import { makeExectableSchema } from '@graphql-tools/schema'
import data from '~/data'
const typeDefs = [
`#graphql
type Query {
players(teamName: String): [Player!]!
}
type Player {
name: String!
teamName: String!
}
`
]
const resolvers = {
Query: {
players: (_query, { teamName }) => {
return data.players.filter(player => player.teamName === teamName)
}
}
}
const schema = makeExectableSchema({ typeDefs, resolvers })
const source = `#graphql
query getPlayersByTeamName($teamName: String!) {
players(teamName: $teamName) {
name
}
}
`
const { result } = await graphql({
schema,
source,
variableValues: { teamName: '最強高校' }
})
console.log(result.data.players)
それから GraphQL Code Generator を使ってコード生成をしました。
スキーマとクエリを元に型定義ファイルを作ってくれたり、クエリを呼び出すためのオブジェクトを作ってくれたりするので便利です。
静的サイトジェネレータ+GraphQLだと、Gatsby.jsも選択肢にあがります。少し触ってみたのですがGatsbyのプラグインを書くのが面倒だったのと、独自のリゾルバを書けなさそうだったのでやめました。
グラフの描画
D3.jsを使いました。ただしReactでページをつくることにしていたので、データの加工ののみD3.jsを使い、SVGの各要素の作成はReactでやりました。
import { line, scaleLinear, scaleTime } from 'd3'
const xScale = scaleTime()
.domain([new Date('2022-04-08', new Date('2024-08-31'])
.rangeRound([0, graphWidth])
.nice()
const yScale = scaleLinear()
.domain([0, 100])
.rangeRound([graphHeight, 0])
const lineGenerator = line().x(d => new Date(d.date)).y(d => d.stamina)
const d = lineGenerator([
{ date: '2022-04-08', stamina: 10 },
{ date: '2023-04-01', stamina: 40 },
{ date: '2024-04-01', stamina: 70 },
{ date: '2024-08-31;, stamina: 100 },
])
return <path d={d} fill="none" stroke="black" strokeWidth={2} />
D3.jsのファイルサイズが気になってクライアント用のバンドルファイルに入れたくなかったので、 getStaticProps
の中でグラフ描画用のデータを作り、NextPage
コンポーネントは描画用のデータのみ受け取ることにしました。
また、ここからは趣味の範囲になるんですが、D3.jsが作るデータの型をGraphQLのスキーマで定義し、GraphQLのリゾルバの中でD3.jsを呼び出すことにしました。
スタイリング
これまでTailwind CSS を使ったことがなかったので採用しました。
クラス名を付与するだけでスタイルが当たっていくのは面白いですね。
作ったグラフ
選手別経験値獲得量グラフ
横軸はゲーム内の日付で、縦軸は獲得した経験値の量です。項目別とその総和の両方を知りたいので積み上げ面グラフにしました。
経験値獲得量比較グラフ
あるゲーム内の日付においての、選手ごとに獲得した経験値の量を比較するグラフです。
おわりに
肝心のデータのほうを取りきれなかったので、途中で断念してしまいました(この記事自体、ドラフトのまま1年間放置してたのを見つけて投稿したものなので……)。しかしながらいろいろと技術を試せて良かったです。
Discussion