jQuery: ビジュアライズ系で紹介を避けてきた D3.js は究極の低レベルインフラなのよ

あえて避けたい夜もある。

ワタシのサイトでの紹介だけに基いてこれら(jQuery のチャート系プラグイン)について知ろうとしている人であっても、注意深い人ならとっくに気付いているはずなんだけれど、D3.js は実は既に「使っている」。Built on top of d3.js なライブラリが非常に多いからだ。アタシがこれまで触れてきたものでは plotly.jsCytoscape.jsCubism.js がそう。

ワタシももちろん plotly.js を見つけたその日に真っ先に気付いたし、20 best JavaScript charting libraries で紹介されてるのももちろん気付いてた。ですぐさま公式サイトに飛んでみて、印象を確かめて、「究極の低レベルインフラだよなこれは」と即座に理解したので、検証も紹介も控えてた。

20 best JavaScript charting libraries の著者はすごく誠実な人だね、ほかの紹介サイトとは一味も二味も違う。自分で公式サイトに行ってみて上述の第一印象を持ってからこの紹介サイトの文章を初めて読んだが、とてもポイントをおさえた良い紹介文だ:

D3 is often the first name that comes up for any kind of data visualisation. It’s a really powerful open source project that lets you create stunning visual effects and graphics by dynamically updating the DOM. In a way, it brings data to life using HTML, SVG and CSS.

It conforms to the W3C web standards and is compatible across browsers. Developers tend to love it for the range of features it brings, things like “Enter and Exit” and powerful transitions. You can see some examples built with D3 over here.

The caveat is that it does not ship with pre-built charts, and has a pretty steep learning curve to get even basic charts up. But developers are an innovative bunch and have come up with quite a few out-of-the-box charting libraries, based on D3. We’ll cover some of the best ones later.

ワタシが「xx ベストほにゃらら」サイトを毛嫌いするのは「観点がない」ことだが、この著者はそうではない。誠実、というのはこういうことを言う。

ワタシや 20 best… の著者が言ってることは、実際に公式サイトに行ってみればわかると思うよ。ここな:

