LoginSignup
7
6

More than 3 years have passed since last update.

D3.jsのnpmパッケージを使いたいがためにWebpackを設定する

Last updated at Posted at 2019-12-14

この記事は「株式会社オープンストリーム 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

npmpackage.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 で呼び出して図を描画する

20191212.png

D3.jsを使って図を描くページ: ./public/index.html

HTMLにはD3.jsで描画された図を出すためのボックスのみを用意します。

今回は npm でD3.jsを使えるようにするため、スタイルシートは簡単にHTML内に入れています。
Webpackには外部のCSSファイルを下のようにHTML内に埋め込んでくれるプラグインもあります!

./public/index.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 がノードを自動で配置したり、マウスのドラッグでノードを動かすとほかのノードも追従するものになっています。

./src/main.js
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

20191212-sample.png

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 の環境変数を仕込みます・

package.json
  "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 を切り替えられるようにします。

webpack.config.js
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

webpack.config.js
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/

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6