d3.jsでobjectの上に格子状の枠を作成する

# 目的
完成イメージのように、ステータスを表示するダッシュボード画面上に四角いオブジェクトがあり、格子状の枠を作って色を塗りつぶしたり、テキストを入れるのがゴールです。使っているのはJavaScriptのライブラリd3.jsです。

ポイントは2つ。
- foreignObject[^1]要素で、オブジェクト内部にtableを追加し、格子状の枠を作る。
- clipPath[^2]要素で、角を丸くする。

初めは座標位置を指定してpathやareaで力技で描画することを考えたのですが、テキストのセンタリングやオブジェクトの拡大・縮小で破綻するので止めて、Webで色々調べつつまとめてみました。

本記事は、d3.jsでコードを書いたことがある人を前提に書いています。

# 完成イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556119/0d7f8aee-e15f-6e4d-0f3b-b97a2f778bdc.png)

## 完成版HTML
最初に完成形を貼っておきます。

```html:grid.html
<html>
  <head>
    <script src="https://d3js.org/d3.v6.min.js"></script>
    <style>
/* おまじない */
table {
  border-collapse: collapse;
  border-spacing: 0;
}
    </style>
  </head>
  <body>
    <div id="svg-area"></div>
    <script>
'use strict';
let data = [
  {t:'RED',c:'#ff0000'},
  {t:'GREEN',c:'#00ff00'},
  {t:'BLUE',c:'#0000ff'}
];
let width = 1000;
let height = 500;
let gridSize = 300;
let gap = 8;
let svg = d3.select("#svg-area").append("svg")
  .attr("width",width)
  .attr("height",height);

/* 角Rをつけた四角い図形でくり抜く */
svg.append("clipPath")
  .attr("id","clip")
  .selectAll("rect")
  .data(data)
  .enter()
  .append("rect")
  .attr("width", gridSize-gap)
  .attr("height", gridSize)
  .attr("x", (d,i) => i*gridSize)
  .attr("y", 0)
  .attr("rx", 16)
  .attr("ry", 16);

let g = svg.append("g")
  .attr("width",width)
  .attr("height",height);

let cards = g.selectAll("rect").data(data);

/* ベースとなる四角 */
cards.enter().append("rect")
  .attr("id",(d,i) => "card-"+i)
  .attr("x", (d,i) => i*gridSize)
  .attr("y", 0)
  .attr("width", gridSize-gap)
  .attr("height", gridSize)
  .attr("clip-path", "url(#clip)")
  .style("fill", "#000000");

/* tableタグのためのforeignObject要素 */
let table = cards.enter().append("foreignObject")
  .attr("x", (d,i) => i*gridSize)
  .attr("y", 0)
  .attr("width", gridSize-gap)
  .attr("height",gridSize)
  .attr("clip-path", "url(#clip)")
  .append("xhtml:table")
  .attr("width", gridSize-gap)
  .attr("height",gridSize)
  .attr("border",1)
  .attr("frame","void")
  .attr("bordercolor","#ffffff");

/* 通常のtableタグと同様に表の内部を作る */
let tr1 = table.append("tr");
let tr2 = table.append("tr");
let tr3 = table.append("tr");
tr1.append("td")
  .attr("height","33%")
  .attr("width","50%")
  .attr("bgcolor", (d) => d.c);
tr1.append("td")
  .attr("height","33%")
  .attr("width","50%")
  .style("text-align", "center")
  .append("font")
  .attr("color", "#ffffff")
  .text((d) => d.t);

tr2.append("td")
  .attr("height","33%");

tr2.append("td")
  .attr("height","33%");

tr3.append("td")
  .attr("colspan",2);
    </script>
  </body>
</html>
```

# ヘッダ

細かく見ていきます。

ヘッダはシンプルにd3.jsライブラリを読み込むだけです。なお、styleタグの記述がないと枠にスペースが出来てしまいます。[^3]

```html
<html>
  <head>
    <script src="https://d3js.org/d3.v6.min.js"></script>
    <style>
/* おまじない */
table {
  border-collapse: collapse;
  border-spacing: 0;
}
    </style>
  </head>
```
ないとこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556119/70333540-d4fe-9ae8-a64c-4931ea6f76d7.png)
# ボディ
こちらもシンプルで、div要素のみ。

