チームの1年間のいいね数をD3.jsを使ってアニメーションで可視化する

![dynamicgraph.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272803/b0a2f483-8920-41f4-702e-89d4bc539ede.gif)


[Qiitaイイネやフォロワーを社内メンバーでウォッチして盛り上がる](https://qiita.com/accakr/items/85125db86061f1485df6)
弊社では昨年の7月からエンジニアメンバーで積極的にQiita投稿に取り組んで来ました。
**いいね数**を主なKPIとして、1記事/月を目標に投稿してきました。
取組が始まって1年が経つにあたり、これまでの**いいね数**の軌跡を<code>D3.js</code>を使ってアニメーションで可視化してみました。

こういう可視化の仕方も面白いですね。
記事がバズったりすると一気に順位が変動するのが分かって良いです。

# ソース
```{html:index.html}
<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8' />
    <title>Qiita Dynamic</title>
    <script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js" charset="utf-8"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/locale/ja.js" charset="utf-8"></script>
    <script src="./data2.js" charset="utf-8"></script>
    <script src="./userdata.js" charset="utf-8"></script>
    <style>
        body {
            font-family: "Arial", sans-serif;
        }

        .axis {
            font-size: 20px;
        }

        .label {
            font-size: 20px;
        }

        .chart-title {
            width: 840px;
            margin-left: 120px;
            text-align: center;
        }
    </style>

</head>

<body>

    <h2 class="chart-title">
        Qiitaいいね数
        <span class="date"></span>
    </h2>
    <div id="graphic"></div>

    <script>

        const time = 100;

        const margin = { top: 0, right: 40, bottom: 30, left: 120 };
        const width = 960 - margin.left - margin.right,
            height = 500 - margin.top - margin.bottom;

        const svg = d3.select("#graphic").append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .append("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

        let init = true;
        let prev;

        const yScale = d3.scaleBand()
            .rangeRound([height, 0], 0.1)
            .padding(0.4)
            ;

        function setData(obj, date) {

            d3.select('.date').text(date);

            return new Promise((resolve) => {

                const t = d3.transition().duration(time).on('end', resolve);

                const total = {};
                Object.keys(obj).forEach((key) => {
                    total[key] = (obj[key] || 0) + (prev ? prev[key] || 0 : 0);
                });

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

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

                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')
                    ;

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

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

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

                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();

                bars.enter()
                    .append("g")
                    .append("rect")
                    .attr("class", "bar")
                    .attr("x", 0)
                    .style('fill', function (d) {
                        return usercolor[d.name];
                    })
                    .merge(bars)
                    .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();

                labels.enter()
                    .append("g")
                    .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))); };
                    })
                    ;

                prev = total

            });

        }

        const start = moment('2018-07-01');
        const end = moment('2019-07-01');
        const sleep = msec => new Promise(resolve => setTimeout(resolve, msec));

        (async function () {

            let dt = start.clone();

            do {

                const d = dt.format('YYYY-MM-DD HH');

                if (data[d]) {
                    await setData({ ...userdata, ...data[d] }, dt.format('YYYY-MM-DD'));
                }

                dt.add(1, 'hour');

            } while (dt < end);

        })();

    </script>

</body>

</html>
```

```{javascript:data.js}
// YYYY-MM-DD HHのフォーマットでユーザーごとのいいね数のカウントを格納
var data = { "2019-01-01 01": { "xxx": 5, "yyy": 5 }, ... }
```

```{javascript:userdata.js}
var userids = [
    "xxx",
    "yyy",
    "zzz",
];

var userdata = {
    "xxx": 0,
    "yyy": 0,
    "zzz": 0
};

var usercolor = {
    "xxx": '#4e79a7',
    "yyy": '#e15759',
    "zzz": '#f28e2b',
}
```