みんなのLGTMをアニメーションで可視化するサービスをリリースするまで

![qiitaracingshort3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272803/732195b2-d666-0565-86d5-54cc0472e075.gif)


# はじめに
[チームの1年間のいいね数をD3.jsを使ってアニメーションで可視化する](https://qiita.com/tonio0720/items/2548d810e37c442aa540)
以前この記事でD3.jsを使った、いいねをアニメーションで可視化する手法を紹介しました。
今回はQiita APIを使って、LGTMを可視化するサービスをGithub Pagesを使って公開してみました。

# サービス
[Qiita Racer](https://tonio0720.github.io/QiitaRacing/index.html)
※このヌルヌル動くグラフのことを<b>Racing Bar Chart</b>と呼ぶらしいので、Qiita Racerと名付けてみました。

## ソース
こちらに公開しています。
https://github.com/tonio0720/QiitaRacing

## 使い方
URLにアクセスすると「アクセスの許可を求めています」と出るのでそのまま許可してください。
ユーザーID（最大8個）を入力し、データ取得を実行すると動きます。
※QiitaAPIの制限の都合により、総LGTM数が1000以上のユーザーは抽出できないように制御しています。

# リリースするまで
リリースに至るまでのプロセスを少し紹介します。
何かの参考にしてもらえればと思います。

## Reactで画面開発
https://qiita.com/tonio0720/items/0b9d670389286171af07
この記事で紹介した、Reactのテンプレートを使って開発しています。
今回はブラウザから直接Qiita APIを叩くのでサーバーは必要ありません。

ReactとD3.jsの合わせ技で動く棒グラフは作っています。

```jsx:RacingBarChart.js
import React from 'react';
import * as d3 from 'd3';

import styles from './index.module.less';

const time = 100;

export default class RacingBarChart extends React.Component {
    constructor(props) {
        super(props);
        this.chart = React.createRef();
        this.user2Color = {};
    }

    componentDidMount() {
        const margin = {
            top: 0,
            right: 40,
            bottom: 30,
            left: 120
        };

        const width = this.chart.current.parentNode.clientWidth - margin.left - margin.right;
        const height = this.chart.current.parentNode.clientHeight - margin.top - margin.bottom;
        this.width = width;
        this.height = height;

        this.svg = d3.select(this.chart.current)
            .attr('width', width + margin.left + margin.right)
            .attr('height', height + margin.top + margin.bottom)
            .append('g')
            .attr('transform', `translate(${margin.left},${margin.top})`);

        this.yScale = d3.scaleBand()
            .rangeRound([height, 0], 0.1)
            .padding(0.4);
        console.log(this);
    }

    moveToDate = (dateData) => {
        const {
            width,
            height,
            svg,
            yScale,
            user2Color
        } = this;

        return new Promise((resolve) => {
            const t = d3.transition().duration(time).on('end', resolve);

            const data = Object.keys(dateData).map((user) => {
                const value = dateData[user] || 0;
                return {
                    name: user,
                    value
                };
            }).sort((a, b) => d3.ascending(a.value, b.value));

            const max = d3.max(data, (d) => d.value);
            const xScale = d3.scaleLinear()
                .range([0, width])
                .domain([0, max < 10 ? 10 : max]);

            yScale.domain(data.map((d) => d.name));

            let xAxis = svg.select('.x.axis');
            if (xAxis.empty()) {
                xAxis = svg.append('g')
                    .attr('class', 'x axis')
                    .attr('transform', `translate(0,${height})`);
            }

            xAxis.transition(t)
                .call(d3.axisBottom(xScale))
                .selectAll('g');

            let axis = svg.select('.y.axis');
            if (axis.empty()) {
                axis = svg.append('g')
                    .attr('class', 'y axis');
            }

            let barsG = svg.select('.bars-g');
            if (barsG.empty()) {
                barsG = svg.append('g')
                    .attr('class', 'bars-g');
            }

            const bars = barsG
                .selectAll('.bar')
                .data(data, function (d) {
                    return d.name;
                });

            bars.exit().remove();

            const enterBars = bars.enter();

            enterBars
                .append('rect')
                .attr('class', 'bar')
                .attr('x', 0)
                .merge(bars)
                .style('fill', function (d) {
                    return user2Color[d.name] || '#333';
                })
                .transition(t)
                .attr('height', () => yScale.bandwidth())
                .attr('y', function (d) {
                    return yScale(d.name);
                })
                .attr('width', function (d) {
                    return xScale(d.value);
                });

            const labels = barsG
                .selectAll('.label')
                .data(data, function (d) {
                    return d.name;
                });

            labels.exit().remove();

            const enterLabels = labels.enter();

            enterLabels
                .append('text')
                .attr('class', 'label')
                .attr('x', function (d) {
                    return 0;
                })
                .merge(labels)
                .transition(t)
                .attr('y', function (d) {
                    return yScale(d.name) + yScale.bandwidth() / 2 + 4;
                })
                .attr('x', function (d) {
                    return xScale(d.value) + 3;
                })
                .tween('text', function (d) {
                    const selection = d3.select(this);
                    const start = d3.select(this).text();
                    const end = d.value;
                    const interpolator = d3.interpolateNumber(start, end);
                    return function (t) { selection.text(Math.round(interpolator(t))); };
                });

            axis.transition(t)
                .call(d3.axisLeft(yScale))
                .selectAll('g');
        });
    }

    render() {
        return (
            <div style={{ height: 400 }} className={styles.racingBarChart}>
                <svg ref={this.chart} />
            </div>
        );
    }
}

```

Sliderと再生ボタンを配置すると、動画っぽくなっていいです。
動かすとグラフも連動して動きます。

### 使用したフレームワーク/ライブラリ
* React
* D3.js
* Ant Design

## OAuthでQiita APIのトークンを発行
OAuthを使ってトークンの発行をしています。
こちらのサイトがとても参考になりました。
https://qiita.com/nutti/items/688de20382e60286d26d

## Github Pagesに公開
https://qiita.com/star__hoshi/items/490959aee12dbf528f7c
npmの`gh-pages`というモジュールを使いました。

```json:package.json
    "scripts": {
        ...
        "predeploy": "npm run build",
        "deploy": "gh-pages -d build"
    },
```

コマンドで下記を実行

```bash
npm run deploy
```

Github → Settings → GitHub PagesのSourceを「gh-pages Branch」に変更

## 所感
今回はツールを作って、Github Pagesに公開するということをやってみました。
Github Pagesは初めて使いましたが、とても便利ですね。

Qiita APIの1時間に1000リクエストの制約は少し不便ですね。
本ツールではユーザーに紐づくすべての記事を取得して、それぞれの記事のLGTM数を取得するということをやっているので、結構APIを消費します。
1ページにおける最大取得件数が100件というのもあり、あまりこういう使い方は向いていないのだなと感じました。
本ツールでは総LGTM数が1000を超えるユーザーは抽出できないように制御しています。

## 今後の展望について
Qiita APIだと何かと制約が多いので、日々データをDBに格納し、その結果を抽出/可視化ができるようなサービスを作りたいですね。
独自のQiitaダッシュボードとかができるとさらに面白みが増しますね。

# おわりに
という感じで色々やってみましたが、やっぱり楽しいですね。
Github PagesとReactを使うとあっという間にアプリケーションがリリースできるので今後も使っていきたいです。
自分やチームの投稿を振り返る際にでも活用してもらえると嬉しいです！