本記事はMichaela Lehr、Tim Severienが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
D3.jsはデータに基づいてドキュメントを操作するJavaScriptライブラリです。D3.jsはHTML、SVG、CSSを使ってデータを活用します。
私がおすすめする、Web制作者が学ぶべきJavaScriptライブラリーは、jQuery、Underscore.js、そしてD3.jsの3つだけです。この3つのライブラリーは、新しい方法でコードを考えることができます。jQueryを使うと少ないコードでDOMを簡単に操作でき、便利な関数が用意されているUnderscore.js(またはlodash)なら簡潔にプログラムを書けます。そしてD3.jsはデータ操作やグラフィックのプログラミングに使用できる豊富な機能を提供しています。D3.jsになじみが薄い人は、ぜひ時間をとって、D3.jsで実現できるすばらしい実例集を見てください。
D3.jsは昔からあるただのグラフ作成ライブラリーではありません。
William Playfairは棒グラフ、線グラフ、そして面グラフを1786年に、円グラフを1801年に発明しました。これらのグラフは現在でもデータを表すのに主要な方法として使われています。もちろんこれらのグラフはすばらしいのですが、D3.jsが提供するツールを使うと、Web上で柔軟かつユニークなデータビジュアライゼーションができます。あなたの独創力だけが唯一の制限要因といってもいいかもしれません。
D3.jsは、jQueryのようなAPIでHTML、SVGドキュメントにデータをマッピングできる、非常に柔軟性の高い、低水準のビジュアライゼーション用ライブラリーです。D3.jsはデータ変換や物理計算に役立つ数学関数を多く含み、SVGで図形の形状や線を操作できる強みを持ちます。
本記事では、上級者向けにD3.jsの特性の概要を説明します。実例では入力データ、変換、出力ドキュメントを交えて説明します。各関数の動作を説明するのではなく、コードを提示し、どのように動作するのか、大まかに把握することを目的としています。もっとも重要なコンセプトであるスケールとセレクションだけを詳しく説明していきましょう。
棒グラフ
D3.jsで作成する図表は、William Playfairの昔からあるただの図表よりも優れていると述べましたが、D3.jsがどのようにデータを変換するのか分かりやすく説明するために、HTMLを使ったシンプルな棒グラフを使います。コードは次のとおりです。
d3.select('#chart')
.selectAll("div")
.data([4, 8, 15, 16, 23, 42])
.enter()
.append("div")
.style("height", (d)=> d + "px")
selectAll関数は、D3.jsの「セレクション」を返します。セレクションは、各データポイントごとにデータを保存(enter)し、div要素を追加(append)して作成される要素の配列です。
前述のコードは入力データ[4, 8, 15, 16, 23, 42]を次のHTMLのとおりに配置して出力します。
<div id="chart">
<div style="height: 4px;"></div>
<div style="height: 8px;"></div>
<div style="height: 15px;"></div>
<div style="height: 16px;"></div>
<div style="height: 23px;"></div>
<div style="height: 42px;"></div>
</div>
CSSにはすべてに共通するスタイル属性を記述します。
#chart div {
display: inline-block;
background: #4285F4;
width: 20px;
margin-right: 3px;
}
GitHubの貢献度チャート
前述の棒グラフに少しコードを追記するだけで、Githubの貢献度チャートのように変えられます。
棒グラフのように、データ値をもとに高さを設定するのではなく、かわりにbackground-colorを設定します。
const colorMap = d3.interpolateRgb(
d3.rgb('#d6e685'),
d3.rgb('#1e6823')
)
d3.select('#chart')
.selectAll("div")
.data([.2, .4, 0, 0, .13, .92])
.enter()
.append("div")
.style("background-color", (d)=> {
return d == 0 ? '#eee' : colorMap(d)
})
colorMap関数は0と1の間の入力値をとり、指定した2色間のグラデーションを返します。グラフィックやアニメーションのプログラミングではinterpolate関数が重要な手法となります。後ほどほかの例で説明します。
SVG入門
D3.jsの大きな強みとして、円、多角形、線、テキストのような、2Dグラフィックを描画するタグを含んだSVGと一緒に動作する点が挙げられます。
<svg width="200" height="200">
<circle fill="#3E5693" cx="50" cy="120" r="20" />
<text x="100" y="100">Hello SVG!</text>
<path d="M100,10L150,70L50,70Z" fill="#BEDBC3" stroke="#539E91" stroke-width="3">
</svg>
上のコードは次の図形、文字列を描画します。
- x=50、y=120の位置に半径20の円
- x=100、y=100の位置に「Hello SVG!」の文字列
- 線の太さが3pxの三角形で、d属性の意味は次の通り
- x=100、y=10の位置へ移動
- x=150、y=70の位置に向かって線を描画
- x=50、y=70の位置に向かって線を描画
- 現在地点から始点まで直線を描画
SVGでは<path>要素がもっとも強力です。
円
前の例で使用したデータはシンプルな数字の配列でしたが、D3.jsではより複雑なデータ種別を使用できます。
const data = [{
label: "7am",
sales: 20
},{
label: "8am",
sales: 12
}, {
label: "9am",
sales: 8
}, {
label: "10am",
sales: 27
}]
各データポイントへ、<g>(グループ)属性を#chartに追加し、前述したデータの属性を使用して<circle>、<text>属性をグループの下に追加します。
const g = d3.select('#chart')
.selectAll("g")
.data(data)
.enter()
.append('g')
g.append("circle")
.attr('cy', 40)
.attr('cx', (d, i)=> (i+1) * 50)
.attr('r', (d)=> d.sales)
g.append("text")
.attr('y', 90)
.attr('x', (d, i)=> (i+1) * 50)
.text((d)=> d.label)
変数gは<g>ノードの配列を含むD3.jsの「セレクション」を保持し、append()を実行することで新しい要素をセレクションの各アイテムへ追加します。
上のコードは入力データを次のSVGドキュメントのとおりマッピングします。仕組みが分かりますか?
<svg height="100" width="250" id="chart">
<g>
<circle cy="40" cx="50" r="20"/>
<text y="90" x="50">7am</text>
</g>
<g>
<circle cy="40" cx="100" r="12"/>
<text y="90" x="100">8am</text>
</g>
<g>
<circle cy="40" cx="150" r="8"/>
<text y="90" x="150">9am</text>
</g>
<g>
<circle cy="40" cx="200" r="27"/>
<text y="90" x="200">10am</text>
</g>
</svg>
ラインチャート
SVGでラインチャートを描画するのはとてもシンプルです。次のようなデータをもとにチャートを描画してみます。
const data = [
{ x: 0, y: 30 },
{ x: 50, y: 20 },
{ x: 100, y: 40 },
{ x: 150, y: 80 },
{ x: 200, y: 95 }
]
データを次のようにコード化します。
<svg id="chart" height="100" width="200">
<path stroke-width="2" d="M0,70L50,80L100,60L150,20L200,5">
</svg>
注釈:y=100をSVGの上部とするため(0が上部)、yの値はグラフの高さ(100)から減算されています。
シングルパス要素なので、次のようなコードで書けます。
const path = "M" + data.map((d)=> {
return d.x + ',' + (100 - d.y);
}).join('L');
const line = `<path stroke-width="2" d="${ path }"/>`;
document.querySelector('#chart').innerHTML = line;
D3.jsには前のコードをさらにシンプルにできる線描画用の関数があります。コードは次のようになります。
const line = d3.svg.line()
.x((d)=> d.x)
.y((d)=> 100 - d.y)
.interpolate("linear")
d3.select('#chart')
.append("path")
.attr('stroke-width', 2)
.attr('d', line(data))
前よりもずっといいですね! interpolate関数を使うとx、y座標をつなぐ線の形状を変更できます。「linear」、「step-before」、「basis」の順に見ていきます。
スケール
スケール関数は入力範囲の値を出力範囲内に拡大、または縮小してマッピングします。
-CodePenを参照
これまで見てきた例では、「マジックナンバー」を使用して、グラフの範囲内に適切に配置できる座標を使用してきましたが、データが動的である場合、拡大または縮小の計算をしてデータを適切なスケールする必要があります。
幅500px、高さ200pxのグラフに次のデータを使用してラインチャートを描画したいとします。
const data = [
{ x: 0, y: 30 },
{ x: 25, y: 15 },
{ x: 50, y: 20 }
]
y軸の値を0から30(yの最大値)、x軸の値を0から50(xの最大値)とし、データがグラフ領域全体を使うように描画するのが理想です。
d3.maxを使用してデータセット内の最大値を検索し、入力したx、y値を出力するSVG線描画用のx、y座標に変換するためのスケール関数を生成します。
const width = 500;
const height = 200;
const xMax = d3.max(data, (d)=> d.x)
const yMax = d3.max(data, (d)=> d.y)
const xScale = d3.scale.linear()
.domain([0, xMax]) // input domain
.range([0, width]) // output range
const yScale = d3.scale.linear()
.domain([0, yMax]) // input domain
.range([height, 0]) // output range
これらのスケール関数はGitHubの貢献度チャートで説明したinterpolate関数に似ていて、単純に入力値を出力範囲内の値にマッピングする関数です。
xScale(0) -> 0
xScale(10) -> 100
xScale(50) -> 500
スケール関数は入力範囲外の値のスケールにも対応しています。
xScale(-10) -> -100
xScale(60) -> 600
次のコードのように線を描画する関数内で、スケール関数も使用できます。
const line = d3.svg.line()
.x((d)=> xScale(d.x))
.y((d)=> yScale(d.y))
.interpolate("linear")
スケール関数を使用して、出力範囲のまわりのpaddingも簡単に設定できます。
const padding = 20;
const xScale = d3.scale.linear()
.domain([0, xMax])
.range([padding, width - padding])
const yScale = d3.scale.linear()
.domain([0, yMax])
.range([height - padding, padding])
ここまで説明したコードを使うことで、動的なデータセットを使った線グラフを描画でき、上下左右に20pxのpaddingを持ちつつ、幅500px・高さ200pxに収まるようになります。
線形スケールはもっとも一般的なスケールですが、指数スケールのpow、名前や分類など非数値データを表すordinalスケールなどもあります。データ範囲へのマッピングには定量的スケールや順序尺度、さらに時間スケールもあります。
たとえば、次にコードのように人の一生涯を0から500の間の数値にマッピングするスケール関数の生成もできます。
const life = d3.time.scale()
.domain([new Date(1986, 1, 18), new Date()])
.range([0, 500])
// At which point between 0 and 500 was my 18th birthday?
life(new Date(2004, 1, 18))
アニメーション化したフライトの可視化
これまでは静的なグラフィックのみを説明してきました。ここからは、経過時間で変動する、オーストラリアのメルボルンとシドニーの間を飛行するフライトのアニメーションエフェクトを作成してみます。
このSVGは文字列、線、円で、次のように構成されています。
<svg id="chart" width="600" height="500">
<text class="time" x="300" y="50" text-anchor="middle">6:00</text>
<text class="origin-text" x="90" y="75" text-anchor="end">MEL</text>
<text class="dest-text" x="510" y="75" text-anchor="start">SYD</text>
<circle class="origin-dot" r="5" cx="100" cy="75" />
<circle class="dest-dot" r="5" cx="500" cy="75" />
<line class="origin-dest-line" x1="110" y1="75" x2="490" y2="75" />
<!-- for each flight in the current time -->
<g class="flight">
<text class="flight-id" x="160" y="100">JQ 500</text>
<line class="flight-line" x1="100" y1="100" x2="150" y2="100" />
<circle class="flight-dot" cx="150" cy="100" r="5" />
</g>
</svg>
動的要素は時刻とフライトグループで、次のようなデータを使います。
let data = [
{ departs: '06:00 am', arrives: '07:25 am', id: 'Jetstar 500' },
{ departs: '06:00 am', arrives: '07:25 am', id: 'Qantas 400' },
{ departs: '06:00 am', arrives: '07:25 am', id: 'Virgin 803' }
]
動的に変化する時間を表すためのx座標を割り出すには、チャート内で出発、到着時刻をx座標に配置するための時間スケールを生成する必要があります。最初は作業を簡単にするために、前述したデータをループしてDateオブジェクトとスケール関数を追加していきます。 Moment.jsを使用すると日時の変換、操作が容易になります。
data.forEach((d)=> {
d.departureDate = moment(d.departs, "hh-mm a").toDate();
d.arrivalDate = moment(d.arrives, "hh-mm a").toDate();
d.xScale = d3.time.scale()
.domain([departureDate, arrivalDate])
.range([100, 500])
});
前のコードで生成されるxScale関数に動的に変換する日時を渡すだけで各フライトのx座標を導き出せます。
レンダリングループ
出発、到着時刻は5分刻みのため、データも最初の便の出発から最終便の到着まで5分置きに時間経過させます。
let now = moment(data[0].departs, "hh:mm a");
const end = moment(data[data.length - 1].arrives, "hh:mm a");
const loop = function() {
const time = now.toDate();
// Filter data set to active flights in the current time
const currentData = data.filter((d)=> {
return d.departureDate <= time && time <= d.arrivalDate
});
render(currentData, time);
if (now <= end) {
// Increment 5m and call loop again in 500ms
now = now.add(5, 'minutes');
setTimeout(loop, 500);
}
}
開始、更新、終了
D3.jsを使うと、次の場合に要素の変形、トランジションを設定できます。
- 新しいデータ要素が入ってきたとき(開始:Enter)
- 既存のデータ要素が変化したとき(更新:Update)
- 既存のデータ要素が削除されたとき(終了:Exit)
const render = function(data, time) {
// render the time
d3.select('.time')
.text(moment(time).format("hh:mm a"))
// Make a d3 selection and apply our data set
const flight = d3.select('#chart')
.selectAll('g.flight')
.data(data, (d)=> d.id)
// Enter new nodes for any data point with an id not in the DOM
const newFlight = flight.enter()
.append("g")
.attr('class', 'flight')
const xPoint = (d)=> d.xScale(time);
const yPoint = (d, i)=> 100 + i * 25;
newFlight.append("circle")
.attr('class',"flight-dot")
.attr('cx', xPoint)
.attr('cy', yPoint)
.attr('r', "5")
// Update existing nodes in selection with id's that are in the data
flight.select('.flight-dot')
.attr('cx', xPoint)
.attr('cy', yPoint)
// Exit old nodes in selection with id's that are not in the data
const oldFlight = flight.exit()
.remove()
}
トランジション
前のコードでは500ミリ秒ごとにチャートをレンダリングし、5分経過するごとに次の操作をしています。
- 時刻を更新する
- 新しいフライトがあれば、フライトグループを表す円を作成する
- 現在飛行中のフライトのx/y座標を更新する
- 到着したフライトグループを削除する
前のコードは正常に動作しますが、グラフ内のデータトランジションをよりスムーズにします。D3.jsでセレクションした要素に対して、属性やスタイルを設定する前に、transition()関数を使用し、アニメーションの表示時間をdurationで、トランジションの動きをeaseで設定します。
次の例のとおり、新しいフライトグループの透明度を不透明な状態からはっきりと見える状態まで徐々に上げていきます。
const newFlight = flight.enter()
.append("g")
.attr('class', 'flight')
.attr('opacity', 0)
newFlight.transition()
.duration(500)
.attr('opacity', 1)
既存のフライトグループが見えない状態になるまで透明度を徐々に下げていきます。
flight.exit()
.transition()
.duration(500)
.attr('opacity', 0)
.remove()
x、yの座標間をスムーズに移動できるようにします。
flight.select('.flight-dot')
.transition()
.duration(500)
.ease('linear')
.attr('cx', xPoint)
.attr('cy', yPoint)
tween関数を使用して5分経過する間の時間もトランジションさせることで、5分おきではなく、1分ごとに時刻表示できます。
const inFiveMinutes = moment(time).add(5, 'minutes').toDate();
const i = d3.interpolate(time, inFiveMinutes);
d3.select('.time')
.transition()
.duration(500)
.ease('linear')
.tween("text", ()=> {
return function(t) {
this.textContent = moment(i(t)).format("hh:mm a");
};
});
tは0から1のトランジション間の経過を表す値です。
よりクリエイティブに
記事で紹介しきれなかったライブラリーがたくさんあります。以下のライブラリーにもさらに詳しく実例を交えながら説明したかったものがありますが、本末転倒にならないよう学んでいくことが大切です。
- Polar Clock(ポーラー時計)
- Force Layouts(力指向グラフ)
- Geography & Projections(地図&投影法の指定)
- Voronoi Tessellation(ボロノイ図)
- Prim’s Algorithm V(プリム法)
- OMG Particles II(オーマイゴッド粒子II)
D3.jsギャラリーにはもっとたくさんの実例があります。より深く学びたい人はScott MurrayのD3.jsチュートリアルとD3.jsドキュメントを読んでみることを強くおすすめします。どちらもとてもすばらしいです。
実践的な例を交えて上級者向けにD3.jsの概要を説明してきましたが、Selections、Scales、Transitionsがどのように動作するか理解できたと思います。ぜひデータを表現するのに最適な方法を考え、楽しみながらユニークなデータビジュアライゼーションを作ってください。
(原文:Learn to Create D3.js.js Data Visualizations by Example)
[翻訳:谷口夕紀]
[編集:Livit]