Amami’s competitive diary

Vue.jsでAtCoderのレートのグラフを取得して表示したい

経緯

最近少しフロントエンドを触り始めて、Vue.jsを使ってポートフォリオを作ろうとしていました。
そこで私はAtCoderをやっていたので、せっかくだからレートのグラフを表示してみたいなと思いました。

Vue.js

なぜVue.jsを選んだのかというとVue, React, Angularを触ってみた感じ、個人的に分かりやすかったのがVueだったからで、特に深い理由はありません。
Vue.js自体の使い方については公式サイトや他の方の記事などがたくさんあるのでここでは特に触れないです。

以下公式ページから

Vue (発音は / v j u ː / 、 view と同様)はユーザーインターフェイスを構築するためのプログレッシブフレームワークです。他の一枚板(モノリシック: monolithic)なフレームワークとは異なり、Vue は少しずつ適用していけるように設計されています。中核となるライブラリは view 層だけに焦点を当てています。そのため、使い始めるのも、他のライブラリや既存のプロジェクトに統合するのも、とても簡単です。また、モダンなツールやサポートライブラリと併用することで、洗練されたシングルページアプリケーションの開発も可能です。 (https://jp.vuejs.org/v2/guide/)

AtCoderAPI

AtCoderには基本的にはAPIは提供されていませんが、1つだけ、個人のコンテストの成績表のAPIが提供されています。
ここにアクセスすると指定したユーザー名のコンテスト成績のデータがjson形式で帰ってきます。

https://atcoder.jp/users/tourist/history/json

touristのところを書き換えると任意のユーザー名のデータを取得できます。

f:id:amami0522:20210219165921p:plain

全てのデータが一気に返ってくるので少し分かりづらいですが、要素ごとに分解するとこんな感じになっています。

{
        "IsRated": true,
        "Place": 2,
        "OldRating": 0,
        "NewRating": 2720,
        "Performance": 3920,
        "InnerPerformance": 3920,
        "ContestScreenName": "agc004.contest.atcoder.jp",
        "ContestName": "AtCoder Grand Contest 004",
        "ContestNameEn": "",
        "EndTime": "2016-09-04T22:50:00+09:00"
},

なんかたくさんの情報がありますが今回必要なのはNewRatingですね。

とりあえずjsonを取得するコードを書いてみましょう。
データの数が多いので最初の1つだけ表示してみます。

const fetch = require('node-fetch');

fetch('https://atcoder.jp/users/tourist/history/json')
.then(response => {
    return response.json();
})
.then(json => {
    console.log(json[0]);
});

これを実行してみると。

f:id:amami0522:20210219171311p:plain

無事にコンテストの結果が取得できました。
ここでNewRatingの部分をを表示するには、

console.log(json[0]);

となっているところを、

console.log(json[0].NewRating);

または

console.log(json[0]['NewRating']);

に変えればいいですね。

f:id:amami0522:20210219171215p:plain

無事に表示できました。

Chart.js

Chart.jsはグラフの描画を簡単に行えるJavascriptのライブラリです。

vue-chart.js

vue-chart.jsについては以下の公式サイトより

vue-chartjs は Chart.js をvueで使用するためのラッパーです。 再利用可能なチャートコンポーネントを簡単に作成できます。

Vue.jsでChart.jsを使うために必要なものって認識です。

Vue.jsでAtCoderAPIを取得する

先程と同様にAtCoderAPIを取得します。
とりあえずAPiを取得してコンソールに表示するようにしてみます。

まずはAPIを取得するファイルをsample.vueとして書きます。

<template>
    <div>
    </div>
</template>

<script>
export default {
    name: "sample",
    mounted() {
        this.getData();
    },
    methods: {
        getData: function () {
            fetch('https://atcoder.jp/users/tourist/history/json')
            .then(response => {
                return response.json();
            })
            .then(json => {
                console.log(json);
            })
        }
    },
}
</script>

<style scoped>
</style>

次にこのモジュールを使用してsample.vueを表示するapp.vue

<template>
    <div id="app">
        <sample/>
    </div>
</template>

<script>
import sample from "@/components/sample";

export default {
    name: 'App',
    components: {
        sample
    }
}
</script>

<style>
#app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
}
</style>

これでlocalhost:8080にアクセスするととりあえずコンソールに取得したjsonが取得されるはず。

しかしここで問題が

