Open10

d3.js sampleの解析

Akira OtakaAkira Otaka

いきなりコード書き換えが必要だった。
元コードは下記。

// Create the horizontal time scale.
const x = d3.scaleUtc()
  .domain(d3.extent(stocks, d => d.Date))
  .range([marginLeft, width - marginRight])
  .clamp(true)

Dateが正しく認識されず(?)、d3.isoParseを使用している

// Create the horizontal time scale.
const x = d3
  .scaleUtc()
  .domain(d3.extent(stocks, (d) => d3.isoParse(d.Date)))
  .range([marginLeft, width - marginRight])
  .clamp(true);

scaleUtc()は次のように定義されています。

scaleUtc(domain, range)
Examples · Source · Equivalent to scaleTime, but the returned time scale operates in Coordinated Universal Time rather than local time. For example, to create a position encoding:

const x = d3.scaleUtc([new Date("2000-01-01"), new Date("2000-01-02")], [0, 960]);
x(new Date("2000-01-01T05:00Z")); // 200
x(new Date("2000-01-01T16:00Z")); // 640
x.invert(200); // 2000-01-01T05:00Z
x.invert(640); // 2000-01-01T16:00Z

If domain is not specified, it defaults to [2000-01-01, 2000-01-02] in UTC time. If range is not specified, it defaults to [0, 1].

TIP
A UTC scale should be preferred when possible as it behaves more predictably: days are always twenty-four hours and the scale does not depend on the browser’s time zone.

refs: scaleUtc

ドメイン(データ)と範囲(座標)を引数にとり、それらをマッピングしてくれる。
ドメインが日時であることがポイントなのかな。
引数に指定しない場合はデフォルト値が与えられるが、メソッドチェインでdomainrangeを設定することで同じことができるみたい。

scaleUtcで設定するよりscaleUtc+domain+rangeの方がコードがわかりやすいから、必須の記載方法になりそう。

clamp() : デフォルトでは線形スケールは設定した出力レンジ外の数値を返すこともあります。たとえば設定した入力ドメイン外の数値が与えられた時、スケールは出力レンジ外の対応する値を返します。しかし、スケールに .clamp(true) メソッドを適用すると、すべての出力値は設定した出力レンジに収まるようになります。この場合、レンジ外の出力値は、レンジの最小値か最大値のいずれか近い方の値に丸められます。

D3 チュートリアル: 15 スケール

domain+rangeでは入力値を出力値に合わせる努力をしてくれるが、絶対ではないらしい。clamp(true)を指定することでそれが絶対になるらしい。これも必須かな?

Akira OtakaAkira Otaka
const series = d3
  .groups(stocks, (d) => d.Symbol)
  .map(([key, values]) => {
    const v = values[0].Close;
    return {
      key,
      values: values.map(({ Date, Close }) => ({
        Date: d3.isoParse(Date),
        value: Close / v,
      })),
    };
  });

group(iterable, ...keys)
Examples · Source · Groups the specified iterable of values into an InternMap from key to array of value. For example, to group the penguins sample dataset by species field:

const species = d3.group(penguins, (d) => d.species);

To get the elements whose species field is Adelie:

species.get("Adelie") // Array(152)

If more than one key is specified, a nested InternMap is returned. For example:

const speciesSex = d3.group(penguins, (d) => d.species, (d) => d.sex)

To get the penguins whose species is Adelie and whose sex is FEMALE:

speciesSex.get("Adelie").get("FEMALE") // Array(73)

Elements are returned in the order of the first instance of each key.

groups(iterable, ...keys)

const species = d3.groups(penguins, (d) => d.species); // [["Adelie", Array(152)], …]

Equivalent to group, but returns an array of [key, value] entries instead of a map. If more than one key is specified, each value will be a nested array of [key, value] entries. Elements are returned in the order of the first instance of each key.
Grouping data

groups()group()の拡張。
まず、group()では(同一構造のオブジェクトの)配列を受け取り、その中の一つのプロパティをkey、それ以外のプロパティをvalueとする連想配列(InterMap)を構築する。なお、複合キーを指定することはできないが、keyを複数していした場合は、valueの中でさらに連想配列が作成されることになる。

また、連想配列で構築されているので、get()を使用し、引数にkeyを指定することでvalueを取り出すこともできる。複数のkeyを指定している場合はget()をチェインすることで深い階層のデータを取り出すこともできる。

groups()は[key, value]の配列(配列の配列)になる。連想配列ではないのでkeyで取り出すことはできない。

map(iterable, mapper)
Source · Returns a new array containing the mapped values from iterable, in order, as defined by given mapper function.

d3.map(new Set([0, 2, 3, 4]), (d) => d & 1) // [0, 0, 1, 0]

Like array.map, but works with any iterable.

map(iterable, mapper)
map()は既定のarray.mapと似ている。データを加工して配列を作成しなおしている。

Akira OtakaAkira Otaka
const k = d3.max(
  series,
  ({ values }) =>
    d3.max(values, (d) => d.value) / d3.min(values, (d) => d.value)
);

