はじめに
最近Vue.jsを使って開発する機会が多い。面倒なDOM操作から開放されてロジックだけに集中できるので、コードを書いていて気持ちが良いうえバグの発生も抑えることができるので精神的にも非常に良い。
さて、そんな今日このごろだが、Vue.jsで構築してるシステムにグラフ表示させる必要があったのでいろいろ調べながら実装してみた。
JavaScriptでグラフを描画するならD3.jsで決まりなのだが、実はVer4になってからは一度も触ってなかった。なので、これをいい機会と思いVue.jsらしくD3.jsでのグラフを実装して見た。
/* ちなみに、Chrome以外ではトランジションがうまく動いてないっぽい */
参考にしたのは 「Vue.js+d3.jsで折れ線グラフを描く」 と 「Vue.js + d3.js (using virtual DOM)」 の二つのページ。
普通にD3.jsでグラフ描画させるときとは結構お作法が違うのでいろいろ迷うところもあったが、まずはVer3のサンプルでもいいから描画させたいグラフを選び、そのSVGのソースを見ながらVue.jsのテンプレートに落とし込んでいくのが一番の近道だった。
ということで、簡単に解説でも。
HTML部分
まずはHTML部分。
<div id="graph">
<line-graph :lines="lines"></line-graph>
</div>
<template id="line">
<svg :width="width" :height="height" :viewBlx="'0 0 ' + width + ' ' + height">
<transition-group name="line" tag="g">
<path class="line" :style="lineStyle(i)"
v-for="(line, i) in lines" :key="i"
:d="d(line)" :transform="transform()"
:x-scale="scale('Left')" :y-scale="scale('Bottom')">
</path>
</transition-group>
<g :transform="transform()">
<g class="axis">
<g :id="'axis' + orient" :transform="transform(orient)"
v-for="(orient, i) in axis" :key="i"></g>
</g>
</g>
</svg>
</template>
#graph
なコンテナ部分を用意し、その後で埋め込むテンプレートを準備している。
実際に描画してるのは v-for="(line, i) in lines"
でループさせてる lines.length
分の path
と、縦横2本の :id="'axis' + orient"
なAxisのみ。実際はAxisにTicksが複数入るのだけど、それは後から説明するJavaScriptで自動的に描画される仕組みになってるのでTicks用のテンプレートは不要。
なので、Vue.js用に用意するHTMLはこれだけで済む。ここだけ見ても普通のD3.jsの作法とは全然違うのがわかると思う。あと、実際にSVGのソースを見たことがある人だったらイメージしやすいと思う。
JavaScript部分
次にJavaScriptの部分だが、こちらは少し長い。
テンプレート処理
一つ目は #line
用の処理から。
Vue.component("line-graph", {
template: "#line",
props: ["lines"],
data: function () {
return {
width: 600, height: 300, margin: 20, axis: ["Left", "Bottom"]
}
},
mounted: function () {
for (var i = 0; this.axis.length > i; i++) {
var o = this.axis[i];
d3.select("#axis" + o).call(d3["axis" + o](this.scale(o)));
}
},
computed: {
viewBox: function () {
return [0, 0, this.width, this.height].join(" ");
},
xArray: function () {
var arr = [];
this.lines.forEach(function (l) {
arr = arr.concat(l.map(function (d) { return d[0]; }));
});
return arr;
},
yArray: function () {
var arr = [];
this.lines.forEach(function (l) {
arr = arr.concat(l.map(function (d) { return d[0]; }));
});
return arr;
}
},
methods: {
lineStyle: function (i) {
return { stroke: this.stroke(i) };
},
scale: function (o) {
var linear = d3.scaleLinear();
if (["Left", "Right"].indexOf(o) > -1) {
return linear.domain([10, 0]).range([0, this.height - this.margin * 2]);
} else if (["Top", "Bottom"].indexOf(o) > -1) {
return linear.domain(d3.extent(this.xArray)).range([0, this.width - this.margin * 2]);
}
return nil;
},
transform: function (o) {
if (o == undefined) {
return "translate(" + this.margin + "," + this.margin / 2 + ")";
}
var x = (o == "Right") ? this.width : 0;
var y = (o == "Bottom") ? this.height - this.margin * 2 : 0;
return "translate(" + x + "," + y + ")";
},
d: function (l) {
var xScale = this.scale("Bottom");
var yScale = this.scale("Left");
var line = d3.line();
line
.curve(d3.curveMonotoneX)
.x(function (d) { return xScale(d[0]); })
.y(function (d) { return yScale(d[1]); });
return line(l);
},
stroke: function (i) {
return d3.schemeCategory10[i];
}
}
});
長いといっても各メソッドは簡単で短いコードなので、見ただけで内容的には大体わかってもらえると思う。 Bottom
と Left
が出てくるのがちょっとイケてないのだが。
で、実際に迷った部分っていうのが mounted
で実行しているAxis部分だったのだが、結論的には .call
メソッドを実行すればScaleとTicksにあわせて自動的に描画してくれる。Ver3のときから触ってるから .call
メソッドで作成するのは理解してたんだけど、作成されているSVGのソースから逆に実装させてたので ticks
はどう表すべきなのだろう?って難しく考えすぎちゃった。
他の部分は、D3.jsでキーになるスケール計算と実際の値にそのスケールを掛け合わすコードが大半となっている。
Vue化処理
二つ目は実施にHTMLをVue化させるコード。
var app = new Vue({
el: "#graph",
data: {
lines: null
},
created: function () {
this.lines = this.newLines();
setInterval(function (app) {
app.lines = app.newLines();
}, 1000, this);
},
methods: {
newLines: function () {
var lines = [];
for (var i = 0; i < 3; i++) {
var line = [];
for (var j = 0; j < 11; j++) {
line.push([j, Math.floor(Math.random() * 10)
]);
}
lines.push(line);
}
return lines;
}
}
});
こちらはD3.jsにもVue.jsにもあまり関係のなく、データ作成のみを行う処理になっている。見ての通り1sec毎にデータをランダムに作り直しているだけなのだが、データを作り直すだけでグラフが変更されるのがVue.jsらしい。
CSS部分
最後にCSSだが、ランダムに動くトランジションはCSSで指定されている。
<style>
.line {
fill: none;
stroke-width: 2px;
transition: all 1s;
}
.line-move {
transition: transform 1s;
}
</style>
先程のHTMLの中に transition-group name="line" tag="g"
というタグがあったのだが、そこがVue.jsのトランジション機能で、Vue.jsが name
の値を元にして自動的にクラスをあててくれる。今回は line
な g
タグが1secかけて移動するというCSSになっている。
ちなみに、複数要素がある場合が transition-group
で一つの場合は transition
らしいが、違いまでは追ってない。
まとめ
参考になるページがあったので、比較的簡単にVUe.js的にD3.jsグラフを描画させることができた。
今回のサンプルでは実装していないけど、例えば v-if
でフィルターをかけたり class
で色分けしたりするのもHTML部分で実装できるので、UI部分はすごく分かりやすく実装できそう。D3.jsのコードは分かる人が見れば分かるのだが、分からない人が見たらどこで何してるのかよくわからなかったよね。
Vue.js最高ですわ。
de.select
での enter
も分かりづらいと言うほどでもなかったが、わかり易さから言ったらこちらの方が格段に上だと思う。UI部分とロジック部分が別れているというのは見てても書いてても非常に気持ちが良い。