まくろぐ
更新: / 作成:

D3.js はクライアントサイド JavaScript から SVG 画像を生成するためのライブラリです。 ここでは、Web サイト生成用のフレームワークである Astro 上で、D3.js を使う方法を紹介します。 前提条件として、Astro のプロジェクトはすでに作成済みであるとします。 まだ作成していない場合は、次のように作成できます。

Astro プロジェクトの生成
$ npm create astro@latest

D3.js はコンポーネントスクリプトからは使えない

D3.js はクライアントサイド JS として動作し、DOM ツリー上の SVG 要素を動的に操作することを想定して設計されています。 一方で、Astro コンポーネントのフロントマター部分に記述するスクリプトは、サーバーサイドで(ビルド時に)実行されることを想定しています。 つまり、D3.js を使ったコードは、Astro のフロントマタースクリプトとして記述することはできません。 残念!

Chart.astro(間違った方法)
---
import * as d3 from "d3";
const svg = d3.select("#mysvg");  // ここでは DOM 要素にアクセスできない!
// ...
---

<svg id="mysvg" width="400" height="200"></svg>

D3.js を使ったコードは、次のように <script> 要素の中に記述する必要があります。 ここに記述した JavaScript コードは、クライアントサイド(Web ブラウザ上)で実行されます。

Chart.astro(正しい方法)
---
---

<svg id="mysvg" width="400" height="200"></svg>
<script>
import * as d3 from "d3";
const svg = d3.select("#mysvg");  // 正しく DOM 要素を参照できる
// ...
</script>

D3.js のインストール

まず、d3 モジュールと、TypeScript 用の型ファイルをインストールしておきます。

$ npm install d3
$ npm install --save-dev @types/d3

Astro コンポーネントの作成

下記は、D3.js を使って上のような SVG を描画する Astro コンポーネント (Chart.astro) の実装例です。 3 つの circle 要素の座標値は、別ファイルの sample.json ファイルから読み込んでいます。

src/components/Chart.astro
---
interface Props {
  x: number;
  y: number;
}
const { w, h } = Astro.props as Props;
---

<svg id="mysvg" width={w} height={h}></svg>
<script>
  import * as d3 from "d3";
  import sampleData from "../data/sample.json";

  d3.select("#mysvg")
    .style("background", "mistyrose")
    .selectAll("circle")
    .data(sampleData.points)
    .join("circle")
    .attr("cx", (d) => d[0])
    .attr("cy", (d) => d[1])
    .attr("r", 10)
    .attr("fill", "dodgerblue");
</script>
<style>
  svg {
    border: solid 1px orangered;
  }
</style>

あとは、任意のページコンポーネントから次のように Chart.astro コンポーネントを使用するだけです。

src/pages/test.astro
---
import Chart from "../components/Chart.astro";
---

<!DOCTYPE html>
<meta charset="UTF-8" />
<title>D3.js test</title>
<Chart w="400" h="200" />

できたー ٩(๑❛ᴗ❛๑)۶ わーぃ

(おまけ)外から JSON データを渡せるようにしてみる

次の Chart.astro コンポーネントは、props で外から JSON データを渡せるようにしてみたものです。 これが意外とくせものでした。

Astro の設計上、script タグの define:vars 属性経由で JSON データを橋渡ししようとすると、その JavaScript コードはインライン展開扱い (is:inline) になってしまうので、import * as d3 from "d3" のように NPM モジュールを読み込むことができなくなってしまいます。 しょうがないので、D3.js は別の script タグで読み込んでいます。 むむぅ。

src/components/Chart.astro
---
interface Props {
  x: number;
  y: number;
  data: any;
}
const { w, h, data } = Astro.props as Props;
---

<svg id="mysvg" width={w} height={h}></svg>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script define:vars={{ data }} type="module">
  d3.select("#mysvg")
    .style("background", "mistyrose")
    .selectAll("circle")
    .data(data.points)
    .join("circle")
    .attr("cx", (d) => d[0])
    .attr("cy", (d) => d[1])
    .attr("r", 10)
    .attr("fill", "dodgerblue");
</script>
src/pages/test.astro
---
import Chart from "../components/Chart.astro";
import jsonData from "../data/sample.json";
---

<!DOCTYPE html>
<meta charset="UTF-8" />
<title>D3.js test</title>
<Chart w="400" h="200" data={jsonData} />

こーゆークライアントサイドで動く JavaScript がメインになるようなサイトであれば、Svelte を使う 方がよいのかもしれません。

関連記事

まくろぐ
サイトマップまくへのメッセージ