WonderPlanet Tech Blog

アドバンストテクノロジー部やCTO室のメンバーを中心に最新の技術情報を発信しています。

D3.js v5でデータを可視化 - ツールチップ付きのグラフを作成する方法

こんにちはAT部の@y-matsushitaです。
今回はデータ分析結果の表示を行うために、ブラウザで動的コンテンツを描画するJavaScriptライブラリであるD3.jsを試してみたのでご紹介します。
本投稿ではD3.jsのv5で動作するコードを記載していきます。D3.jsの情報はv3向けが多かったのですが、せっかくなので新しいv5向けに書いてみました。バージョンによってコードの書き方が結構異なるので、見つけたサンプルが動作しない場合はバージョンの違いを疑ってみると良いかもしれません。

d3js.org

ツールチップ付き棒グラフ

以下のページに作成した棒グラフのデモとコードを載せています。
コードが長いのでコード全体は以下のデモページからご参照ください。本投稿では要所だけピックアップして説明していきます。

デモとソースコード

f:id:y-matsushita:20180605204311p:plain:w600

D3.jsの読み込み

index.htmlのsvgの下に以下のタグを書いてD3.jsを読み込みます。
読み込むバージョンはv5を指定して、その後に棒グラフを描画するコードが書かれたjsファイルを読み込みます。

index.html 13~14行目

<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="bar_chart.js"></script>

スケール設定

軸の表示領域をマージンを除いた幅に設定します。
paddingは0.2でバーとバーの間に20%の隙間を作ります。
yの座標は値が大きいほどに画面下に行くため、rangeの値を反転([cHeight, 0])しておきます。

bar_chart.js 17~18行目

var x = d3.scaleBand().rangeRound([0, cWidth]).padding(0.2);
var y = d3.scaleLinear().range([cHeight, 0]);

以下の部分でx軸に日付を、y軸に値の幅を設定します。
y軸はそのままだと最小値が0扱いになってしまうため、少し幅に余裕をもたせてあります。

bar_chart.js 28~29行目

x.domain(data.map(function(d) { return d.day; }));
y.domain([d3.min(data, function(d) { return d.value; }) / 1.005, d3.max(data, function(d) { return d.value; }) * 1.005]);

x軸とy軸の表示

x軸はグラフの高さcHeight分、y方向に座標をズラしてグラフの下に配置します。
また、表示後に日付に応じて曜日が土日であれば色を変えられるようにクラスを付与しておきます。 tickに対して直接classを指定することが出来なかったため、後の処理で日付から曜日を推定し土曜日であればsat、日曜日であればsunのクラスを付与しています。

bar_chart.js 44~58行目

g.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + cHeight + ")")
    .call(
      d3.axisBottom(x)
      .ticks(12)
      .tickFormat(d3.timeFormat("%Y/%m/%d"))
    );
// 横の目盛りを日付のみにして土日にクラスを付与する
var ticks = d3.selectAll(".axis--x text");
ticks.attr("class", function(d){
  if(d3.timeFormat("%a")(d) == "Sat") return "sat";
  if(d3.timeFormat("%a")(d) == "Sun") return "sun";
  return "weekday";
}).html(function(d) {return formatTime(d);});

色の指定は satsunクラスに対してcssで行います。

style.css 54~59行目

.sat{
  fill:#1874CD;
}
.sun{
  fill:#f2594b;
}

y軸では .tickSizeInner(-cWidth) でx軸方向に逆側へグリッドを伸ばします。
また text を追加して回転させ軸に沿う形で値を表示しました。

bar_chart.js 61~75行目

g.append("g")
    .attr("class", "axis axis--y")
    .call(
      d3.axisLeft(y)
      .ticks(6)
      .tickSizeInner(-cWidth)
      .tickFormat(function(d) { return d/1000 + "k"; }))
    .append("text")
    .attr("class", "axis-title")
    .attr("transform", "rotate(-90)")
    .attr("y", 6)
    .attr("dy", ".71em")
    .style("text-anchor", "end")
    .attr("fill", "#5D6971")
    .text("アクセス量");

棒グラフの描画

取得したdataをもとにバーを描画します。x軸方向に日付を設定し、y軸方向に値を入力しました。

bar_chart.js 88~102行目

g.append("g")
    .selectAll("rect")
    .data(data)
    .enter()
    .append("rect")
    .attr("class", "bar")
    .attr("x", function(d) {
      return x(d.day);
    })
    .attr("y", function(d) {
      return y(d.value);
    })
    .attr("width", x.bandwidth())
    .attr("height", function(d) { return cHeight - y(d.value); })
    .attr("fill", "steelblue");

フォーカスとツールチップの設定

