Blog スタッフブログ

D3.jsでブラシを使ってグラフをズームさせる

Category | Blog
Tag | /
/ 6,661views

こんにちは、CTOの奥田です。
あっという間に年の瀬となり、もう2020年が終わろうとしていますね。
この記事を書いているのは2020年の年末なのですが、今年の年末年始は例年とは少し違ったものになりそうですね。
私も帰省はせず、自宅で過ごすこととなりそうです。

さて、今回は久しぶりにD3.jsの記事です。
構築したグラフが複雑になってくるとズーム機能が欲しくなってくると思います。
D3.jsにはBrushという機能があり、意外と簡単にズームさせることが出来ます。
今回はBrush機能を使ったズーム機能の実装方法についてご説明できればと思います。

Table of contents

  1. チャートを描画する
  2. ブラシを追加する
  3. ズームする
  4. ズームをリセットする
  5. 「戻る」と「進む」を実装する
  6. さいごに

チャートを描画する

まずはチャートを描画します。
チャートを描画するまでの書き方は前回までのブログを参照ください。
D3.jsで折れ線グラフ(Line Chart)を描画してみる

今回のチャートではデータの形式を少し変えています。
それぞれのデータがvaluesに入っているような構成です。

chart.dataset = [
    {
        name: "Data A",
        color:"#007bff",
        values:[...]
    },{
        name: "Data B",
        color:"#2ad4ac",
        values:[...]
    },{
        name: "Data C",
        color:"#ffc107",
        values:[...]
}]

X軸の時間は同じようにtimesに入れています。

chart.times = {
    name: "date",
    values:[...]
}

また、ZoomしているのがわかりやすいようにGridや凡例を表示しています。
併せて参考にしてみてください。

ブラシを追加する

次にブラシを追加します。d3.brush()でXYの両方を範囲指定できるブラシを生成します。
d3.brushX()やd3.brushY()とすればどちらかのみを範囲指定できます。

this.brush = d3.brush()
this.brushGroup = this.main.append("g")
.attr("class", "x brush")

extent()でブラシの有効範囲を指定します。
こちらは画面サイズが変わった際にリサイズが効くようにupdate()時に実行するようにします。
.on(“end”, () => {})でブラシで選択が終わった後にコールバックを呼び出せます。
他にも開始時の”start”やドラッグ中も実行される”brush”などがあります。

this.brush
.extent([
    [this.margin.left, this.margin.top],
    [this.width - this.margin.right , this.height - this.margin.bottom]
])
.on("end", () => {
    this.brushed()
});
this.brushGroup.call(this.brush)

コールバック関数内で d3.event.selection に選択範囲が入ります。
d3.event.selection[[x0,y1],[x1,y0]]このような形で値が入るのでそれぞれをxScale、yScaleで取得します。

brushed() {
    const s = d3.event.selection;
    
    if(s){
        const x0 = [this.xScale.invert(s[0][0]),this.xScale.invert(s[1][0])]
        const y0 = [this.yScale.invert(s[1][1]),this.yScale.invert(s[0][1])]
    }
}

下記DEMOで範囲選択をしてみてください。
チャートの下部に値が出力されます。

ズームする

取得したxのmin,maxとyのmin,maxの値をそれぞれdomainに設定し、ブラシを非表示にします。

this.xScale.domain(x0);
this.yScale.domain(y0);
// ブラシを非表示に
this.svg.select(".brush").call(this.brush.move, null);
// zoomを実行
this.zoom();

そしてzoomの処理をtransitionを効かせて実行します。

zoom(){
    // トランジションを指定
    const t = this.svg.transition().duration(300);

    this.x.transition(t).call(this.axisx)
    this.y.transition(t).call(this.axisy)

    this.gridY.transition(t).call(this.gridAxisy)
    this.gridX.transition(t).call(this.gridAxisx)

    this.paths.forEach((path,i) =>{
        
        path
        .datum(this.dataset[i].values)
        .attr("d", this.lines[i]);
        
    })
    this.zoomed = true
}

ズームをリセットする

現状だとズームした後戻ることが出来ないのでリセットを実装したいと思います。
今回はチャート全体をダブルクリックした際にズームをリセットする処理を実装します。

handleDblClick(){
    this.main.on("dblclick",() => {
        if(this.zoomed){
            this.zoomClear()
        }
    })
}

生成時にメソッドを実行します。

this.handleDblClick()

zoomリセットの処理は初期値this.x0とthis.y0を入れてトランジションをセットし、ズームの時と同じ処理を行うだけです。

zoomClear() {
    this.xScale.domain(this.x0)
    this.yScale.domain(this.y0).nice();

    const t = this.svg.transition().duration(300);
    this.x.transition(t).call(this.axisx)
    this.y.transition(t).call(this.axisy)
    
    this.gridY.transition(t).call(this.gridAxisy)
    this.gridX.transition(t).call(this.gridAxisx)
    

    this.paths.forEach((path,i) => {
        path
        .datum(this.dataset[i].values)
        .attr("d", this.lines[i]);
    })
    
    this.zoomed = false
}    

