1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-31

qiitaracingshort3.gif

はじめに

チームの1年間のいいね数をD3.jsを使ってアニメーションで可視化する
以前この記事でD3.jsを使った、いいねをアニメーションで可視化する手法を紹介しました。
今回はQiita APIを使って、LGTMを可視化するサービスをGithub Pagesを使って公開してみました。

サービス

Qiita Racer
※このヌルヌル動くグラフのことをRacing Bar Chartと呼ぶらしいので、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の合わせ技で動く棒グラフは作っています。

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というモジュールを使いました。

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

コマンドで下記を実行

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を使うとあっという間にアプリケーションがリリースできるので今後も使っていきたいです。
自分やチームの投稿を振り返る際にでも活用してもらえると嬉しいです!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?