
こんにちは、CTOの奥田です。
先日、義父が急逝しました。
 普段は鳥取で生活しており年に数回しか会えなかったのですが、私は義父を実の父のように思っていたので本当に悲しかったです。
 お義父さん、どうか安らかに。
さて、サイトやアプリケーションでグラフの描画をすることがあると思います。
 最近ではグラフ描画用のライブラリも豊富になっておりますがD3.jsはグラフだけでなく様々なビジュアライゼーションが可能です。
 今回はD3.jsについて実際にラインチャートを描画しながらご説明したいと思います。
Table of contents
D3.jsとは
D3.jsとはデータを基盤として、ウェブブラウザで動的コンテンツを描画するJavaScriptライブラリです。
 データを元にチャートやグラフなどあらゆるものをビジュアライズすることが可能です。
 「Data-Driven Documents(データ駆動型ドキュメント)」の頭文字を取って「D3」と名付けられています。
インストール
まずはインストールしましょう。
 BowerやNPMでも使用可能ですが、ドキュメントに含めることでも簡単に使用することができます。
bower install d3 --save
npm install d3 --save
<script src="https://d3js.org/d3.v5.min.js"></script>
チャートの軸を描画する
それではまずはラインチャートを描画するための軸を描画してみたいと思います。
 D3.jsはjQueryライクな書き方ができます。
 d3.selcetやselectAllでElementを取得できます。
const contents = d3.select('#chart--wrapper');
const svg = contents.append("svg");また、node()で通常のNodeを取得できます。
// グラフの幅 width = contents.node().clientWidth - padding; // グラフの高さ height = contents.node().clientHeight - padding;
今回はX軸を日付、Y軸を数値で描画してみたいと思います。
 以下のようなデータを用意し配列に代入しておきます。
let dataset = [
    {
        date : "2019/1/1",
        value : 70
    },
    ...
]
日時のデータとして扱うために、d3.timeParseを使用します。
 d3.timeParseは指定した形式の日時を標準時間のフォーマットへパースします。
 d3.timeFormatは逆に標準時間を指定したフォーマットヘ変換します。
let timeparser = d3.timeParse("%Y/%m/%d");
// x軸の目盛りの表示フォーマット
let format = d3.timeFormat("%Y/%m");
// データをパースします
dataset = dataset.map(function(d){
    // 日付のデータをパース
    return  { date: timeparser(d.date), value:d.value } ;
});
それぞれの軸をg要素(グループ要素)内に配置するためg要素を追加します。
// svg要素にg要素を追加しクラスを付与しxに代入
x = svg.append("g")
.attr("class", "axis axis-x")
// svg要素にg要素を追加しクラスを付与しyに代入
y = svg.append("g")
.attr("class", "axis axis-y")
X軸は時間のスケールなのでd3.scaleTimeを使用します。
 domain()に最小値、最大値を渡すことで領域を設定できます。
 range()で表示する範囲を指定します。
// x軸の目盛りの量
let xTicks = (window.innerWidth < 768) ? 6: 12;
// X軸を時間のスケールに設定する
xScale = d3.scaleTime()
// 最小値と最大値を指定しX軸の領域を設定する
.domain([
    // データ内の日付の最小値を取得
    d3.min(dataset, function(d){return d.date;}),
    // データ内の日付の最大値を取得
    d3.max(dataset, function(d){return d.date;})
])
// SVG内でのX軸の位置の開始位置と終了位置を指定しX軸の幅を設定する
.range([padding, width]);
Y軸は値のスケールなのでd3.scaleLinearを使用します。
// Y軸を値のスケールに設定する
yScale = d3.scaleLinear()
// 最小値と最大値を指定しX軸の領域を設定する
.domain([
    // 0を最小値として設定
    0,
    // データ内のvalueの最大値を取得
    d3.max(dataset, function(d){return d.value;})
])
// SVG内でのY軸の位置の開始位置と終了位置を指定しY軸の幅を設定する
.range([height, padding]);
目盛りの設定をします。ticks()で目盛りの数、tickFormat()で目盛りの表示フォーマットを設定することができます。
// scaleをセットしてX軸を作成 axisx = d3.axisBottom(xScale) // グラフの目盛りの数を設定 .ticks(xTicks) // 目盛りの表示フォーマットを設定 .tickFormat(format); // scaleをセットしてY軸を作成 axisy = d3.axisLeft(yScale);
最後にg要素でcall()すれば軸が描画されます。
// X軸の位置を指定し軸をセット
x.attr("transform", "translate(" + 0 + "," + (height) + ")")  
.call(axisx); 
// Y軸の位置を指定し軸をセット
y.attr("transform", "translate(" + padding + "," + 0 + ")")
.call(axisy)
線を描画する
次に線を描画してみます。まずは線の色を定義します。
 d3.rgb()を使用すれば.brighter()やdarker()で指定した色を明るくしたり暗くしたりすることができます。
 ※opacityを操作する際はcolor.opacity = 0.5というふうにするのですが元の色自体も変わってしまうことに注意してください。
