便利な layer 関数を実装する
SVG の g
要素は、描画要素の表示順序を制御するためのレイヤーとして使われることがあります(参考: SVG の g 要素の使い方)。
D3.js でレイヤー構造を作るのは簡単で、次のように任意のセレクションオブジェクトの append()
メソッドで g
要素を追加するだけです。
function draw(data) {
const svg = d3.select("#mysvg");
const layer1 = svg.append("g"); // 奥に表示するレイヤー
const layer2 = svg.append("g"); // 手前に表示するレイヤー
// layer1 への描画
layer1.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", () => Math.random() * 170 + 15)
.attr("cy", () => Math.random() * 40 + 15)
.attr("r", 10)
.attr("fill", "#00f9")
// layer2 への描画(省略)
}
注意しなければいけないのは、上記のように直接 append()
メソッドを呼び出す場合は、このコードが 1 度だけ実行されるようにしておくことです。
D3.js の data
メソッドによるデータ結合の仕組み を使っていると、データ配列に変更があったときに、enter()
や append()
を組み合わせたコードを繰り返し呼び出すことになります。
このとき、上記のように単純に append()
している部分まで呼び出してしまうと、g
要素がどんどん増えていってしまいます。
draw(d3.range(3)); // 1 度目の描画でレイヤー用の g 要素を作成する
draw(d3.range(3)); // データ変更後の再描画で余計な g 要素が作成されてしまう
次のユーティリティ関数 layer()
は、このようなミスを防ぐために、レイヤー用の g
要素がまだ追加されていない場合のみ append()
を実行するようにしています。
/**
* レイヤーを新たに作成または既存のレイヤーを取得します。
* @see {@link https://maku.blog/p/298nhnq/}
*/
function layer(parent, id) {
const g = parent.select("#" + id);
return !g.empty() ? g : parent.append("g").attr("id", id);
}
// (TypeScript 対応版)
// function layer(parent: d3.Selection<d3.BaseType, unknown, d3.BaseType, unknown>, id: string) {
// const g = parent.select<SVGGElement>('#' + id);
// return !g.empty() ? g : parent.append('g').attr('id', id);
// }
前述の draw()
関数を次のように書き換えると、安心して繰り返し呼び出せるようになります。
function draw(data) {
const svg = d3.select("#mysvg");
// レイヤー用のセレクションオブジェクトを取得
const layer1 = layer(svg, "layer1");
const layer2 = layer(svg, "layer2");
// ...
}
layer 関数の使用例
下記は、このユーティリティ関数 (layer
) の具体的な使用例です。
<svg id="svg-af2nv6q" width="200" height="70"></svg>
<script>
draw(d3.range(3)); // 1回目の描画(内部でレイヤーが生成される)
draw(d3.range(3)); // 2回目の描画(再描画時はレイヤーが再利用される)
function draw(data) {
const svg = d3.select("#svg-af2nv6q");
const layer1 = layer(svg, "layer-rx5sfkn");
const layer2 = layer(svg, "layer-v73ikwo");
layer1.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", () => Math.random() * 170 + 15)
.attr("cy", () => Math.random() * 40 + 15)
.attr("r", 10)
.attr("fill", "#00f9")
layer2.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", () => Math.random() * 170 + 15)
.attr("cy", () => Math.random() * 40 + 15)
.attr("r", 10)
.attr("fill", "#f009")
}
/**
* レイヤーを新たに作成または既存のレイヤーを取得します。
* @see {@link https://maku.blog/p/298nhnq/}
*/
function layer(parent, id) {
const g = parent.select("#" + id);
return !g.empty() ? g : parent.append("g").attr("id", id);
}
</script>
さらに、次のように全レイヤーのセレクションオブジェクトをまとめて返す関数 (getLayers
) を作っておくのもいいですね。
これを使えば、draw()
関数をよりシンプルに記述できるようになるだけでなく、レイヤーの表示順序(=作成順序)に関する知識を関数内に閉じることができます。
function getLayers() {
const svg = d3.select("#svg-af2nv6q");
return {
layer1: layer(svg, "layer-rx5sfkn"),
layer2: layer(svg, "layer-v73ikwo"),
};
}
function draw(data) {
const { layer1, layer2 } = getLayers();
// ... 以下同様 ...
}
(おまけ)data([null]) を使う方法
前述の説明では、セレクションオブジェクトの empty()
メソッドを使うことで、g
要素が追加済みかどうかを調べていましたが、次のようにデータ結合 (data()
) の仕組みを利用することでも重複 append
を避けることができます。
この実装では、サイズ 1 のデータ配列 ([null]
) をデータ結合することで、それに対応する 1 つの g
要素を 1 度だけ追加するように制御しています。
このイディオムは D3.js の作者である Mike Bostock (mbostock) 氏も使っていたりしますが、コードの意味がわかりにくく気持ち悪いので、個人的には empty()
メソッドを使うコードを推奨します。