スコット・マレイ
コード・アーティスト

Tutorials > D3 > スケール

スケール

最終更新日 2012年12月30日(原文)  2013年05月03日(翻訳 / h.sakai

「スケールとは入力ドメインを出力レンジにマップする関数のことである」

これは D3 の作者、マイク・ボストック氏による D3 のスケールの定義です。

(※訳注。ドメイン:領域、レンジ:範囲、マップする:変換する/対応付ける)

データを視覚化した場合、必ずしも元の値がうまくデータ表示領域に収まるとは限りません。その場合、元の値を適切な値にマップすることでスムーズな視覚化が可能になります。そのための簡単な手段を提供するのがスケール関数です。

D3 のスケールは、自分で定義して使う関数です。スケール関数を生成後、呼び出して値を渡すことにより、適切にスケール(縮尺)された値が戻ります。スケール関数は好きな数だけ定義し、使うことができます。 

スケールと聞くと、数値を示す目盛りのように、何か目に見えるものを想像しがちですがそうではありません!D3 における目盛りとは「」の構成要素のことです。スケールを視覚的に表現したものがなのです。スケールとは数学的な関係を示すものであって、それ自体は目に見えるものではありません。スケールと軸は、互いに関係はしているものの、異なった概念であることを理解しておきましょう。

スケールには指数、対数などいろいろな種類がありますが、この章で扱うのは普通の線形スケールです。線形と書くと難しそうですが、要は小学校で習ったグラフの X 軸、Y 軸のような、等間隔の縮尺のことです。線形スケールを理解しておけば、他のスケールも簡単に理解できます。

リンゴとピクセル

次のデータセットを、街道沿いの果物屋さんで売られているリンゴの月ごとの売り上げだと思ってください。

var dataset = [ 100, 200, 300, 400, 500 ];

なかなか立派な数字ですね。月を追うごとにリンゴの売り上げが百個ずつ伸びています。実に景気の良い話です。この繁盛ぶりを示すために、月々の売り上げの急激な伸びを棒グラフにするとします。データの数値を棒の高さで表しましょう。

これまでのグラフの解説では、元のデータをそのまま表示する値としてきました。単位の違いは無視してきたのです。同じ方法ですと今回のリンゴの売り上げの 500 個という数字も、そのまま 500 ピクセルの棒の高さになってしまいます。

一見これで問題は無さそうですが、もし来月の売り上げが 600 個になったらどうでしょう?そして一年後、1800 個のリンゴが売れたら?棒グラフを表示するために、一回り大きなディスプレイに買い替えるべきでしょうか?

ここでスケールの出番です。リンゴはピクセルではありません(もちろんオレンジでもありません)。リンゴからピクセルに転換するためにスケールが必要となるのです。

ドメインとレンジ

スケールの入力ドメインとは、入力データの値の取りうる範囲のことです。上記リンゴの売り上げデータについて言えば、100 から 500(すなわちデータセットの最小値と最大値)、あるいはもう少し広く取ればゼロから 500 が入力ドメインとなります。

一方スケールの出力レンジとは、出力値の取りうる範囲のことです。通常その値は、画面表示用にピクセル単位となります。出力レンジを決めるのはあなたです。情報のデザイナーとしてあなたがこれを決めるのです。もしリンゴ売上棒グラフの高さを、最小 10 ピクセル、最大 350 ピクセルとしたいのなら、10 から 350 が出力レンジとなります。

たとえば入力ドメインが 100,500、出力レンジが 10,350 のスケールを作ったとしましょう。そのスケールに 100 を与えると 10 が戻されます。500 を投げれば 350 が投げ返されます。300 を手渡せば 180 が銀のトレイに乗って出てきます(300 はドメインの中央値であり、180はレンジの中央値です)。

このドメインとレンジの関係を、上下二つの軸に並べて視覚化してみましょう。

入力ドメイン 100 300 500 10 180 350 出力レンジ

この入力ドメイン出力レンジの二つの用語はともかく混同しやすいので、ここでしっかり憶えておきましょう。「入力」ときたら「ドメイン」です。「出力」なら「レンジ」。繰り返してください。

憶えましたね?

正規化

もしすでに正規化(※)の概念をご存知の方は線形スケールも理解しやすいと思います。線形スケールとはすなわち正規化に他ならないからです。

(※ 訳注:ここでいう正規化とは数学の用語で、ベクトルにおける正規化のこと。線形空間のベクトルのノルムを 1 にするような演算を施すこと。規格化。)

正規化とは、元の数値を、その取りうる最小値と最大値に基づいて、0 から 1 の間の値に変換する処理のことです。たとえば一年 365 日の中の 310 日目は、一年間のうちのおよそ 0.85、あるいは 85% に変換されます。

線形スケールを適用するということは、実は D3 に数学の正規化処理を実行させているだけなのです。入力値はドメインに基づいていったん正規化され、次にその正規化された値が出力範囲にスケールされているのです。

スケールの生成

D3 のスケールは、d3.scale に目的のスケールのメソッド名を指定することで生成されます。今回は線形スケールですので linear() を指定します。

var scale = d3.scale.linear();

これだけです!この時点ですでに変数 scale には関数が代入されており、入力値を受け取ることができます。ここで var 宣言に惑わされないようにしましょう。JavaScript 関数も変数に代入できるのです。

scale(2.5);  // 2.5 が戻る

まだドメインもレンジの設定も行っていないので、この関数は入力を出力に 1:1 のスケールで変換します。すなわち、入力値をそのまま返すだけです。

スケールに入力ドメインを設定するため、domain() メソッドに100,500を配列として渡します。

scale.domain([100, 500]);

出力レンジの設定も、range() で同様に行います。

scale.range([10, 350]);

このように別々に分けて書いても良いのですが、チェインして一行にまとめることもできます。

var scale = d3.scale.linear()
                    .domain([100, 500])
                    .range([10, 350]);

これで scale の準備は整いました。

scale(100);  // 10 が戻る
scale(300);  // 180 が戻る
scale(500);  // 350 が戻る

通常、スケール関数を単独で使うことは無く、attr() 等のメソッド内から呼ばれます。さて、いよいよ前章で作成した散布図による視覚化に、動的スケールを適用して行きます。

散布図をスケールする

散布図のデータセットをもう一度見てみましょう。

var dataset = [
                [5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
                [410, 12], [475, 44], [25, 67], [85, 21], [220, 88]
              ];

この dataset が配列の配列だということを憶えてますか?各内部配列の最初の値を X 軸に、二番目の値を Y 軸にマップしたのでした。ではまず X 軸から始めましょう。

まず X の値を一つ一つチェックします。どうやら 5 から 480 の間に収まっているようですね。もう少し余裕を持たせて、入力ドメインを 0, 500 あたりにするのはどうでしょう。

え?何か変ですか?美しくない?もっとコードを柔軟に、スケーラブルに書くべきだろう?そうすれば将来データが変更になってもそのまま使える?・・・なるほど!

実はドメインに固定値を設定しなくても、min()max() のように、その場でデータ解析を行える便利な配列関数が D3 にはあります。たとえば次のコードは、各内部配列の X の値をループし、最も大きな値を返します。

d3.max(dataset, function(d) {    // 480 が戻る
    return d[0];  // 各内部配列の最初の値を参照
});

ここまで説明したことを使って X 軸のスケール関数を作ってみましょう。

var xScale = d3.scale.linear()
                     .domain([0, d3.max(dataset, function(d) { return d[0]; })])
                     .range([0, w]);

まず、このスケール関数を xScaleと名付けている点に注意してください。もちろんどんな名前をつけても構わないのですが、xScaleという名前にしておけば、これが何をする関数なのか一目瞭然です。

次に注意してほしいの入力ドメインの最小値です。ここではゼロにしていますが、最大値と同様、min()を用いて動的に計算することもできます。ドメインの最大値には dataset の最大値(すなわち 480 )がセットされます。

最後の行で、出力レンジを 0 から w の範囲にセットしています。後者は SVG の横幅でした。

Y 軸のスケール関数もほぼ同様に作成できます。

var yScale = d3.scale.linear()
                     .domain([0, d3.max(dataset, function(d) { return d[1]; })])
                     .range([0, h]);

X 軸のコードと異なっている箇所は、まず max() 関数の参照するのが d[1]、すなわち各内部配列の 2 番目の値であること、それと range() の最大値が w ではなく h であることです。

以上で二つのスケール関数が完成しました。あとはこれをコードに組み込むだけです。元のコードでデータの値ごとに circle を生成する部分を修正します。

.attr("cx", function(d) {
   return d[0];
})

円の X 座標、すなわち cx 属性に、元の値ではなくスケールした値を設定します。

.attr("cx", function(d) {
   return xScale(d[0]);
})

Y 軸についても同様に修正します。

.attr("cy", function(d) {
   return d[1];
})

やはり cy 属性をスケール関数で設定します。

.attr("cy", function(d) {
   return yScale(d[1]);
})

円の座標にあわせて、テキストラベルの座標も修正しなければなりません。次の行を…

.attr("x", function(d) {
   return d[0];
})
.attr("y", function(d) {
   return d[1];
})

同様にスケール関数で書き換えます。

.attr("x", function(d) {
   return xScale(d[0]);
})
.attr("y", function(d) {
   return yScale(d[1]);
})

こうなりました!

Scatterplot using x and y scales

サンプル画面はこちらです。前章の散布図に比べて見た目の変化はありませんが、がっかりする必要はありません。中身はうんと進歩しています。

ブラッシュアップ

お気づきのとおり、この散布図は Y の値が小さいデータほど上に、大きいデータほど下に表示しています。SVG の座標系では左上が原点だからです。これを直観に合うよう上下逆転させることも、スケールを使えばごく簡単にできます。単に yScale 関数の出力レンジを変更するだけです。

.range([0, h]);

.range([h, 0]);

と書き換えます。

Scatterplot with y scale inverted

サンプル画面です。これで yScale 関数の入力値が小さいほど大きな値を返すようになりました。つまり入力値が小さいほど circlelabel 要素は画像の下の方へ配置されるようになったのです。本当に簡単ですね!

しかし図を見ると表示領域からはみ出している要素があります。これを解決するために padding 変数を導入します。パディングとは、表示領域の内側余白のことです。

var padding = 20;

二つのスケール関数のレンジを設定するときにこの padding 分を計算に入れるのです。xScale 関数の元のレンジは range([0, w])でした。これを

.range([padding, w - padding]);

と変更します。

yScale 関数のレンジは range([h, 0]) でした。同様に

.range([h - padding, padding]);

と変更します。

この変更によって、SVG 領域の端から上下左右それぞれ 20 ピクセルずつのパディングが確保されました。

Scatterplot with padding

しかしこれでもまた一番右のテキストラベルが領域からはみ出しています。パディングのうち、xScale の右側部分だけ倍にしてみましょう。

.range([padding, w - padding * 2]);

Scatterplot with more padding

OK です!ここまでのサンプル画面はこちら

もう一工夫してみましょう。現在は circle の半径には対応する Y の値の平方根をセットしています。これはあまり上等なハックではありませんし、何の実用性もありません。半径の計算にもスケール関数を使ってみましょう!

var rScale = d3.scale.linear()
                     .domain([0, d3.max(dataset, function(d) { return d[1]; })])
                     .range([2, 5]);

このスケール関数を半径 r の属性の設定に使います。

.attr("r", function(d) {
   return rScale(d[1]);
});

これはなかなかちょっとした工夫です。なぜなら、これで円の半径が常に[2, 5]の範囲に収まることが保証されるからです(正確に書くと「常に」ではなく「ほとんどの場合」です-- 章末の clamp() の項参照)。データの値が最小値の 0の場合、得られる半径は 2(つまり直径 4 ピクセル)となり、最大値の場合、得られる半径は 5(つまり半径 10 ピクセル)となるのです。

Scatterplot with scaled radii

サンプル画面です。この例はスケール関数を、軸の値を求めるためにではなく、視覚的な効果のために使う最初の例でした。

最後に、これでもまだスケールの威力が実感できないという人のために、データセットにもう一つ配列 [600, 150]を追加してみましょう。

Scatterplot with big numbers added

どうでしょう!サンプル画面をご覧ください。ここで注目すべき点は、右上に新しい円が追加されたことで、元の円がすべて、互いの相対的位置関係を維持しつつも、距離を詰めながら左下に移動したことです。

さて、この章の締めくくりに、スケールが秘めている凄さをもう一つお伝えしたいと思います。実はこの段階で SVG のサイズを変更すると、すべての要素がそれにあわせてスケールするようになっているのです。SVG の h100 から 300 に変更してみます。他は一切変更しません。

Large, scaled scatterplot

再度、どうでしょう!更新したサンプル画面です。この凄さをうまくお伝えできていれば良いのですが!例えばクライアントから突然画像サイズを 600 から 800 ピクセルに変更したいと連絡があっても、もはや徹夜で修正作業に追われる必要はないのです。このチュートリアル(と D3 の素晴らしいビルトインメソッド)が、読者の貴重な睡眠時間の確保に役立つことを願ってやみません。

他のメソッド

d3.scale.linear()のメソッドの中から、特に便利なメソッドを簡単に紹介しておきます。

他のスケール

この章で解説した linear スケール以外にも D3 にはスケール用の組込メソッドがいくつかあります。

次章は

インタラクティブ・データ・ヴィジュアライゼーション このチュートリアルの書籍版の翻訳がオライリー・ジャパンより発売されました。タイトルは『インタラクティブ・データビジュアライゼーション ―D3.jsによるデータの可視化』です(画像をクリックするとアマゾンに飛びます)。

このチュートリアルを大幅に拡充し、3倍近い内容となっています。JavaScriptを中心に基礎編をさらに詳しく解説し(書籍版第3章)、応用編としてモーション、イベント、レイアウト、地図の作成法、データのエクスポート(PDFやSVG等)の章が追加されています(同9章~13章)。アマゾンのページで目次を見ることができます。

本チュートリアルがわかりにくいと感じられた方、あるいは本チュートリアルを終え、さらに応用力を身につけたいと思われた方のどちらにもお勧めの内容となっています。

翻訳はコンピュータ・プログラミング関連書籍を多数翻訳されている長尾高弘氏です。