let color = d3.rgb("#85a7cc");線を描画するためのpathを追加します。
// パス要素を追加
path = svg.append("path");lineを生成します。d3.lineのx()とy()に関数を指定することでデータをフィルタリングしてくれます。
//lineを生成
line = d3.line()
// lineのX軸をセット
.x(function(d) { return xScale(d.date); })
// lineのY軸をセット
.y(function(d) { return yScale(d.value); })
pathにdatumでデータをセットし、d属性にlineを渡すことで線が描画できます。
path
// dataをセット
.datum(dataset)
// 塗りつぶしをなしに
.attr("fill", "none")
// strokeカラーを設定
.attr("stroke", color)
// d属性を設定
.attr("d", line)また、d3.line.curve()を指定することで線を曲線にすることができます。
line.curve(d3.curveCatmullRom.alpha(0.4))
曲線の種類については下記サイトを参照してください。
エリアを描画する
ラインだけでなくエリアを追加し、グラデーションを描画してみます。
 pathとdefs要素を追加し、defs内にlinearGradient要素を追加します。
lineArea = svg.append("path")
g = svg.append("g");
linearGradient = svg.append("defs")
.append("linearGradient")
.attr("id", "linear-gradient")
.attr("gradientTransform", "rotate(90)");
linearGradient.append("stop")
    .attr("offset", "0%")
    .attr("stop-color",color.brighter(1.5));
linearGradient.append("stop")
    .attr("offset", "100%")
    .attr("stop-color","rgba(255,255,255,0)");
エリアの描画にはd3.areaを使用します。
 y1()とy0()があるのでyScale(0)でY軸の0地点からデータまでのエリアを指定します。
