d3.js advent calendar 21日目

d3 advent calendar 21日目
d3で適当なデータを作ったり、いろいろフォーマットする

d3の便利な関数の具体的な使い方

サンプルデータを生成する

サンプルデータって必要ですよね。そんな時の方法です

var size = 24 * 7 * 14, mean = 100, stdev = 20;
var norm = d3.random.normal(mean, stdev);
var data = d3.range(0,size).map(function(d, id){
  return {
    id: id,
    age: 0|Math.random() * 40 + 20,
    gender: Math.random() > 0.5 ? 'male' : 'female',
    score: norm()
  }
});

データをネスト化する

何度も繰り返しする作業をやってくれるメソッドで一番使うのがコレです

便利なのでperlに移植したほどです(もう少し拡充させる予定)

var data; // 前項参照
var nested = d3.nest().key(function(d){ return d.gender;})
  .key(function(d){ return 0|d.age / 10; }).entries(data);
// [{key: 'male', values: 
//   [{key: "20", values: [/ この条件に当てはまる要素 / ]},
//    {key: "30", values: [...]}
//   ]},
//  {key: 'female', values: [...]}]

データをネスト化する(map)

var nestedMap = d3.nest().key(function(d){ return d.gender;})
  .key(function(d){ return 0|d.age / 10; }).map(data);
// {"male": {
//   "20": [/ male かつ 20代 のデータ /],
//   "30": [...]
//   },
//  "female": { ...
// }}

nestの注意点

  1. keyが全部、stringになります。年月日とかをkeyでとった時にノリでDateとして扱おうとすると当然エラーになります
  2. entriesの時の子要素はchildrenじゃなくてvaluesです。d3.layout.hierarchy系のlayoutを使うときは、.children() APIでchildren属性を指定する必要があります。
  3. .map()は便利なようでイマイチ使い道がありません(d3では)

2データ間の間を埋める

「色のグラデーション欲しいわー」とか「この2点間を時系列合わせて埋めたいわー」って時に使うのが、.interpolate()です。これも実は.interpolateString(),.interpolateNumber(),.interpolateRgb(),.interpolateHsl(),.interpolateLab(),.interpolateHcl(),.interpolateArray(),.interpolateObject()とファミリーがあってたまにハマります。

.interpolate()

だいたいの場合は、「数値→色的な奴→文字列→配列→オブジェクト」の内部でよきにやってくれています

d3.interpolators = [
  d3_interpolateObject,
  function(a, b) { return Array.isArray(b) && d3_interpolateArray(a, b); },
  function(a, b) { return (typeof a === "string" || typeof b === "string") && d3_interpolateString(a + "", b + ""); },
  function(a, b) { return (typeof b === "string" ? d3_rgb_names.has(b) || /^(#|rgb(|hsl()/.test(b) : b instanceof d3_Color) && d3_interpolateRgb(a, b); },
  function(a, b) { return !isNaN(a = +a) && !isNaN(b = +b) && d3_interpolateNumber(a, b); }
];

はまるケース

1要素の配列でinterpolateArrayが適用されると信じるケース

var a = [0], b = [10];
var ip = d3.interpolate(a, b);
ip(0); // => 0 になる! [0]じゃない!

// 理由: 前項の条件 !isNaN(a = +a) // これが1要素配列だとtrueになるのでinterpolateNumberが適用される // 2要素だとfalseになる

適当に必要に応じてinterpolateArrayを明示的に使っていたんですがissueおくっておいた

時系列に合わせて埋める

var data = [
  {date: new Date(2013, 0, 1), value: 10}, // ここの間が欲しい!
  {date: new Date(2013, 0, 11), value: 100}
];
var interpolate = d3.interpolate(data[0], data[1]);
var interpolater = d3.time.days(data[0].date, data[1].date).map(function(d){
  return interpolate( (d - data[0].date) / 86400000 / d3.time.days(data[0].date, data[1].date).length) );
});
data.splice(0, 0, interpolater);

簡単ですね!(ェ

_人人人人人人人_
> うそです! <
 ̄Y^Y^Y^Y^Y^Y ̄

ハマりどころ

  1. interplateは内部でオブジェクトのリファレンスを使いまわします!issue:1030
  2. spliceは配列で置換要素を受け取れません!

回避策

// interpolate()の返り値はcloneして作り直す必要がある
// 簡単なのはコレですが、循環参照がはいっていたら…
var interpolater = d3.time.days(data[0].date, data[1].date).map(function(d){
  return JSON.parse(JSON.stringify(interpolate( (d - data[0].date) / 86400000 / d3.time.days(data[0].date, data[1].date).length) )) ;
});

// spliceの対応 interpolated.unshift(/ 置換開始 /0, / 置換要素数 / 0); data.splice.apply(data, interpolater);

簡単ですね!