f:id:amami0522:20210219174606p:plain

先程は上手くいったフェッチが失敗したとのエラーが。

エラーを読んでみるとCORS policyとやらによってlocalhost:8080からのリクエストはブロックされたとのこと。

色々調べてリクエストヘッダーの情報を変更してみたりと色々試しましたが出来なかったのでとりあえず別にAPIサーバを立てることに。

一時的にhttp://localhost:3000/api/result/にアクセスしたらAPIを取得してそれをjsonで返すAPiサーバを立てることに。(ややこしい)

APIサーバはJavascriptを使ってこんな感じに。

const express = require('express');
const app = express();

const fetch = require('node-fetch');
const cors = require('cors');

const corsOption = {
    origin: 'http://localhost:8080'
}

app.use(cors(corsOption));

app.set('port', (process.env.PORT || 3000));

app.get('/api/result/', function(request, response) {
    fetch("https://atcoder.jp/users/tourist/history/json")
    .then(res => {
        return res.json();
    })
    .then(json => {
        response.send(json);
    })
});

app.listen(app.get('port'), function() {
    console.log('Listening on ' + app.get('port'));
});

corsOptionでVue開発用のhttp://localhost:8080からのリクエストを許可するように設定しました。
そして、先程のsample.vueのfetchするURLを以下のように書き換えます。

fetch('http://localhost:3000/api/result/')

すると、以下のように取得できました。

f:id:amami0522:20210219183101p:plain

あとは、Chart.jsにデータとしてこれを送るだけです。

実際にはChart.jsを利用するJavascriptファイルを用意して、それを読み込む側からデータを送るという感じでした。

これに関しては公式ドキュメントをそのまま写します。 最終的にはこんな感じになりました。

Chart.jsを利用するlineChart.js

import { Line, mixins } from 'vue-chartjs';
const { reactiveProp } = mixins;

export default {
    mixins: [Line, reactiveProp],
    props: ['options'],
    mounted() {
        this.renderChart(this.data, this.options);
    }

}

APIを取得してそれをlineChart.jsに送るdraw.js

<template>
    <div>
        <h2>AtCoder</h2>
        <div class="graphArea">
            <lineChart :chart-data="datacollection" :option="options"/>
        </div>
    </div>
</template>

<script>
import lineChart from "@/components/lineChart";

export default {
    name: "draw",
    components: {
        lineChart
    },
    data: function() {
        return {
            datacollection: null,
            options: null,
        }
    },
    mounted() {
        this.fillData();
    },
    methods: {
        // 取得したAtCoderのレートをlineChart.jsに送るようにデータを編集
        fillData: function() {
            fetch("http://localhost:3000/api/result")
            .then(res => {
                return res.json();
            })
            .then(json => {
                let year = "0";
                let graphLabels = [];  // グラフに表示されるラベル
                let graphRating = [];  // グラフに描画するレートのデータ
                for(let i = 0; i < json.length; i++) {
                    if(json[i]['IsRated']) {
                        // コンテスト終了の年を取得
                        const nowYear = json[i]['EndTime'].substr(0, 4);

                        // 年が更新されたらラベルに表示
                        if(nowYear !== year) { year = nowYear; graphLabels.push(year); }
                        else { graphLabels.push(""); }

                        // レートをグラフのデータに追加
                        graphRating.push(json[i]['NewRating']);
                    }
                }

                // グラフのオプションの設定
                this.options = {
                    responsive: true,
                    maintainAspectRatio: false,
                };
                // グラフのデータの設定
                this.datacollection = {
                    labels: graphLabels,
                    datasets: [
                        {
                            label: 'レート',
                            data: graphRating,
                            fill: false,
                            lineTension: 0
                        }
                    ]
                };
            })
        }
    },
}
</script>

<style scoped>
.graphArea {
    margin: 50px auto;
    width: 500px;
}
</style>

ということでlocalhost:8080にアクセスすると

f:id:amami0522:20210219184101p:plain

いい感じにグラフを描画することが出来ました。

終わりに

こんな長いだらだらとした文章に付き合っていただいてありがとうございました。
CORSというものを始めて聞いたりとまだまだWebアプリケーションの難しさを実感しました。
まあ個人的な妥協点として解決出来たので今のところはこれで終わっておきます。
もっといい方法があれば教えて欲しいです。
ありがとうございました。