メモ:D3.jsで折れ線グラフを描く

まだd3-annotationをうまく使えてないんですが、D3.jsについてのメモ。

このグラフです(繰り返しますが、d3-annotationがうまく使えてないのでアノテーションが黒くつぶれています…)。

f:id:yutannihilation:20170315232057p:plain:w450

具体的にはこのコード: https://bl.ocks.org/mbostock/3883245

var svg = d3.select("svg"),
    margin = { top: 20, right: 20, bottom: 30, left: 50 },
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom,
    g = svg.append("g").attr("transform", "translate(" + margin.left + ", " + margin.top + ")");

d3.select()は、指定したCSSセレクタで要素を選択するやつ。

ここで、transformの説明は以下。

translate(<x> [<y>])
この変換定義はx軸とy軸による移動を定義します。この変換定義はmatrix(1 0 0 1 x y)を実行した結果と同じです。もしy に何も提供されなければ0が提供されたものと想定します。

transform - SVG | MDN

次。

var parseTime = d3.timeParse("%d-%b-%y");
var timeFormat = d3.timeFormat("%d-%b-%y");

d3.timeParse()は、指定した文字列を時刻型に変換するパーサを返す関数。d3.timeFormat()はその逆に、時刻型を文字列にする。これはデータの前処理的な部分でグラフ自体には関係なくて、Rではlubridateパッケージとかがやるやつなのかな?

var x = d3.scaleTime()
    .rangeRound([0, width]);

var y = d3.scaleLinear()
    .rangeRound([height, 0]);

var line = d3.line()
    .x(d => x(d.date))
    .y(d => y(d.close));

d3.scaletime()とかd3.scaleLinear()。Rで言うと、ggplot2パッケージのscale_*_datetime()scale_*_continuous()に当たるもの?

ドキュメントのContinuous Scalesというところには、以下の例が載っている。domain()で指定しているのは、データの範囲。それに対してrange()で指定しているのは、グラフのX軸の範囲。つまり以下のx()[10, 130]の範囲を[0, 960]の範囲に線形変換?する関数になっている。

var x = d3.scaleLinear()
    .domain([10, 130])
    .range([0, 960]);

x(20); // 80
x(50); // 320

ちなみにinvert()で逆の変換ができるらしい。

x.invert(80); // 20

ちょっと戻って、上の

var x = d3.scaleTime()
    .rangeRound([0, width]);

ではrangeしか指定していない。domainはあとでデータがきてから指定している。

次。

d3.tsv(
    "data.tsv",
    d => {
        d.date = parseTime(d.date);
        d.close = +d.close;
        return d;
    },
    (error, data) => {
    ...
    }
)

d3.tsv()は、指定したURLからデータをfetchして変換していろいろ処理をする、という関数。2つめの引数(data.tsv)がデータのURL、2つめ(d => {})はデータを変換する関数(省略可)、3つめ((error, data))がデータを受け取っていろいろ処理する関数。

ここでさっきのparseTime()を使って文字列を時刻に変換している。

        d.close = +d.close;

の行はよくわからなかった…

        if (error) throw error;

        x.domain(d3.extent(data, d => d.date));
        y.domain(d3.extent(data, d => d.close));

ここで、さっきのx()y()domain()を指定している。domain()d3.extent()でデータの範囲を出してそれを指定している。Rで言うとこれはrange()だけど、plot()とかggplot2を使っている限りはこの値の範囲を自分で気にする必要はほとんどない。

        g.append("g")
            .attr("transform", "translate(0, " + height + ")")
            .call(d3.axisBottom(x))
            .select(".domain")
            .remove();

d3.axisBottom()は、scaleをどっち方向に向けるか、という関数のひとつで、

Constructs a new bottom-oriented axis generator for the given scale, with empty tick arguments, a tick size of 6 and padding of 3.

とのこと(あんまり理解してない)。

selection.call()は、selectした要素(ここではg)それぞれに対して指定した関数を実行する。

selection.remove()は、その要素を取り除くわけですが、ここにはいる理由がよくわからない…。

        g.append("g")
            .call(d3.axisLeft(y))
            .append("text")
            .attr("fill", "#000")
            .attr("transform", "rotate(-90)")
            .attr("y", 6)
            .attr("dy", "0.71em")
            .attr("text-anchor", "end")
            .text("Price ($)");

ここも上と同じなので省略。

        g.append("path")
            .datum(data)
            .attr("fill", "none")
            .attr("stroke", "steelblue")
            .attr("stroke-linejoin", "round")
            .attr("stroke-linecap", "round")
            .attr("stroke-width", 1.5)
            .attr("d", line);

selection.datum()は、指定したデータひとつを割り付ける。なので、上の例だと、dataひとつにつきpathひとつをつくり、d要素にline()を使って変換する。line()は、再掲するとこういうやつでした。

var line = d3.line()
    .x(d => x(d.date))
    .y(d => y(d.close));

このd => x(d.date)d => y(d.close)を使ってdataを変換してpathにする、という感じです(説明あってるか自信がない…)。

まとめ

正直こう書いていても3割くらいしか理解できません。可視化ってむずかしい…。ggplot2だと、

library(tidyverse)
d <- read_tsv("https://yutannihilation.github.io/sagittated-calamary/data.tsv")
d %>%
  mutate(date = lubridate::dmy(date)) %>%
  ggplot(aes(date, close)) +
  geom_line()

f:id:yutannihilation:20170315232143p:plain:w450

だけで終わりです。一瞬です。でも、こういうのに慣れきっている身としては、ゼロからこのグラフを描くとするとどうなるか、と考えることはとても勉強になりますし、ひいてはもっといい感じのグラフを描けるのでは?という気がしています。D3.js力をもっと高めたい。