まずグラフの上に透明な rect をグラフと同じ大きさで配置してレイヤーのように覆います。
この rectにはoverlayクラスを与えておき、cssでfill: none;pointer-events: all;を適用させておくとマウスイベントの効く透明なレイヤーが作成できます。
このレイヤーにマウスが乗った場合(mouseover)フォーカスとツールチップを表示させます。
逆にマウスがグラフ上から外れた場合(mouseout)非表示にします。そしてマウスが動いている場合(mousemove)はマウスに近い位置のバーをフォーカスしてツールチップに情報を表示します。

bar_chart.js 105~122行目

svg.append("rect")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .attr("class", "overlay")
    .attr("width", cWidth)
    .attr("height", cHeight)
    .on("mouseover", function() {
      tooltip.style("display", "block");
      focus.style("display", "block"); })
    .on("mouseout", function() {
      // tooltipとfocusを非表示
      tooltip.style("display", "none");
      focus.style("display", "none");
      // 棒グラフのfocus(一旦focusクラスを全て外す)
      g.selectAll(".bar").attr("class",function(d) {
        return "bar " + formatClass(d.day);
      })
     })
    .on("mousemove", mousemove);

マウス移動時の設定

まずはツールチップを作成しておきます。
日付と値を表示できるようにします。またツールチップにはマウスが乗ってしまっても動作に影響がでないようにcssにpointer-events: none;を適用させておきます。またツールチップの座標を絶対位置で指定できるようにposition:absolute;を適用させておきます。見た目についてはお好みでshadowなどを適用させると見やすくなると思います。

bar_chart.js 125~127行目

var tooltip = d3.select("#contents").append("div").attr("class", "tooltip"),
    tt_date = tooltip.append("time").attr("class", "tt_date"),
    tt_value = tooltip.append("div").attr("class", "tt_value");

style.css 34~36行目

.tooltip {
  position: absolute;
  pointer-events: none;

マウスの移動時はマウス位置をもとにinvertで日付情報に逆変換してマウス座標に近い日付を取得します。
ツールチップは特定した日付の情報を設定、位置をマウスカーソルから少しずらして表示します。
またマウスカーソルがグラフの中央より右側にある場合はツールチップをマウスカーソルの左側に表示するようにします。これによりマウスを画面端にもっていったときにグラフ外に表示されて崩れてしまうという事態を防ぎます。
このとき.duration(200).ease(d3.easeLinear)を設定しておけば移動をアニメーションさせて滑らかな移動の表現ができるようになります。
また特定した日付の.barクラスにcssで色を変えたfocusクラスをattrで追加しておくことでバーの色を変更することができます。これによりマウス座標に近い日付のバーにフォーカスを当てることができます。

bar_chart.js 128~162行目

function mousemove() {
  var x0 = x.invert(d3.mouse(this)[0]),
      i = bisectDate(data, x0, 1),
      d0 = data[i - 1],
      d1 = data[i],
      d = x0 - d0.day > d1.day - x0 ? d1 : d0;

  tt_date.html(function() {return formatTime(d.day);});
  tt_value.html(function() {return "アクセス数:"+d.value});

  // マウスの位置によりtooltipを表示位置を変更(右側 or 左側)
  var centerX = cWidth / 2;
  var tooltipPosX = 5,
      tooltipPosY = -15;
  if(d3.mouse(this)[0] > centerX) {
    // tooltipの大きさ分、左側にx座標をずらす
    tooltipPosX = -tooltip.node().getBoundingClientRect().width;
  }
  tooltip.transition()
        .duration(200)
        .ease(d3.easeLinear)
        .style("left", (d3.event.pageX + tooltipPosX) + "px")
        .style("top", (d3.event.pageY - tooltipPosY) + "px");
  focus.attr("transform", "translate(" + parseInt(x(d.day) + x.bandwidth()/2) + "," + 0 + ")");
  focus.select(".x-hover-line").attr("y2", cHeight - margin.top);

  // 棒グラフのfocus(一旦focusクラスを全て外す)
  g.selectAll(".bar").attr("class",function(d) {
    return "bar " + formatClass(d.day);
  })
  // 棒グラフのfocus(選択したものにfocusクラスをあてる)
  g.select("."+formatClass(d.day)).attr("class",function(d) {
    return "bar " + formatClass(d.day) + " focus";
  })
}

最後に

今回はD3.jsのv5で動きのある棒グラフを作成しました。
このコードを少し書き換えればツールチップ付きの折れ線グラフなども作れます。
ツールチップ付きの折れ線グラフのサンプル

値を入れるだけでサクッと作れるわけではないため少し苦労しますが、
ブラウザ上で何かグラフなどの表現したい場合には頼りになりますね。