株価データなので、最大÷最小で、何倍になっているかを求め、それがすべての配列内で最大のものを探している。

Akira OtakaAkira Otaka
const y = d3
  .scaleLog()
  .domain([1 / k, k])
  .rangeRound([height - marginBottom, marginTop]);

scaleLog(domain, range)
Examples · Source · Constructs a new log scale with the specified domain and range, the base 10, the default interpolator and clamping disabled.

const x = d3.scaleLog([1, 10], [0, 960]);

If domain is not specified, it defaults to [1, 10]. If range is not specified, it defaults to [0, 1].

scaleLog(domain, range)

scaleLog()は底を10とした時のlogを求める。

rangeRound()は範囲内でデータを補完してくれるらしい。ちょっとよくわからなかった。

Akira OtakaAkira Otaka
const z = d3
  .scaleOrdinal(d3.schemeCategory10)
  .domain(series.map((d) => d.Symbol));

scaleOrdinal(domain, range)
Examples · Source · Constructs a new ordinal scale with the specified domain and range.

const color = d3.scaleOrdinal(["a", "b", "c"], ["red", "green", "blue"]);

If domain is not specified, it defaults to the empty array. If range is not specified, it defaults to the empty array; an ordinal scale always returns undefined until a non-empty range is defined.

scaleOrdinal(domain, range)

scaleOrdinal()はドメインと範囲を新しく紐づける。
d3.schemeCategory10は色の定義。それをdomain()で指定されたデータ(連想配列のキー)と紐づけている。

schemeCategory10

Akira OtakaAkira Otaka
const bisect = d3.bisector((d) => d.Date).left;

bisector(accessor)
Examples · Source · Returns a new bisector using the specified accessor function.

const bisector = d3.bisector((d) => d.Date);

If the given accessor takes two arguments, it is interpreted as a comparator function for comparing an element d in the data with a search value x. Use a comparator rather than an accessor if you want values to be sorted in an order different than natural order, such as in descending rather than ascending order. The above is equivalent to:

const bisector = d3.bisector((d, x) => d.Date - x);

The bisector can be used to bisect sorted arrays of objects (in contrast to bisect, which is for bisecting primitives).

bisector(accessor)

bisector()は二分探索を行うためのソートを行う。引数でしていしたアクセサ、または、比較演算子を用いてソートの方法を決定する。(と思う)

なぜsortとかじゃないんだろう?

bisector.left(array, x, lo, hi)

d3.bisector((d) => d.Date).left(aapl, new Date("2014-01-02")) // 162

Like bisectLeft, but using this bisector’s accessor. The code above finds the index of the row for Jan. 2, 2014 in the aapl sample dataset.

bisector.left(array, x, lo, hi)

left()はlo~hiの範囲でarrayからxを探す。
ソート済みのデータに対して二分探索を実施する。

Akira OtakaAkira Otaka
const svg1 = d3
  //.create("svg")
  .selectAll("#svgArea1,#svgArea2")
  .attr("width", width)
  .attr("height", height)
  .attr("viewBox", [0, 0, width, height])
  .attr(
    "style",
    "max-width: 100%; height: auto; -webkit-tap-highlight-color: transparent;"
  );

元々のコードだとcreate()でsvgタグを追加していたのだけど、訳あって2つのグラフを作りたかったので、HTML内にsvgタグを2つ作って選択した。

selectAll()を使い、,(カンマ)で区切るのがポイント。

Akira OtakaAkira Otaka
svg1
  .append("g")
  .attr("transform", `translate(0,${height - marginBottom})`)
  .call(
    d3
      .axisBottom(x)
      .ticks(width / 80)
      .tickSizeOuter(0)
  )
  .call((g) => g.select(".domain").remove());

Y軸方向は正負両方向に線が伸びるので、中央線を決める。

selection.call(function, ...arguments)
Source · Invokes the specified function exactly once, passing in this selection along with any optional arguments. Returns this selection. This is equivalent to invoking the function by hand but facilitates method chaining. For example, to set several styles in a reusable function:

function name(selection, first, last) {
  selection
      .attr("first-name", first)
      .attr("last-name", last);
}

Now say:

d3.selectAll("div").call(name, "John", "Snow");
This is roughly equivalent to:
name(d3.selectAll("div"), "John", "Snow");

The only difference is that selection.call always returns the selection and not the return value of the called function, name.

call()は選択状態の(selectなどの)要素を引数として渡し、操作(子要素を作成するなど)を行う場合に使用する。

  .call(
    d3
      .axisBottom(x)
      .ticks(width / 80)
      .tickSizeOuter(0)
  )

このコードではX軸を作成し、追加している。たぶん、この追加するというのが大事で、attr()style()data()は属性自体を変更するが、call()<g>要素自身には影響を与えない(と思う)。

Akira OtakaAkira Otaka
const serie = svg1
  .append("g")
  .style("font", "bold 10px sans-serif")
  .selectAll("g")
  .data(series)
  .join("g");

join()だが、選択範囲とデータを結びつけ、データに対する要素が足りない場合は新たに要素を作成する。意外と奥が深いようで、selection.join notebookまで用意されている。