D3.jsの手続き的記述をElmで宣言的にスマートに書こう!


SVGは普段お使いですか? SVGを操作するためのライブラリは何をお使いでしょうか? JavaScriptでは、まず間違いなくD3.jsの名前が挙がるのではないでしょうか。今回の記事では関数型言語Elmを利用し宣言的に書けるスマートさが伝わればなと思っています。例として使うのは以下のような棒グラフモドキです。

以下の記事のサンプルを流用させていただきました。@m_ohsumiさんありがとうございました。

D3.jsの概要と使い所について

D3.jsの概要と問題点

D3.jsはSVGやHTMLをDOM操作するjQueryとよく似たライブラリです。数多くの機能と豊富なサンプルがあり小回りが効くライブラリです。しかし、手続きによる処理が多いため、DOMの状態を意識する必要があったり、機能が多すぎて自分がやりたい操作を習得するまでの学習的コストが掛かる点です。

宣言的な記述

近年、仮想DOMという考えが広まり様々なライブラリが、その考えを取り入れました。ReactはJSXの記法を利用することで、描画されるSVG表記ほぼそのままの形で直感的に記述することができます。

return (
        <svg
            width="100%"
            height="100%"
            xmlns="http://www.w3.org/2000/svg"
            xmlnsXlink="http://www.w3.org/1999/xlink"
        >
            <style>
                { `.classA { fill:${props.fill} }` }
            </style>
            <defs>
                <g id="Port">
                    <circle style={{fill:'inherit'}} r="10"/>
                </g>
            </defs>

            <text y="15">black</text>
            <use x="70" y="10" xlinkHref="#Port" />
            <text y="35">{ props.fill }</text>
            <use x="70" y="30" xlinkHref="#Port" className="classA"/>
            <text y="55">blue</text>
            <use x="0" y="50" xlinkHref="#Port" style={{fill:'blue'}}/>
        </svg>
    );

Elm

関数型言語Elmは、AltJSの一種でJSXのような特殊な文法はありませんが、基礎文法の範囲内で無理なく宣言的にHTMLやSVGを記述することができます。その上、型安全で強力なコレクションライブラリなどが標準で備わっていることが魅力です。

D3.jsとElmの比較

それでは、D3.jsとElmの棒グラフを描画するためのコードを比較してみましょう。描画するためのデータをオブジェクトの配列の形で保持し、描画の計算に必要な値を宣言します。そして、d3オブジェクトからDOM操作に必要なメソッドを呼び出していきますが、種類が多く正確に意味を捉えていくことは難しいです。D3.jsの解説記事ではないので細かくは説明しませんが、セレクタで要素を指定し、dataメソッドで描画に必要なデータをセットし矩形(rect)の属性を付与していく形になっています。私の場合は、attrの呼び出しが単なる値の場合コールバック関数でデータのみを引数に取る場合コールバック関数でデータとインデックスを引数に取る場合と一つのメソッドで何パターンもあるのが複雑と感じました。

const chartData = [
      { value: 100, color: 'blue' },
      { value: 20, color: 'red' },
      { value: 10, color: 'green' }
  ];

  const SVG_HEIGHT = 150;
  const BAR_WIDTH = 100;
  const BAR_INTERVAL = 20;

  let $bars = d3.select('.svg')
      .selectAll('rect')
      .data( chartData )
      .enter()
      .append('rect')
      .attr('x', (d, i) => {
          return i * BAR_WIDTH + i * BAR_INTERVAL;
      })
      .attr('y', (d) => {
          return SVG_HEIGHT - d.value;
      })
      .attr('width', BAR_WIDTH)
      .attr('height', (d) => {
          return d.value;
      })
      .attr('fill', (d) => {
          return d.color;
      });

Elmで記述する場合を見ていきましょう。読み慣れていない人のために部分的に解説をしていきます。まずはデータの宣言部分です。Elmにはレコード型というものが用意されており、JavaScriptのオブジェクトと同じような機能を持つものです。しかし型に厳密で、存在しないキーなどはコンパイルエラーとして教えてくれます。今回はvaluefillを要素として持つレコードにRectという名前を付けました。。必要な値は、関数として定義していきます。chartDataは、Rectのリストです。オブジェクトのように{value = 100, fill = "blue" }のように書くこともできますが、型名を利用して、以下のコードのようにシンプルに書くこともできます。

type alias Rect =
      { value : Int, fill : String }


  svgHeight =
      150


  barWidth =
      100


  barInterval =
      20


  chartData =
      [ Rect 100 "blue"
      , Rect 20 "red"
      , Rect 10 "green"
      ]

以下が、整数(Int)とRectレコードを受け取り、一つの矩形(SVG)を返す関数になります。内部は、rect [属性群] [子要素]という形式の関数を呼び出しています。<|は、パイプ関数で、x (toString (i * barWidth + i * barInterval))という括弧のネストを無くす為に使っています。x, y, width, height, fillこれらは属性を表す関数で、定義されているものなのでスペルミスや値の型が違う場合はコンパイル時にエラーを出してくれるため非常に安全です。

bar : Int -> Rect -> Svg msg
bar i rct =
      rect
          [ x <| toString <| i * barWidth + i * barInterval
          , y <| toString <| svgHeight - rct.value
          , width <| toString barWidth
          , height <| toString rct.value
          , fill rct.fill
          ]
          []

mainの関数はとてもシンプルです。let-inは、letでローカル変数を定義し、inで戻り値を返すための式を記述します。svgの子要素にbarsリストを渡しているので、これが棒グラフ本体になります。List.indexedMapの型は、(Int -> a -> b) -> List a -> List bで、整数とa型を受け取りb型を返す関数とa型のリストを渡せば、a型の要素に対して渡した関数を適用していってb型のリストを返してくれます。このときの整数は、関数名から分かる通りリストのインデックスになります。渡す関数というのは、先ほど説明した棒グラフのSVGを返すためのbar関数になります。

main =
      let
          bars =
              List.indexedMap bar chartData
      in
          svg [] bars

コード全体です。OnlineEditorにコードを貼ればElmを今すぐ試せます。

まとめ

SVGの操作を行うためのライブラリの説明と比較をおこないました。常にElmが最良の選択肢ではありません。パフォーマンスがシビアで細かい調整が求められるような場合にはD3.jsの機能の豊富さやサンプルの多さは強い武器となります。しかし学習コストが高く、単純なものでも複雑な記述が必要となります。描画以外の複雑なロジックを解決したり、宣言的にスマートに記述し、可読性を向上させるためにはElmはとても良い選択だと思います。興味があれば是非Elmにチャレンジしてみてください。