D3.js による折れ線グラフ SVG の作成と PNG 変換 - Node.js

前回、(ConvNetJS による処理結果を)折れ線グラフ化した方法に関して書いておきます。

ソースは http://github.com/fits/try_samples/tree/master/blog/20160222/

準備

Node.js で D3.js を使うには、下記モジュールを npm でインストールします。

インストール例
> npm install d3 jsdom --save

D3.js は DOM を操作しますので、Node.js で使うには DOM を扱う jsdom のようなモジュールが必要となります。

(1) 折れ線グラフ(SVG)作成

前回csv ファイルの内容をグラフ化しましたが、今回は変数定義した配列の値を折れ線グラフ化する事にします。 (配列の値を縦軸、配列のインデックス値 + 1 を横軸にします)

グラフ作成の手順は以下の通りです。

  • (a) svg 要素の追加
  • (b) スケールの定義
  • (c) 軸の定義
  • (d) 軸の追加
  • (e) 折れ線の追加

(a) では console.log(document.body.innerHTML) した内容 ※ が .svg ファイルとして (Web ブラウザ等で) そのまま表示できるように xmlns を設定しています。

 ※ 今回、document.body へ追加した要素は svg 要素だけですので、
    document.body.innerHTML は svg 要素の内容となります

また、今回は svg 要素の直下ではなく、translate を使って left と top のマージン分だけ位置をずらした g 要素の下にグラフを追加するようにしています。

(b) では、描画するデータの値の範囲(domain で指定)を DOM 要素の幅・高さ(range で指定)へマッピング(変換)する関数を作成します。

今回のグラフの場合、要素の高さとグラフの縦軸で数値の増加する方向が逆になるため (高さは上から下、縦軸は下から上)、高さ 0 の位置が縦軸の最大値となるようにマッピングしています。

(c) では axis を使って縦軸や横軸を生成する関数を作成します。縦軸か横軸かは orient へ指定した値で決定されるようです。

(d) では (c) の結果を call へ指定する事で縦軸や横軸を追加できます。今回は text 要素を使って軸のラベルも追加しています。

あとは折れ線を作成する関数を定義し、path 要素の d 属性へ折れ線の作成結果を設定するだけです。(e)

line_chart.js
var d3 = require('d3');
var jsdom = require('jsdom').jsdom;

var w = 400; // 幅 (マージン部分は除く)
var h = 300; // 高さ (マージン部分は除く)
var margin = { left: 50, top: 20, bottom: 50, right: 20 };
var xDomain = [0, 10]; // 横軸の値の範囲
var yDomain = [15, 0]; // 縦軸の値の範囲

var document = jsdom();

// データ
var data = [3, 5, 4, 6, 9, 2, 8, 7, 5, 11];

// (a) svg 要素の追加
var g = d3.select(document.body).append('svg')
    .attr('xmlns', 'http://www.w3.org/2000/svg')
    .attr('width', w + margin.left + margin.right)  // svg 要素の幅
    .attr('height', h + margin.top + margin.bottom) // svg 要素の高さ
    .append('g')
        .attr('transform', `translate(${margin.left}, ${margin.top})`);

// (b) スケールの定義
var x = d3.scale.linear().range([0, w]).domain(xDomain);
var y = d3.scale.linear().range([0, h]).domain(yDomain);

// (c) 軸の定義
var xAxis = d3.svg.axis().scale(x).orient('bottom');
var yAxis = d3.svg.axis().scale(y).orient('left');

// (d1) 横軸の追加
g.append('g')
    .attr('transform', `translate(0, ${h})`)
    .call(xAxis)
    .append('text') // 横軸のラベル
        .attr('x', w / 2)
        .attr('y', 35)
        .text('番号');

// (d2) 縦軸の追加
g.append('g')
    .call(yAxis)
    .append('text') // 縦軸のラベル
        .attr('x', -h / 2)
        .attr('y', -30)
        .attr('transform', 'rotate(-90)')
        .text('個数');

// 折れ線の作成関数
var createLine = d3.svg.line()
    .x( (d, i) => x(i + 1) )
    .y( d => y(d) );

// (e) 折れ線の追加
g.append('path')
    .attr('d', createLine(data))
    .attr('stroke', 'blue')
    .attr('fill', 'none');

// SVG の出力
console.log(document.body.innerHTML);

実行結果は以下の通りです。

実行例
node line_chart.js > sample.svg
sample.svg の例 (実際は改行・字下げは無し)
<svg xmlns="http://www.w3.org/2000/svg" width="470" height="370">
  <g transform="translate(50, 20)">
    <g transform="translate(0, 300)">
      ・・・
    </g>
    ・・・
    <!-- 折れ線部分 -->
    <path d="M40,240L80,200L120,219.99999999999997L160,180L200,120L240,260L280,140L320,160L360,200L400,80" stroke="blue" fill="none"></path>
  </g>
</svg>

(2) SVGPNG へ変換

次は、(1) で出力した SVG ファイルを PNG ファイルへ変換します。

変換する方法は色々あると思いますが、今回はコマンドラインで使用する事を前提にして下記を試してみました。

(a) PhantomJS でキャプチャする

Web コンテンツを open して render する PhantomJS 用スクリプトを用意します。

capture.js
var system = require('system');

var webFile = system.args[1];
var pngFile = system.args[2];

var page = require('webpage').create();

page.open(webFile, function(status){
    if (status == 'success') {
        // ページのキャプチャをファイルへ出力
        page.render(pngFile);
    }

    phantom.exit();
});

phantomjs コマンドで以下のように実行すれば SVG ファイルを PNG ファイルへ変換できます。

実行例
> phantomjs capture.js sample.svg sample_phantom.png
sample_phantom.png

f:id:fits:20160222005216p:plain

(b) ImageMagick を使う

convert コマンドで変換すると、文字化けしました。

実行例
> convert sample.svg sample_im.png
sample_im.png

f:id:fits:20160222005231p:plain

-font オプションを使っても駄目でしたが、 以下のように SVGfont-family の style 設定を追加すると文字化けは解消しました。

line_chart_im.js
・・・
// 横軸の描画
svg.append('g')
    .attr('transform', `translate(0, ${h})`)
    .call(xAxis)
    .append('text')
        .attr('x', w / 2)
        .attr('y', 35)
        // ImageMagick による PNG 変換時の文字化け対策
        .style('font-family', 'Sans') 
        .text('番号');

// 縦軸の描画
svg.append('g')
    .call(yAxis)
    .append('text')
        .attr('x', -h / 2)
        .attr('y', -30)
        .attr('transform', 'rotate(-90)')
        // ImageMagick による PNG 変換時の文字化け対策
        .style('font-family', 'Sans')
        .text('値');
・・・
実行例2 (font-family 版)
> node line_chart_im.js > sample2.svg
> convert sample2.svg sample2_im.png
sample2_im.png

f:id:fits:20160222005249p:plain

(c) GraphicsMagick を使う

普通に gm convert すると、gm convert: Unable to read font (n019003l.pfb) というエラーが発生しました。 (Ghostscript をインストールしていない事が原因かも)

今回は -font でフォントを指定して回避しました。

実行例
> gm convert -font ipagp.ttf sample.svg sample_gm.png
sample_gm.png

f:id:fits:20160222005304p:plain

(d) Inkscape を使う

Inkscapeベクター画像編集の GUI ツールですが、コマンドラインでも実行できます。 PNG ファイルへ変換するには -e オプションを使用します。

実行例
> inkscape -f sample.svg -e sample_ink.png
sample_ink.png

f:id:fits:20160222005326p:plain