D3.jsでグリッドを作る



``` html:grid.html
<!DOCTYPE html>
<html lang="jp">
  <head>
    <meta charset="utf-8" />
    <script src="https://d3js.org/d3.v5.js"></script>
    <script src="./js/grid.js"></script>
  </head>
  <body>
    <div id="grid"></div>
    <span id="data" style="display:none;">
    [
      [
        {"x":1,"y":1,"width":100,"height":100,"selected":false},
        {"x":101,"y":1,"width":250,"height":50,"selected":false}
      ],
      [
        {"x":101,"y":51,"width":100,"height":50,"selected":false},
        {"x":201,"y":51,"width":150,"height":50,"selected":false}
      ],
      [
        {"x":1,"y":101,"width":50,"height":300,"selected":false},
        {"x":51,"y":101,"width":50,"height":150,"selected":false},
        {"x":101,"y":101,"width":100,"height":150,"selected":false},
        {"x":201,"y":101,"width":150,"height":150,"selected":false}
      ],
      [
        {"x":51,"y":251,"width":50,"height":150,"selected":false},
        {"x":101,"y":251,"width":100,"height":150,"selected":false},
        {"x":201,"y":251,"width":150,"height":150,"selected":false}
      ]
    ]
    </span>
  </body>
</html>
```

```JavaScript:grid.js
document.addEventListener("DOMContentLoaded", function () {
    // html内のJson文字列をデシリアイズします。
    const gridData = JSON.parse(document.querySelector("#data").textContent);

    const grid = d3
        .select("#grid")
        .append("svg")
        .attr("width", "510px")
        .attr("height", "510px");

    const row = grid
    	.selectAll()
        .data(gridData)
        .enter()
        .append("g")
        .attr("class", "row");

    const noSelectedColor = "#ffffff";
    const selectedColor = "#2C93E8";
    const column = row
        .selectAll()
        .data(d) { return d; })
        .enter()
        .append("rect")
        .attr("class", "square")
        .attr("x", function (d) { return d.x; })
        .attr("y", function (d) { return d.y; })
        .attr("width", function (d) { return d.width; })
        .attr("height", function (d) { return d.height; })
        .style("fill", noSelectedColor)
        .style("stroke", "#222")
        .on('click', function (d) {
            d.selected = !d.selected;
            let cell = d3.select(this);
            if (d.selected) {
                cell.style("fill", selectedColor);
            } else {
                cell.style("fill", noSelectedColor);
            }
        });
});
```

