iPhoneで Grafanaの グラフを 参照できる アプリ Grafanizer 作ってます。 詳しくは こちらへ

Vue.jsとD3.jsを使ってグラフを描画してみた

Sep 15, 2017  
#d3.js #vue.js

はじめに

最近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];
    }
  }
});

長いといっても各メソッドは簡単で短いコードなので、見ただけで内容的には大体わかってもらえると思う。 BottomLeft が出てくるのがちょっとイケてないのだが。

で、実際に迷った部分っていうのが 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 の値を元にして自動的にクラスをあててくれる。今回は lineg タグが1secかけて移動するというCSSになっている。

ちなみに、複数要素がある場合が transition-group で一つの場合は transition らしいが、違いまでは追ってない。

まとめ

参考になるページがあったので、比較的簡単にVUe.js的にD3.jsグラフを描画させることができた。

今回のサンプルでは実装していないけど、例えば v-if でフィルターをかけたり class で色分けしたりするのもHTML部分で実装できるので、UI部分はすごく分かりやすく実装できそう。D3.jsのコードは分かる人が見れば分かるのだが、分からない人が見たらどこで何してるのかよくわからなかったよね。

Vue.js最高ですわ。


de.select での enter も分かりづらいと言うほどでもなかったが、わかり易さから言ったらこちらの方が格段に上だと思う。UI部分とロジック部分が別れているというのは見てても書いてても非常に気持ちが良い。