```html
  <body>
    <div id="svg-area"></div>
    <script>
    ...
    </script>
  </body>
```
## オブジェクトを作る
次に、オブジェクトを実際に作っていきます。まずはベースとなる四角いオブジェクトを作成します。

```javascript
'use strict';
let data = [
  {t:'RED',c:'#ff0000'},
  {t:'GREEN',c:'#00ff00'},
  {t:'BLUE',c:'#0000ff'}
];
let width = 1000;
let height = 500;
let gridSize = 300;
let gap = 8;
let svg = d3.select("#svg-area").append("svg")
  .attr("width",width)
  .attr("height",height);

let g = svg.append("g")
  .attr("width",width)
  .attr("height",height);

let cards = g.selectAll("rect").data(data);

/* ベースとなる四角 */
cards.enter().append("rect")
  .attr("id",(d,i) => "card-"+i)
  .attr("x", (d,i) => i*gridSize)
  .attr("y", 0)
  .attr("width", gridSize-gap)
  .attr("height", gridSize)
//  .attr("clip-path", "url(#clip)")
  .style("fill", "#000000");
```
ここまで以下の図形となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556119/c3cdaa09-b26c-c853-872b-3aa33807df0b.png)

## 格子状の枠を作る
内部にtableタグを作成することで枠を作ります。foreignObjectを使ってそれを重ねるイメージです。分かりやすいように以下のような2x2の表を作るとします。

```html
<table>
  <tr>
    <td></td>
    <td></td>
  </tr>
  <tr>
    <td></td>
    <td></td>
  </tr>
</table>
```
以下のようなコードになります。

```javascript
/* tableタグのためのforeignObject要素 */
let table = cards.enter().append("foreignObject")
  .attr("x", (d,i) => i*gridSize)
  .attr("y", 0)
  .attr("width", gridSize-gap)
  .attr("height",gridSize)
//  .attr("clip-path", "url(#clip)")
  .append("xhtml:table")
  .attr("width", gridSize-gap)
  .attr("height",gridSize)
  .attr("border",1)
  .attr("frame","void")
  .attr("bordercolor","#ffffff");

/* 完成版HTMLと違います */
let tr1 = table.append("tr");
let tr2 = table.append("tr");
tr1.append("td")
  .attr("height","50%")
  .attr("width","50%");
tr1.append("td")
  .attr("height","50%")
  .attr("width","50%");
tr2.append("td")
  .attr("height","50%")
  .attr("width","50%");
tr2.append("td")
  .attr("height","50%")
  .attr("width","50%");
```
するとこんな感じで格子状の枠が作れました。完成イメージにするには、3段にする、背景色を指定する、カラムを結合する、テキストを加えるなど、通常のtableと同じような作業をやります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556119/3b43f6de-b62b-fbe3-0ff0-c1e8f82c2f31.png)
## 角を丸くする
最後に角を丸くするには、clipPathを使います。描画部分を窓枠のようにマスクするイメージで、Web検索すれば色々出てきます。

以下のコードを追加し、上記解説のclip-path属性のコメントアウトを外します。

```javascript
/* 角Rをつけた四角い図形でくり抜く */
svg.append("clipPath")
  .attr("id","clip")
  .selectAll("rect")
  .data(data)
  .enter()
  .append("rect")
  .attr("width", gridSize-gap)
  .attr("height", gridSize)
  .attr("x", (d,i) => i*gridSize)
  .attr("y", 0)
  .attr("rx", 16)
  .attr("ry", 16);
```
こちらになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556119/ffee8fdd-ff31-5b6e-837e-503a1e9cf4b6.png)

## まとめ

いかがでしたでしょうか？完成イメージのような図形でも、自由度の高いd3.jsライブラリを使えば割と簡単に描くことが出来るのがいいところです。

[^1]: &lt;foreignObject&gt; - SVG: Scalable Vector Graphics | MDN (https://developer.mozilla.org/ja/docs/Web/SVG/Element/foreignObject)
[^2]: &lt;clipPath&gt; - SVG: Scalable Vector Graphics | MDN (https://developer.mozilla.org/ja/docs/Web/SVG/Element/clipPath)
[^3]: テーブルのセルの隙間をリセットするCSS (https://qiita.com/macer_fkm/items/bac56f0f863a19cfd674)