下記DEMOでズームさせた後、チャート部分をダブルクリックしてみてください。
ズームがリセットされていれば成功です。

「戻る」と「進む」を実装する

最後にズームした際に「戻る」と「進む」を実装してみたいと思います。
実装のイメージはズームするごとにzoomHistoryにXとYの値を記憶しておき、現在の位置をnowPositionに記憶しながら「戻る」と「進む」の処理を行うというイメージです。

まず「戻る」と「進む」のボタンを生成します。

this.prevButton = document.createElement("button")
this.prevButton.setAttribute("class","js-chart__zoom--ctrls js-chart__zoom--prev")
this.el.appendChild(this.prevButton)

this.prevButton
.addEventListener("click",() => {
    this.zoomPrev()
},false)

this.nextButton = document.createElement("button")
this.nextButton.setAttribute("class","js-chart__zoom--ctrls js-chart__zoom--next")
this.el.appendChild(this.nextButton)

this.nextButton
.addEventListener("click",() => {
    this.zoomNext()
},false)

updateの際に初期値を入れます。またボタンはdisabledにします。

this.zoomHistory = [[this.x0,this.y0]]
this.nowPosition = 1
this.prevButton.setAttribute("disabled","disabled")
this.nextButton.setAttribute("disabled","disabled")

次にそれぞれを押した際の処理を書きます。
zoomTransitionはボタンを連打した際にtransitionを効かせている箇所で誤作動を起こさないためのものです。

zoomPrev() {
    if(this.zoomTransition){
        return false
    }
    if(this.nowPosition > 1){
        if(this.nowPosition <= this.zoomHistory.length){
            this.nowPosition--
            const x0 = this.zoomHistory[this.nowPosition-1][0]
            const y0 = this.zoomHistory[this.nowPosition-1][1]
            this.xScale.domain(x0);
            this.yScale.domain(y0).nice();
            this.prevButton.setAttribute("disabled","disabled")
            this.nextButton.setAttribute("disabled","disabled")

            this.zoom();
            if(this.nowPosition == 1){
                this.zoomed = false
            }
        }
    }
}
zoomNext(){ 
    if(this.zoomTransition){ 
        return false 
    }
    if(this.nowPosition > 0){
        if(this.nowPosition < this.zoomHistory.length){
            this.nowPosition++
            const x0 = this.zoomHistory[this.nowPosition-1][0]
            const y0 = this.zoomHistory[this.nowPosition-1][1]
            this.xScale.domain(x0);
            this.yScale.domain(y0).nice();
            this.prevButton.setAttribute("disabled","disabled")
            this.nextButton.setAttribute("disabled","disabled")
            this.zoom();
        }
    }
}

次にbrushedメソッドを以下のように修正します。

brushed() {
    const s = d3.event.selection;
    
    if(s){
        
        const x0 = [this.xScale.invert(s[0][0]),this.xScale.invert(s[1][0])]
        const y0 = [this.yScale.invert(s[1][1]),this.yScale.invert(s[0][1])]
        this.xScale.domain(x0);
        this.yScale.domain(y0);
        this.svg.select(".brush").call(this.brush.move, null);
        
        if(this.nowPosition != this.zoomHistory.length){
            const l = this.zoomHistory.length - this.nowPosition
            if(l > 0){
                for (let i = 0; i < l; i++) {
                    this.zoomHistory.pop()
                }
            }
        }
        this.nowPosition ++
        this.zoomHistory.push([x0,y0])

        this.zoom();
    }
}

zoomメソッドを以下のように書き換えます。
.on(“end”,() => {this.zoomTransition = false})でtransitionが終了してからthis.zoomTransitionをfalseにしています。

zoom(){
    this.zoomTransition = true
    const t = this.svg.transition().duration(300);

    this.x.transition(t).call(this.axisx)
    .on("end",() => {
        this.zoomTransition = false
    })
    this.y.transition(t).call(this.axisy)

    this.gridY.transition(t).call(this.gridAxisy)
    this.gridX.transition(t).call(this.gridAxisx)

    this.paths.forEach((path,i) =>{
        
        path
        .datum(this.dataset[i].values)
        .attr("d", this.lines[i]);
        
    })
    this.zoomed = true

    if(this.nowPosition == 1){
        this.prevButton.setAttribute("disabled","disabled")
    }else{
        this.prevButton.removeAttribute("disabled")
    }

    if(this.nowPosition > 0){
        this.nextButton.removeAttribute("disabled")
    }
    if(this.nowPosition == this.zoomHistory.length){
        this.nextButton.setAttribute("disabled","disabled")
    }

}

これで「戻る」と「進む」の処理の実装が出来ました。

さいごに

いかがでしたか?D3.jsはブラシを使えば意外と簡単にズームの処理を実装できます。
他にもマウスホイールでズームさせてマウスで移動させたりなど様々な機能があります。
皆様も是非活用してみてください。

Category | Blog
Tag | /
Author | Mineo Okuda / 6,661views

Company information

〒650-0024
神戸市中央区海岸通5 商船三井ビルディング4F

Contact us

WEBに関するお問い合わせは
078-977-8760 (10:00 - 18:00)