この記事は「株式会社オープンストリーム Advent Calendar 2019」の12日目の記事です。
どうも、データから図を作るときにD3.jsを試している @ysd_marrrr です。
D3.jsの導入方法を見るとファイルを配置する以外にもこちらのHTMLタグを挿入するだけでD3.jsを使うことができます。
<script src="https://d3js.org/d3.v5.min.js"></script>
しかし、「zipファイルを展開して配置するのが面倒くさい」「パッケージで管理できないか」と考えてしまいますよね? なんと npmにD3.jsのパッケージがあるんです
そこで、npmパッケージになっているD3.jsを利用するためにWebpackを導入する記事になります。
パッケージ一つだけであればBrowserfyとかparcelとかより簡単そうなものはありますが
開発環境
macOSで動作確認をしています。
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.1
BuildVersion: 19B88
$ node -v
v13.2.0
$ npm -v
6.13.1
$ yarn -v
1.21.0
Windowsの場合は 後述する「Webpackのモード切替」がうまくいかないため調査中です。 余計なことしなければいいのに
Webpackのモードを固定にすると正しく動作します。
プロジェクトの設定: npm init
npm
も package.json
も何も使っていない状態で始めようとするときは、新しくディレクトリを作って npm init
を実行します。
いきなりD3.jsのパッケージを npm
で取得しようとすると 予測できない場所に保存されます(実体験)
npm init
途中で entry point: index.js
と質問されますが、主にnpmのモジュールとして公開したときに呼び出される設定です。
今回は作ったものをnpmのパッケージとして公開する予定はないため 何も変更せず 次に進めます。
Webpackの導入
npm
でインストールするD3.jsのパッケージを読み込めるようにWebpackをインストールします。また、動作確認用にWebサーバーがほしいので webpack-dev-server
も導入します。
npm install -D webpack webpack-cli webpack-dev-server
babel-loaderの導入
D3.jsそのものではなく「D3.jsを使って図を描くスクリプト」でECMAScript6を使うため、 babel-loader
を導入します。
npm install -D babel-core babel-loader@7 babel-preset-env
もちろんD3.jsの導入
そして、今回の主役であるD3.jsのnpmパッケージをインストールします。
npm install d3
webpack.config.js
少し長いですが、主に次の4つを設定しています
- 「D3.jsを使って図を描くスクリプト」を指定する
entry
- 「D3.jsそのもの」「D3.jsを使って図を描くスクリプト」を一つにまとめて変換したJavaScriptファイルの出力先を指定する
output
- 開発用にWebサーバーを用意するための
devServer
- 「D3.jsを使って図を描くスクリプト」を変換するための
balel-loader
のルール(D3.js
がインストールされているnode_modules
が除外されていますね?)
const path = require("path");
module.exports = {
mode: development,
entry: "./src/main.js",
devtool: "source-map",
output: {
path: path.join(__dirname, "public", "js"),
filename: "app.js",
publicPath: "/js/"
},
devServer: {
open: true,
openPage: "index.html",
contentBase: path.join(__dirname, "public"),
watchContentBase: true,
port: 3010
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["babel-preset-env"]
}
}
}
]
}
};
この設定で、ディレクトリ構成は次の図のようになります。
-
npm
で導入したD3.jsパッケージが含まれるnode_modules
と - 「D3.jsを使って図を描くスクリプト」が入っている
./src/app.js
を - 予め用意した公開用ディレクトリ
./public
の配下にある./public/js
にまとめて - 予め用意した
./public/index.html
で呼び出して図を描画する
D3.jsを使って図を描くページ: ./public/index.html
HTMLにはD3.jsで描画された図を出すためのボックスのみを用意します。
今回は npm
でD3.jsを使えるようにするため、スタイルシートは簡単にHTML内に入れています。
Webpackには外部のCSSファイルを下のようにHTML内に埋め込んでくれるプラグインもあります!
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>D3.js Sample with npm</title>
<style type="text/css">
#drawarea {
box-shadow: 5px 5px 15px 5px black;
}
.node text {
font: 16px helvetica;
}
</style>
</head>
<body>
<h1>D3.js Sample</h1>
<p>この下のボックスにD3.jsで描画されたものが来ます!</p>
<svg id="drawarea" width="400" height="400"></svg>
<hr />
<script src="/js/app.js"></script>
</body>
</html>
WebpackでまとめられたJavaScriptのファイルは同じ ./public
にある index.html
から見てのパスで指定します。
D3.jsを使って図を描く
https://wizardace.com/d3-forcesimulation-simple/ をベースに、ノード(点)にテキストを表示できるようにするなどカスタマイズしています。
こちらの forceCenter
をHTML側の #drawarea
の中心に合わせる必要があり,大きくズレると「あれ?エラーはないのに出ないなぁ…」と困る羽目になります。
forceSimulation
がノードを自動で配置したり、マウスのドラッグでノードを動かすとほかのノードも追従するものになっています。
import * as d3 from "d3";
// 1. 描画用のデータ準備
const nodesData = [
{ id: "お母さん" },
{ id: "赤ずきんちゃん" },
{ id: "おばあさん" },
{ id: "オオカミ" },
{ id: "猟師さん" },
{ id: "お花" }
];
const linksData = [
{ source: 0, target: 1 },
{ source: 1, target: 2 },
{ source: 3, target: 1 },
{ source: 3, target: 2 },
{ source: 4, target: 3 },
{ source: 1, target: 5 },
{ source: 5, target: 2 }
];
// JS内に埋め込んだデータでも、d3.json()で外部から読み込んだデータでも描画できるように
// 描画部分は別の関数に分けている
process_network(nodesData, linksData);
function process_network(nodesData, linksData) {
console.log(linksData);
// 2. svg要素を配置
var link = d3
.select("#drawarea")
.selectAll("line")
.data(linksData)
.enter()
.append("line")
.attr("stroke-width", 1)
.attr("stroke", "black");
var node = d3
.select("#drawarea")
.selectAll("g")
.data(nodesData)
.enter()
.append("g")
.call(
d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
node
.append("circle")
.attr("r", 24)
.attr("stroke", "black")
.attr("stroke-width", 1.5)
.attr("fill", "peachpuff");
node
.append("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.style("fill", "gray")
.text(function(d) {
return d.id;
})
.append("title")
.text(function(d) {
return d.id;
});
// 3. forceSimulation設定
// ここでエッジの長さや描画の中心などを決める
var simulation = d3
.forceSimulation(nodesData)
.velocityDecay(0.3)
.alpha(0.7)
.force("link", d3.forceLink().distance(250))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(200, 200));
simulation.nodes(nodesData).on("tick", ticked);
simulation.force("link").links(linksData);
// 4. forceSimulation 描画更新用関数
function ticked() {
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node
.select("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
// 追加: ノードの下に付けたテキストもforceSimulationで追従できるように設定する
node
.select("text")
.attr("dx", function(d) {
return d.x;
})
.attr("dy", function(d) {
return d.y + 40;
});
}
// 5. ドラッグ時のイベント関数
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
ここでwebpack-dev-serverを起動します。うまくいけば図が表示されます。
npm run start
d3.jsが読み込まれているみたいだけど図を描く操作が反映されない
この場合、スクリプトは body
タグの一番最後で読み込む必要があります。
https://stackoverflow.com/questions/23803146/d3js-external-javascript-file
You're probably including the Javascript file in the header. You need to include it in the body after the elements it's operating on are defined. – Lars Kotthoff May 22 '14 at 10:03
ほかの例 でも body
の一番最後でスクリプトを読み込んでいますね🤔
D3.jsは簡単に図を描くものではなく、図とデータを結びつけるもの
D3.jsにおける描画はあらかじめ用意された描画用の関数を通して、ではなくSVG/canvasの要素とデータを紐付けて描画します。
そのため「D3.jsがあるから簡単に図が出せる」…というわけではありません。
詳しくカスタマイズするときはSVGの知識が必要にあります。この例を作るときもSVGについて調べました。
(canvasで実現しようとするとD3.jsだけでは足りず、Fubric.jsも同時に使う例がヒットします)
また、D3.jsのサンプルを見ると その種類は多岐に渡り、「どの図を使おう」「どうやって書いたらいいのかイメージできない」といった事態に陥ります。
特に「簡単にグラフを描きたい!」という場合はChart.jsの導入を検討してください。
出力されるJavaScriptを圧縮する
Webpackをproductionモードで実行すると出力されるJavaScriptファイルが自動的に圧縮されます。
https://www.konosumi.net/entry/2018/06/23/024057
https://webpack.js.org/configuration/mode/
そのモードの切替ですが、
-
package.json
でWebpackを起動するスクリプトに環境変数を仕込み -
webpack.config.js
で環境変数をmode
にセットすると
npm
コマンドの使い分けでモードの切替ができます。
はじめに、 package.json
のスクリプトの設定で webpack
コマンドの前に WEBPACK_MODE
の環境変数を仕込みます・
"scripts": {
"start": "webpack-dev-server --hot",
"dev": "WEBPACK_MODE=development webpack",
"build": "WEBPACK_MODE=production webpack",
"watch": "webpack --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
webpack.config.js
は環境変数で mode
を切り替えられるようにします。
const path = require("path");
module.exports = {
mode: process.env.WEBPACK_MODE,
...
npm
のコマンドで開発用・本番用ビルドを切り替えることができます。
# 開発用ビルド
npm run dev
# 本番用ビルド(JS圧縮有効)
npm run build
圧縮にはES6に対応した TerserPlugin
が使用されます。
Sets process.env.NODE_ENV on DefinePlugin to value production . Enables FlagDependencyUsagePlugin , FlagIncludedChunksPlugin , ModuleConcatenationPlugin , NoEmitOnErrorsPlugin , OccurrenceOrderPlugin , SideEffectsFlagPlugin and TerserPlugin .
また、productionモードではWebpackでまとめたファイルと元のファイルの対応関係を出してデバッグを容易にするmapファイルを出力しないようにしましょう。
https://qiita.com/mwatanabe@github/items/42cfdc5fdf193eee5a98
module.exports = {
mode: process.env.WEBPACK_MODE,
entry: "./src/main.js",
// この一行を削除して
devtool: "source-map",
...
};
// ここでモードごとにmapファイルを出力するかどうか決める
if (process.env.WEBPACK_MODE !== 'production') {
module.exports.devtool = 'source-map';
}
おわりに
この方法でD3.jsなど欲しいライブラリをnpmのパッケージで使うことができました。
すでにプロジェクトで他のnpmパッケージを導入しているのであれば、読み込み方法の統一やパッケージの依存関係の解決の観点でnpmのパッケージでライブラリを導入したくなります。
一応サンプルコードはこちらにあります。
https://github.com/ysd-marrrr/d3-webpack-sample-20191212
D3.jsをはじめてみてSVGの書き方を知ることになりました
参考
【TypeScript+D3.js】Web初心者がWebアプリケーションをでっちあげるまでの道のり - Qiita
https://qiita.com/YSRKEN/items/6683ec1c1de085935a69
npmとwebpack4でビルド - jQueryからの次のステップ - Qiita
https://qiita.com/civic/items/82c0184bcadc50965f91#es2015es6%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B
webpack4対応webpack-dev-serverの主要な設定オプション(CLI,webpack.config.js)の意味と挙動 - Qiita
https://qiita.com/riversun/items/d27f6d3ab7aaa119deab
【初心者向け】NPM・package.jsonを理解する - Qiita
https://qiita.com/righteous/items/e5448cb2e7e11ab7d477
D3.jsにあてはまらないこと
https://postd.cc/what-d3js-is-not/
D3.js v4/v5 force simulation 最小構成 – サンプル
https://wizardace.com/d3-forcesimulation-simple/