JavaScript
d3
DataVisualization

d3js v4 子要素のデータバインド

はじめに

d3js Version 4.12.2
v4になって変更点がありデバッグ用にリアルタイムでテーブルを書こうとして
数時間はまりました。覚書です。

シンプルな子要素の例

tableのtbodyをデータからリアルタイムに更新する。
データから配列の数だけtrを作って、propertyの数だけtdを作る。
dataはtrにバインドするけど中のtdのテキストを変える。

<tbody>
    <tr>
        <td class="property1">value1</td>
        <td class="property2">value2</td>
        <td class="property3">value3</td>
    </tr>
    <tr>
        <td class="property1">value4</td>
        <td class="property2">value5</td>
        <td class="property3">value6</td>
    </tr>
    <tr>
        <td class="property1">value7</td>
        <td class="property2">value8</td>
        <td class="property3">value9</td>
    </tr>
</tbody>

dataからこのようなHTMLをd3jsで描画する。
trの数、valueはどんどん入れ替わるがpropertyの数は変わらない。

data (json)

[{
  property1 : value1,
  property2 : value2,
  property3 : value3
},{
  property1 : value4,
  property2 : value5,
  property3 : value6
},{
  property1 : value7,
  property2 : value8,
  property3 : value9
}]

pug(HTML)

table
    thead
        tr
            th.property1 property1
            th.property2 property2
            th.property3 property3
    tbody

js

// trにデータを入れていく
var trAll = d3.select("tbody")
    .selectAll("tr")
    .data(data);
// 配列がなくなってたらtrを消す
trAll.exit().remove();

// 配列が増えてたらtrを追加してtdを入れておく
var trEnter = trAll.enter()
    .append("tr");
trEnter.append("td")
    .attr('class','property1');
trEnter.append("td")
    .attr('class','property2');
trEnter.append("td")
    .attr('class','property3');

// 配列が増えた時と新しいデータが来たときtdの中身を変える
trEnter = trEnter.merge(trAll);
trEnter.select("td.property1")
    .text(function(d){
        return d.property1;
    });
trEnter.select("td.property2")
    .text(function(d){
        return d.property2;
    });
trEnter.select("td.property3")
    .text(function(d){
        return d.property3;
    });

たったこれだけ。
jsは新しいデータがくるたびにまるごと呼びなおして大丈夫。

はまったとこ

tdの中身を変える際にtdをselectAllで選択するとdataが更新されなくなる。
最初の1回(たぶんenter時)はちゃんと動く。

js (ダメな例)

// trにデータを入れていく
var trAll = d3.select("tbody")
    .selectAll("tr")
    .data(data);
// 配列がなくなってたらtrを消す
trAll.exit().remove();

// 配列が増えてたらtrを追加してtdを入れておく
var trEnter = trAll.enter()
    .append("tr");
trEnter.append("td")
    .attr('class','property1');
trEnter.append("td")
    .attr('class','property2');
trEnter.append("td")
    .attr('class','property3');

// 配列が増えた時と新しいデータが来たときtdの中身を変える
trEnter = trEnter.merge(trAll);

//////////////////////////
////// ↓これはだめ //////
//////////////////////////
trEnter.selectAll("td")  // <<< これはだめらしい。
    .text(function(d,index){
        // 更新のたび呼ばれるけどdが最初のデータのまま
        return d["property" + (index+1)];
    });

数時間調べたけどよくわからなかった。
selectAllの後にdataを見ても更新されているが、text中では更新されなくなる。
method名から考えるとバグっぽいのでこの問題はなくなるかもです。

子要素もさらに配列になっている例

tableのtbodyをデータからリアルタイムに更新する。
データから配列の数だけtrを作って、propertyの数だけtdを作る。
dataはtrにバインドするけど中のtd.property1のテキストを変え
さらにtd.property2の中に配列の数だけspanを作る。

<tbody>
    <tr>
        <td class="property1">name1</td>
        <td class="property2">
            <span>value1</span>
            <span>value2</span>
            <span>value3</span>
        </td>
    </tr>
    <tr>
        <td class="property1">name2</td>
        <td class="property2">
            <span>value4</span>
            <span>value5</span>
        </td>
    </tr>
    <tr>
        <td class="property1">name3</td>
        <td class="property2">
            <span>value6</span>
            <span>value7</span>
            <span>value8</span>
            <span>value9</span>
        </td>
    </tr>
</tbody>

dataからこのようなHTMLをd3jsで描画する。
trの数、valueの数と内容はどんどん入れ替わるがpropertyの数は変わらない。

data (json)

[{
  property1 : name1,
  property2 : [value1,value2,value3]
},{
  property1 : name2,
  property2 : [value4,value5]
},{
  property1 : name3,
  property2 : [value6,value7,value8,value9]
}]

pug(HTML)

table
    thead
        tr
            th.property1 property1
            th.property2 property2
    tbody

js

var trAll = d3.select("tbody")
    .selectAll("tr")
    .data(data);
trAll.exit().remove();

var trEnter = trAll.enter()
    .append("tr");
trEnter.append("td")
    .attr('class','property1');
trEnter.append("td")
    .attr('class','property2');

trEnter = trEnter.merge(trAll);
trEnter.select("td.property1")
    .text(function(d){
        return d.property1;
    });

var spanAll = trEnter.select("td.property2")
    .selectAll("span")
    .data(function(d){
        return d.property2;
    });
spanAll.exit().remove();
var spanEnter = spanAll.enter()
    .append('span');
spanEnter = spanEnter.merge(spanAll);
spanEnter.text(function(d){
        return d;
    });

jsは新しいデータがくるたびにまるごと呼びなおして大丈夫。

さいご

かなりのデータ量と秒間何回も更新されるデータをコンソールでデバッグするのにつかれたので、htmlで見るようにするときはまりました。

d3jsは同じような感じでsvgも書けます。
かなり昔に作った地震のモニタ
ezgif-1-9b436b33fd.gif
htmlですが地図の部分は地形も含めてd3のsvgで書いてます。
data drivenってすごい。