[Let's Make a Grid with D3.js][sample]

# 解説

上から順番に解説します。

```javascript
const grid = d3
    .select("#grid")
    .append("svg")
    .attr("width", "510px")
    .attr("height", "510px");
```
```html
<!-- 実行後 -->
<div id="grid">
  <svg width="510px" height="510px">
</svg></div>
```
selectメソッドのセレクターはjsやcssなどと同じです。
取得した d3.selection オブジェクトに append メソッドで svg タグが挿入されます。append をすることでタグが実際に生成されます。append の戻り値は d3.selection オブジェクトです。
attr メソッドで属性を追加します。

```javascript
const row = grid
    .selectAll()
    .data(gridData)
    .enter()
    .append("g")
    .attr("class", "row");
```
```html
<div id="grid">
  <svg width="510px" height="510px">
    <g class="row"></g>
    <g class="row"></g>
    <g class="row"></g>
    <g class="row"></g>
  </svg>
</div>
```
data メソッドでグリッドのオブジェクトを割り当てます。サンプルでは Json 文字列からオブジェクトを生成しています。
enter メソッドで data メソッドで割り当てられたオブジェクトがd3.selectionオブジェクトにセットされます。
append("g")でオブジェクトの各配列要素に [g 要素][svg_g] をバインドし、タグを生成します。
（よくあるサンプルだと始めの selectAll で selectAll("row") などして、空の selection オブジェクトを取得するのを見かけます。ですが、引数は空でいいと思うのですが、わざわざ"row"などして空振りする効果がよくわかりません。）

[d3.jsのセレクタとタグ操作][d3_selector]
[D3.jsで\<g\>要素を活用する][d3_g]

```javascript
const noSelectedColor = "#ffffff";
const selectedColor = "#2C93E8";
const gridStrokeColor = "#222222";
const column = row
    .selectAll()
    .data(d => d)
    .enter()
    .append("rect")
    .attr("class", "square")
    .attr("x", d => d.x)
    .attr("y", d => d.y)
    .attr("width", d => d.width)
    .attr("height", d => d.height)
    .style("fill", noSelectedColor)
    .style("stroke", gridStrokeColor)
    .on('click', function (d) {
        d.selected = !d.selected;
        let cell = d3.select(this);
        if (d.selected) {
            cell.style("fill", selectedColor);
        } else {
            cell.style("fill", noSelectedColor);
        }
    });
```
```html
<div id="grid">
  <svg width="510px" height="510px">
    <g class="row">
      <rect class="square" x="1" y="1" width="100" height="100" style="fill: rgb(255, 255, 255); stroke: rgb(34, 34, 34);">
      </rect>
      <rect class="square" x="101" y="1" width="250" height="50" style="fill: rgb(255, 255, 255); stroke: rgb(34, 34, 34);">
      </rect>
    </g>
    <g class="row">
      <rect class="square" x="101" y="51" width="100" height="50" style="fill: rgb(255, 255, 255); stroke: rgb(34, 34, 34);">
      </rect>
      <rect class="square" x="201" y="51" width="150" height="50" style="fill: rgb(255, 255, 255); stroke: rgb(34, 34, 34);">
      </rect>
    </g>
    <g class="row">
      <rect class="square" x="1" y="101" width="50" height="300" style="fill: rgb(255, 255, 255); stroke: rgb(34, 34, 34);">
      </rect>
      <rect class="square" x="51" y="101" width="50" height="150" style="fill: rgb(255, 255, 255); stroke: rgb(34, 34, 34);">
      </rect>
      <rect class="square" x="101" y="101" width="100" height="150" style="fill: rgb(255, 255, 255); stroke: rgb(34, 34, 34);">
      </rect>
      <rect class="square" x="201" y="101" width="150" height="150" style="fill: rgb(255, 255, 255); stroke: rgb(34, 34, 34);">
      </rect>
    </g>
    <g class="row">
      <rect class="square" x="51" y="251" width="50" height="150" style="fill: rgb(255, 255, 255); stroke: rgb(34, 34, 34);">
      </rect>
      <rect class="square" x="101" y="251" width="100" height="150" style="fill: rgb(255, 255, 255); stroke: rgb(34, 34, 34);">
      </rect>
      <rect class="square" x="201" y="251" width="150" height="150" style="fill: rgb(255, 255, 255); stroke: rgb(34, 34, 34);">
      </rect>
    </g>
  </svg>
</div>
```
ここでは、先程 g 要素にバインドした配列の行オブジェクトの要素から rect を生成します。x や y、width、height、fill、stroke は SVG の属性です。
jQuery のように、on メソッドでイベントを付与できます。ここでは、クリックすると背景色を切り替えるイベントを付与しています。
（ただ、ここでもう一度 data(d => d).enter() しないといけないのがよくわからない。）

# 完成形

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29324/dfc30f18-db74-706d-79b7-c061846b7abd.png)

[sample]:https://bl.ocks.org/cagrimmett/07f8c8daea00946b9e704e3efcbd5739
[d3_selector]:https://qiita.com/nobuyuki-ishii/items/44d324735ed3b10349df
[d3_g]:https://qiita.com/cieloazul310/items/d2d90318c9c2a5c62703
[svg_g]:https://developer.mozilla.org/ja/docs/Web/SVG/Element/g