area = d3.area()
.x(function(d) { return xScale(d.date); })
.y1(function(d) { return yScale(d.value); })
.y0(yScale(0))
// カーブを設定
.curve(d3.curveCatmullRom.alpha(0.4))
後はデータを指定し、d属性に指定するだけです。
 また、fillにurl(#linear-gradient)を指定することでグラデーションになります。
lineArea
.datum(dataset)
.attr("d",area)
.style("fill", "url(#linear-gradient)")
アニメーションさせる
パスに沿ってアニメーションさせる場合はpath.node().getTotalLength()で長さを取得し、アニメーション前を設定、transition()を使用してアニメーション後の値を指定することでアニメーションさせることができます。
let totalLength = path.node().getTotalLength();
path
.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(1000)
.ease(d3.easeCircleInOut)
.attr("stroke-dashoffset", 0);
lineArea.style("opacity",0)
.transition()
.delay(500)
.duration(300)
.ease(d3.easeCircleOut)
.style("opacity",1);データに沿ってアニメーションさせる
d属性をアニメーションさせるにはinterpolatePath()プラグインが必要なので下記より取得します。
attrTween()にd3.interpolatePath()を指定し、変化前と変化後のd属性を渡すことでアニメーションさせることができます。
let dLine0 = path.attr("d");
let dArea0 = lineArea.attr("d");
// データをセットする
dataset = datasets[dataSelect.value];
path
.attr("d", dLine0)
.transition()
.duration(1500)
.ease(d3.easeExpInOut)
.attrTween('d', function () { 
    return d3.interpolatePath(dLine0, dLine1(dataset)); 
});
lineArea.attr("d", dArea0)
.transition()
.delay(50)
.duration(1500)
.ease(d3.easeExpInOut)
.attrTween('d', function () { 
    return d3.interpolatePath(dArea0, dArea1(dataset)); 
});ツールチップを表示する
ツールチップはCSSで装飾し、HTMLで表示します。
 今回はツールチップだけでなく、フォーカスしたポイントとY軸の表示もしてみようと思います。
 ツールチップのスタイルは以下のようにしました。
<style>
    .chart--tooltip {
        position: absolute;
        width: auto;
        height: auto;
        padding: 10px 15px;
        border-radius: 4px;
        background: rgba(255,255,255,.95);
        box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
        visibility: hidden;
        font-size: 12px;
        font-weight: bold;
        max-width: 160px;
    }
    </style>
bodyにツールチップの要素を追加し、取得します。
 d3.bisector()は値の比較をする関数です。日付の比較用に定義しておきます。
// tooltipを追加
const tooltip = d3.select("body").append("div").attr("class", "chart--tooltip");
const bisectDate = d3.bisector(function(d) { return d.date; }).left;
それぞれ必要な要素を追加します。
// フォーカス要素のグループを追加
focus = svg.append("g")
.attr("class", "focus")
.style("visibility","hidden")
// フォーカス時のY軸を追加
focusLine = focus.append("line");
// フォーカス時のポイントを追加
focusPoint = focus.append("circle")
.attr("r", 4)
.attr("fill", "#fff")
.attr("stroke", color)
.attr("stroke-width",2)
// オーバーレイ要素を追加
overlay = svg.append("rect");overlay要素はマウスイベントをバインドするためのものなのでpointer-eventsをallにしています。
// オーバーレイ要素を設定
overlay
.style("fill", "none")
.style("pointer-events", "all")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
// フォーカスした際のY軸を設定
focusLine
.style("stroke","#ccc")
.style("stroke-width","1px")
.style("stroke-dasharray","2")
.attr("class", "x-hover-line hover-line")
.attr("y1", padding)
.attr("y2",  height)マウスイベントを定義します。
 xScale.invert(d3.mouse(this)[0])でポインタ位置のXの値を取得できます。
 bisectDateでその値がdatasetの配列に入った際にどのindexになるかを返します。
 そこからより近いindexを比較し、データを取得しています。
 あとはツールチップにデータを入れ、表示するだけです。
handleMouseMove : function(){
    if(!isAnimate){
        let x0 = xScale.invert(d3.mouse(this)[0]),
        i = bisectDate(dataset, x0, 1),
        d0 = dataset[i - 1],
        d1 = dataset[i],
        d = x0 - d0.date > d1.date - x0 ? d1 : d0;
        let format = d3.timeFormat("%Y/%m/%d");
        let tooltipY = (d3.event.pageY - 40);
        let tooltipX = (d3.event.pageX + 20);
        if(( window.innerWidth - 160 ) < tooltipX){
            tooltipX = (d3.event.pageX - 200);
        }
        tooltip
        .html("")
        .style("visibility", "visible")
        .style("top", tooltipY + "px")
        .style("left", tooltipX + "px")
        
        tooltip
        .append("div")
        .attr("class","tooltip--time")
        .html(format(d.date) +'
' + d.value + '%')
        focus
        .style("visibility","visible")
        .attr("transform", "translate(" + xScale(d.date) + "," + 0 + ")");
        focusPoint.attr("transform", "translate(" + 0 + "," + yScale(d.value) + ")")
    }
},
handleMouseOut : function (d, i) {
    tooltip.style("visibility", "hidden");
    focus.style("visibility","hidden");
},最後にon("mousemove")とon("mouseout")にバインドすることでツールチップが表示されます。
overlay
.on("mousemove",this.handleMouseMove)
.on("mouseout",this.handleMouseOut)
さいごに
いかがだったでしょうか?
 D3.jsは学習コストはかなり高いですが柔軟にビジュアライゼーションが可能です。
 グラフの描画だけでなく様々な用途に使用できそうですね。
 皆さんの参考になれば幸いです。