このギャラリーが、「凄まじい」わけである。で、「すんげー、こんなことも出来るのか!!」と大喜びするわけよ。けどさ、例えばワタシが plotly.js で苦労してみせた Scatterplot Matrix一撃で出来るのね! と思ったらダメ。こんなだよ:

  1 <!DOCTYPE html>
  2 <html>
  3   <head>
  4     <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
  5     <link type="text/css" rel="stylesheet" href="style.css"/>
  6     <style type="text/css">
  7 
  8 svg {
  9   font-size: 14px;
 10 }
 11 
 12 .axis {
 13   shape-rendering: crispEdges;
 14 }
 15 
 16 .axis line {
 17   stroke: #ddd;
 18   stroke-width: .5px;
 19 }
 20 
 21 .axis path {
 22   display: none;
 23 }
 24 
 25 rect.extent {
 26   fill: #000;
 27   fill-opacity: .125;
 28   stroke: #fff;
 29 }
 30 
 31 rect.frame {
 32   fill: #fff;
 33   fill-opacity: .7;
 34   stroke: #aaa;
 35 }
 36 
 37 circle {
 38   fill: #ccc;
 39   fill-opacity: .5;
 40 }
 41 
 42 .legend circle {
 43   fill-opacity: 1;
 44 }
 45 
 46 .legend text {
 47   font-size: 18px;
 48   font-style: oblique;
 49 }
 50 
 51 .cell text {
 52   pointer-events: none;
 53 }
 54 
 55 .setosa {
 56   fill: #800;
 57 }
 58 
 59 .versicolor {
 60   fill: #080;
 61 }
 62 
 63 .virginica {
 64   fill: #008;
 65 }
 66 
 67     </style>
 68   </head>
 69   <body>
 70     <h2 style="font-size:24px;">
 71       Edgar Anderson’s <i>Iris</i> data set<br>
 72       scatterplot matrix
 73     </h2>
 74     <script type="text/javascript" src="d3/d3.js"></script>
 75     <script type="text/javascript" src="d3/d3.csv.js"></script>
 76     <script type="text/javascript">
 77 
 78 d3.csv("iris.csv", function(flowers) {
 79 
 80   // Size parameters.
 81   var size = 140,
 82       padding = 10,
 83       n = 4,
 84       traits = ["sepal length", "sepal width", "petal length", "petal width"];
 85 
 86   // Position scales.
 87   var x = {}, y = {};
 88   traits.forEach(function(trait) {
 89     // Coerce values to numbers.
 90     flowers.forEach(function(d) { d[trait] = +d[trait]; });
 91 
 92     var value = function(d) { return d[trait]; },
 93         domain = [d3.min(flowers, value), d3.max(flowers, value)],
 94         range = [padding / 2, size - padding / 2];
 95     x[trait] = d3.scale.linear().domain(domain).range(range);
 96     y[trait] = d3.scale.linear().domain(domain).range(range.reverse());
 97   });
 98 
 99   // Axes.
100   var axis = d3.svg.axis()
101       .ticks(5)
102       .tickSize(size * n);
103 
104   // Brush.
105   var brush = d3.svg.brush()
106       .on("brushstart", brushstart)
107       .on("brush", brush)
108       .on("brushend", brushend);
109 
110   // Root panel.
111   var svg = d3.select("body").append("svg:svg")
112       .attr("width", 1280)
113       .attr("height", 800)
114     .append("svg:g")
115       .attr("transform", "translate(359.5,69.5)");
116 
117   // Legend.
118   var legend = svg.selectAll("g.legend")
119       .data(["setosa", "versicolor", "virginica"])
120     .enter().append("svg:g")
121       .attr("class", "legend")
122       .attr("transform", function(d, i) { return "translate(-179," + (i * 20 + 594) + ")"; });
123 
124   legend.append("svg:circle")
125       .attr("class", String)
126       .attr("r", 3);
127 
128   legend.append("svg:text")
129       .attr("x", 12)
130       .attr("dy", ".31em")
131       .text(function(d) { return "Iris " + d; });
132 
133   // X-axis.
134   svg.selectAll("g.x.axis")
135       .data(traits)
136     .enter().append("svg:g")
137       .attr("class", "x axis")
138       .attr("transform", function(d, i) { return "translate(" + i * size + ",0)"; })
139       .each(function(d) { d3.select(this).call(axis.scale(x[d]).orient("bottom")); });
140 
141   // Y-axis.
142   svg.selectAll("g.y.axis")
143       .data(traits)
144     .enter().append("svg:g")
145       .attr("class", "y axis")
146       .attr("transform", function(d, i) { return "translate(0," + i * size + ")"; })
147       .each(function(d) { d3.select(this).call(axis.scale(y[d]).orient("right")); });
148 
149   // Cell and plot.
150   var cell = svg.selectAll("g.cell")
151       .data(cross(traits, traits))
152     .enter().append("svg:g")
153       .attr("class", "cell")
154       .attr("transform", function(d) { return "translate(" + d.i * size + "," + d.j * size + ")"; })
155       .each(plot);
156 
157   // Titles for the diagonal.
158   cell.filter(function(d) { return d.i == d.j; }).append("svg:text")
159       .attr("x", padding)
160       .attr("y", padding)
161       .attr("dy", ".71em")
162       .text(function(d) { return d.x; });
163 
164   function plot(p) {
165     var cell = d3.select(this);
166 
167     // Plot frame.
168     cell.append("svg:rect")
169         .attr("class", "frame")
170         .attr("x", padding / 2)
171         .attr("y", padding / 2)
172         .attr("width", size - padding)
173         .attr("height", size - padding);
174 
175     // Plot dots.
176     cell.selectAll("circle")
177         .data(flowers)
178       .enter().append("svg:circle")
179         .attr("class", function(d) { return d.species; })
180         .attr("cx", function(d) { return x[p.x](d[p.x]); })
181         .attr("cy", function(d) { return y[p.y](d[p.y]); })
182         .attr("r", 3);
183 
184     // Plot brush.
185     cell.call(brush.x(x[p.x]).y(y[p.y]));
186   }
187 
188   // Clear the previously-active brush, if any.
189   function brushstart(p) {
190     if (brush.data !== p) {
191       cell.call(brush.clear());
192       brush.x(x[p.x]).y(y[p.y]).data = p;
193     }
194   }
195 
196   // Highlight the selected circles.
197   function brush(p) {
198     var e = brush.extent();
199     svg.selectAll(".cell circle").attr("class", function(d) {
200       return e[0][0] <= d[p.x] && d[p.x] <= e[1][0]
201           && e[0][1] <= d[p.y] && d[p.y] <= e[1][1]
202           ? d.species : null;
203     });
204   }
205 
206   // If the brush is empty, select all circles.
207   function brushend() {
208     if (brush.empty()) svg.selectAll(".cell circle").attr("class", function(d) {
209       return d.species;
210     });
211   }
212 
213   function cross(a, b) {
214     var c = [], n = a.length, m = b.length, i, j;
215     for (i = -1; ++i < n;) for (j = -1; ++j < m;) c.push({x: a[i], i: i, y: b[j], j: j});
216     return c;
217   }
218 });
219 
220     </script>
221   </body>
222 </html>

「has a pretty steep learning curve to get even basic charts up」の意味はすぐにわかるであろう?

逆に言えば、plotly.jsCytoscape.js がターゲットとしていないようなチャートを描きたい場合だって、ないことはないであろう? そういった場合に、D3.js のレベルに下りてしまって この凄まじ過ぎるギャラリーから自分がやりたいことに近いものを選んで「猿真似を試みる」ということは、考えてもいいんだよね(当面やらないけどいつかやるかもしんない)。jQuery: Cytoscape.js お試せたで前置きしたように、この手のを「手書きで javascript をゴリゴリ書く」わきゃぁなくて、何かテンプレートエンジンの類の力をどうせ借りるわけだよ、その場合「一回だけちゃんと理解する」必要はあっても、永遠に記憶しとく必要はないわけさ。

ギャラリーにはとかく興味をひくものが多いんだけれど、これはやってみたいなぁと思った:

画像キャプチャだけだと伝わらないので、是非実際にここを訪れてみて。マウスホイールを動かしてみるのだぞ。