2時間縛りでd3.js挑戦してみた
何この記事
あんちべという人から無茶ぶりがきたので対応した
mizchi、d3ブログ書いてくれた呑む
— 3D円グラフ皆殺し (@AntiBayesian) 2014, 3月 2
@AntiBayesian D3まだ極めてないです
— 高意識エネルギー (@mizchi) 2014, 3月 2
@mizchi 2時間やろう、早く極めたまえ
— 3D円グラフ皆殺し (@AntiBayesian) 2014, 3月 2
@AntiBayesian おっしゃ待ってろ
— 高意識エネルギー (@mizchi) 2014, 3月 2
前提
よく誤解されるんですが、D3.jsはグラフ描画ツールではなく、JavaScriptでSVGを生成するためのjQuery風DSLで、DSLとはいえかなりローレベルなライブラリです。SVGはベクタグラフィックスを生成する規格。ブラウザ上のSVGは、図形を書けるDOMであり、他のDOMノードと同じくクリックイベントをとれたりするのが便利。
単純に図形を生成して配置するだけならば、raphael.jsとかsnap.svgの方が楽かもしれない。
- Raphaël—JavaScript Library http://raphaeljs.com/
- Snap.svg - Home http://snapsvg.io/
というぐらいの知識でd3を触り始めたわけですが
D3を理解する
まずはこのチュートリアル読みながら。
sampleSVG = d3.select("#viz") .append("svg") .attr("width", 100) .attr("height", 100) sampleSVG.append("circle") .style("stroke", "gray") .style("fill", "white") .attr("r", 40) .attr("cx", 50) .attr("cy", 50) .on("mouseover", -> d3.select(this).style("fill", "aliceblue")) .on("mouseout", -> d3.select(this).style("fill", "white"))
d3.select は document.querySelectorと同等のCSSクエリによるDOM検索で、d3化されたインスタンスにくるんで返却する。 このコードでは、100x100の背景を確保し、円を書く。このマウスはhoverイベントを扱っていて、オンマウスで色が変わる。ってのはコードを読めばわかる。 このコードによって次のSVGが生成されている。
<svg width="100" height="100"> <circle r="40" cx="50" cy="50" style="stroke: #808080; fill: #ffffff;"></circle> </svg>
circleのmouseover, mouseoutはjavascript側から定義を与えるとしてここからは見えないが、よくある普通のxmlとして表現されている。o circleノードがどのような属性を持ちうるかはW3Cのドキュメントとかでググってればわかる。
- Scalable Vector Graphics (SVG) 1.1 (Second Edition) http://www.w3.org/TR/SVG/
- SVG Tutorial http://www.w3schools.com/svg/
ここでわかるのは、最悪d3やその他ライブラリでsvgで生成せずとも、XMLを手打ちで入力すれば(コールバックイベントなどは定義できないが)図形の描画はできる、ということだ。好きなテンプレートエンジンでも良い。ただロジック注入やjavascriptとの親和性を考えてjsで扱うためにDSLとして実装しようぜってのがd3.jsの存在意義で、学習コストを少なくするためにjQueryに似せてある。attrなんて全く同じだ。
ちょうどjadeがある環境だったので試してみた
svg(width="100",height="100") for i in [1,2,3] circle(r=30,cx=i*20,cy="50",style="stroke: #808080; fill: #ffffff;")
まあ手書きでやれなくもない。
アニメーション
transition()でアニメーションオブジェクトを作ってそこにメソッドチェーンで属性を継ぎ足す。
sampleSVG.append("circle") .style("stroke", "gray") .style("fill", "white") .attr("r", 40) .attr("cx", 50) .attr("cy", 50) .transition() .delay(100) .duration(1000) .attr("r", 10) .attr("cx", 30) .style("fill", "black");
たぶん、イージング関数とかどこで入れられるんだろうけど、まあ調べればわかるだろう
コールバックでアニメーションを発火させようとするとこうなる。
sampleSVG.append("circle") .style("stroke", "gray") .style("fill", "white") .attr("r", 40) .attr("cx", 50) .attr("cy", 50) .on 'mousedown', -> d3.select(@).transition() .delay(100) .duration(1000) .attr("r", 10) .attr("cx", 30) .style("fill", "black");
このselect(this)に強烈なjQuery志向を感じる。(個人的にはこのようなthisコンテキストを頻繁に切り替えるスタイルは好きではない)
データバインディング
データバインディングの仕組みがある(まじかよ)
dataset = [] i = 0 for i in [0...5] dataset.push(Math.round(Math.random()*100)); sampleSVG = d3.select("#viz") .append("svg") .attr("width", 400) .attr("height", 75); sampleSVG.selectAll("circle") .data(dataset) .enter().append("circle") .style("stroke", "gray") .style("fill", "white") .attr("height", 40) .attr("width", 75) .attr("x", function(d, i){return i*80}) .attr("y", 20);
双方向じゃないですね…単にインジェクションっぽい。
HTMLを生成
テーブルを生成するらしい。
# 適当な二次元配列を生成 dataset = for i in [0...5] for j in [0...3] then "Row:"+i+",Col:"+j d3.select("body") .append("table") .style("border-collapse", "collapse") .style("border", "2px black solid") .selectAll("tr") .data(dataset) .enter().append("tr") .selectAll("td") .data((d)->d) .enter().append("td") .style("border", "1px black solid") .style("padding", "10px") .on("mouseover", ->d3.select(this).style("background-color", "aliceblue")) .on("mouseout", ->d3.select(this).style("background-color", "white")) .text((d) ->d) .style("font-size", "12px");
これ、svgっていうかtable > tr > tdなのでただのhtmlですね。と思いながら生成されたHTMLをみると本当にただのHTMLで、svgタグでさえなかった。
<table style="border-collapse: collapse; border: 2px solid black;"><tr><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:0,Col:0</td><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:0,Col:1</td><td style="border: 1px solid black; padding: 10px; font-size: 12px; background-color: white;">Row:0,Col:2</td></tr><tr><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:1,Col:0</td><td style="border: 1px solid black; padding: 10px; font-size: 12px; background-color: white;">Row:1,Col:1</td><td style="border: 1px solid black; padding: 10px; font-size: 12px; background-color: white;">Row:1,Col:2</td></tr><tr><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:2,Col:0</td><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:2,Col:1</td><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:2,Col:2</td></tr><tr><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:3,Col:0</td><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:3,Col:1</td><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:3,Col:2</td></tr><tr><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:4,Col:0</td><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:4,Col:1</td><td style="border: 1px solid black; padding: 10px; font-size: 12px;">Row:4,Col:2</td></tr></table>
まあ普通のDOM生成もできる、ということで。 今ちゃんと最初のサンプル見返すと append('svg')してるから、別にsvgそのものに特化したライブラリでもない模様。(じゃあここまでだとただのDOMマニピュレータじゃん!)
グラフを書く
といった感じで基礎を抑えたところで、データビジュアライゼーションの為の機能を使ってみましょう。 参考にしたのはこれ。 Line Chart http://bl.ocks.org/mbostock/3883245
なぜかcsvやtsvのパーサーとローダが付属してます。
csv = """ Year,Make,Model,Length 1997,Ford,E350,2.34 2000,Mercury,Cougar,2.38 """ console.log d3.csv.parseRows(csv) # [Array[4], Array[4], Array[4]] # 0: Array[4] # 0: "Year" # 1: "Make" # 2: "Model" # 3: "Length" # length: 4 # ...
JSONにっぽい素のJSになりました。
今まではstyleで属性を指定していましたが、svgはただのDOMノードなのでCSSが適用されます。次のようなCSSを挿入します。
<style> body { font: 10px sans-serif; } .axis path, .axis line { fill: none; stroke: #000; shape-rendering: crispEdges; } .x.axis path { display: none; } .line { fill: none; stroke: steelblue; stroke-width: 1.5px; } </style>
ただのCSS。
で、あとはサンプルコードを参考にぐわーっと書きました。ステップバイステップで解説するのが非常に辛くなってきたので、コメントで注釈つけました。(元のサンプルコードがだいぶ品質が低かったので少しリファクタリングしてあります)
生成した図はこれ
色が反転してるのはなんかミスったのかなんなのかまあいいや。
#このメソッドを読んだら描画する draw = (data) -> # 余白等の細々したパラメータ margin = top: 20, right: 20, bottom: 30, left: 50 width = 960 - margin.left - margin.right height = 500 - margin.top - margin.bottom # 描画領域を作成 svg = d3.select("body").append("svg") # 領域を指定 .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); # x軸 x = d3.time.scale().range([0, width]) # レンジオブジェクト x.domain(d3.extent(data, (d) -> d.date)) # 最小値/最大値を設定 xAxis = d3.svg.axis().scale(x).orient("bottom") # 下線のオブジェクト svg.append("g") # 描画オブジェクトを追加 .attr("class", "x axis") # マークアップ用classを追加 .attr("transform", "translate(0,#{height})") .call(xAxis) # y軸 # だいたい同上 y = d3.scale.linear().range([height, 0]) y.domain(d3.extent(data, (d) -> d.close)) yAxis = d3.svg.axis().scale(y).orient("left") svg.append("g") .attr("class", "y axis") .call(yAxis) # Priceの文字 .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .style("text-anchor", "end") .text("Price ($)"); # プロット line = d3.svg.line() .x((d) -> x(d.date)) # インジェクトされたデータからx軸として用いる要素を指定する高階関数 .y((d) -> y(d.close)) # 同上 svg.append("path") .datum(data) # 配列を各プロットにバインド .attr("class", "line") .attr("d", line) $ -> d3.tsv "data.tsv", (error, data) -> # データを整形 parseDate = d3.time.format("%d-%b-%y").parse data.forEach (d) -> d.date = parseDate(d.date) d.close = +d.close draw(data)
特徴
- データを扱うためのユーティリティ関数
- DOMを扱うためのユーティリティDSL
- グラフ描画に便利なユーティリティクラス
ここで問題となるであろうのは、ユーティリティの集合体であって、特定のグラフを書くためにロックインしてるわけではないです。
extentとかなんぞや、みたいなのは、 mbostock/d3 Wiki https://github.com/mbostock/d3/wiki/Arrays#wiki-d3_extent を読めば書いてある。medianとかも取れる。
雑感
R感
RstudioとかをJavaScriptで再現したかったのではないでしょうか
jQueryのだめなところを参考にしている
メソッドチェーンスタイルは現代的な構造化DOMのフロントエンドの潮流とは思想的に合わず、あんまりセンスが良いとは思えません。
ユーティリティのカバー範囲のセンスが悪い
オールインワンのくせにそれぞれのカバー範囲が狭く、抽象度が妙なところで低い。個人の意見です。
サンプルコードがどれも品質が低い
そりゃデータ可視化なんてやる人が本職フロントエンドJavaScriptの人だとは思いませんが、それにしても…ってのが多かったです。
誰に向いているか?
- Rっぽく自由度が高い図を書きたい
- jQueryっぽい手続きでグラフを書きたい
- クライアントで多様なデータをJSで(それなりに)完結したい
- 細かいユーティリティが欲しい
グラフ描画したいだけならHighchartとかの方が楽です。ぶっちゃけデータの下処理とかはサーバーやこれに渡す前の段階でやるほうが楽だと思うんですよね。
学習コストが妙に高い気がするのでがっつり使い込むのでなければ手をだすのは危険な気がします。
以上。あとは @AntiBayesian がどうにかしてくれるでしょう。