Elm

elm-vegaでグラフ描画 - JavaScriptからportsでヘッドレスElm

 elm-vegaライブラリを少し触ってみましたので、その備忘録です。簡単に言えばelm-vegaはElm言語でVega-Liteのspecを書くためののもので、Vega-LiteはVegaを使いやすくしたものです。VegaとD3.jsがどう違うのかは以下の記事に詳細にあります。
Vega and D3 - Vega is NOT intended as a "replacement" for D3.

 ちょっと私の理解が追い付かないところもありますが、Vegaは直接描画する代わりに、プログラムとかで描画するためのspecを吐き出し、そのspecはVegaのruntimeで表示させるものだとあります。specはポータブルだということでWebに限定されないようですね。これからもD3.jsとは併存するだろうが、より広く使われていくことになるだろう、みたいなことが述べられています。

関連記事:elm-visualization(D3-like)を使ってみた - Qiita

1.elm-vegaとは

 Vega – A Visualization Grammar
 Vegaはインタラクティブなビジュアルデザインをクリエイトし、保存し、シェアするための宣言的な言語(文法)です。VegaのJSONフォーマットで、ビジュアル表現やビジュアルのインタラクティブな動作を記述することができます。そして Canvas や SVGを使ったWebのビューデータを生成することができます。

 Vega-Lite – A Grammar of Interactive Graphics
 Vega-Liteはインタラクティブ・グラフィックのためのハイレベルな文法です(Vegaと比べてハイレベル?)。データ解析のためのビジュアルを素早く生成することができます。Vega-Lite specificationsはVega specificationsへとコンパイルされます。

 elm-vega - Declarative visualization for Elm
 elm-vegaライブラリを使って、Vega-Lite specificationsを創り出すことができます。Elmは純粋関数なので、宣言的に記述することができます。このライブラリはグラフィックデータを直接生み出しはしません。その代わりにJSON specificationを生成し、Vega-Lite runtime(JavaScript ライブラリ)に送り付けることによって、グラフィックを生成します。それゆえ、このライブラリは'pure'な Elm packageであり、他の non-Elm dependenciesは含みません

 つまりelm-vegaライブラリを使って、Elmコードを書くことによって、Vega-Lite spec を生成し、Vega-Lite runtimeであるJavaScript関数に渡せば、素敵なグラフィックを表示できるということです。興味深いのはHaskellもそうですが、純粋関数型言語はDSLに向いていると言われていますが、ここでのElmの使われ方がまさにそれにあたる気がします。Elmのイベント処理(update)やviewなどの機能は一切使われていません。ただその純粋さが求められているだけです。(青春ですかね。)

            elm-vega                 Vega-Lite runtime
elmコード     --->    Vega-Lite spec      --->            Vega spec  --> Canvas or SVG

2.ソースコード

 プログラムコードですが、exampleのWalkthrough.elmを使います。これはたくさんのグラフを表示しているものですが、ここでは3個だけ表示するようにします。

 またElmプログラムからJavaScriptプログラムにportを使ってデータを渡しますが、portの使い方は過去記事のElmとJavaScriptの会話(Port) - Qiitaを参照してください。

 まずはWalkthrough.elmです。このElmプログラムの役割は、Vega-Lite specを生成するためのコードを書くことです。生成したVega-Lite specはportを通して、JavaScriptのVega-Lite runtimeに渡されます。

Walkthrough.elm
port module Walkthrough exposing (elmToJS)

import Platform
import VegaLite exposing (..)


stripPlot : Spec
stripPlot =
    toVegaLite
        [ dataFromUrl "data/seattle-weather.csv" []
        , mark Tick []
        , encoding (position X [ PName "temp_max", PmType Quantitative ] [])
        ]


histogram : Spec
histogram =
    let
        enc =
            encoding
                << position X [ PName "temp_max", PmType Quantitative, PBin [] ]
                << position Y [ PAggregate Count, PmType Quantitative ]
    in
    toVegaLite
        [ dataFromUrl "data/seattle-weather.csv" []
        , mark Bar []
        , enc []
        ]


stackedHistogram : Spec
stackedHistogram =
    let
        enc =
            encoding
                << position X [ PName "temp_max", PmType Quantitative, PBin [] ]
                << position Y [ PAggregate Count, PmType Quantitative ]
                << color [ MName "weather", MmType Nominal ]
    in
    toVegaLite
        [ dataFromUrl "data/seattle-weather.csv" []
        , mark Bar []
        , enc []
        ]


{- This list comprises tuples of the label for each embedded visualization and
   corresponding Vega-Lite specification.
-}


mySpecs : Spec
mySpecs =
    combineSpecs
        [ ( "singleView1", stripPlot )
        , ( "singleView2", histogram )
        , ( "singleView3", stackedHistogram )
        ]



{- The code below is boilerplate for creating a headless Elm module that opens
   an outgoing port to Javascript and sends the specs to it.
-}


main : Program Never Spec msg
main =
    Platform.program
        { init = ( mySpecs, elmToJS mySpecs )
        , update = \_ model -> ( model, Cmd.none )
        , subscriptions = always Sub.none
        }


port elmToJS : Spec -> Cmd msg

 このElmプログラムの出力であるmySpecsは以下のようになります。この程度のspecならElmプログラムを使わずに手で直にかけるかもしれませんね。elm-vegaでのspecの書き方と、Vega-Liteのspecの書き方は、それぞれのドキュメントで確認してください。木を見て森を見ず。ここでは森だけを見ていきましょう。

mySpecs
{ "singleView1":
    {"$schema":"https://vega.github.io/schema/vega-lite/v2.json",
     "data":{"url":"data/seattle-weather.csv"},
     "mark":"tick","encoding":{"x":{"field":"temp_max","type":"quantitative"}}},

  "singleView2":
    {"$schema":"https://vega.github.io/schema/vega-lite/v2.json",
     "data":{"url":"data/seattle-weather.csv"},
     "mark":"bar",
     "encoding":{"x":{"field":"temp_max","type":"quantitative","bin":true},
                 "y":{"aggregate":"count","type":"quantitative"}}},

  "singleView3":
    {"$schema":"https://vega.github.io/schema/vega-lite/v2.json",
     "data":{"url":"data/seattle-weather.csv"},
     "mark":"bar",
     "encoding":{"x":{"field":"temp_max","type":"quantitative","bin":true},
                 "y":{"aggregate":"count","type":"quantitative"},
                 "color":{"field":"weather","type":"nominal"}}}}

 さてElmプログラムの最後の部分ですが、Platform.programはviewを持たないプログラムを宣言しています。これをヘッドレスプログラムと呼びます。以下がElm公式サイトPlatformで定義されている型です。modelはSpecという型になっています。ちなみに普通のヘッド有りプログラムは Html.programですね。

Platform
type Program flags model msg
init : (model, Cmd msg)
update : msg -> model -> (model,Cmd msg)

 ここで注目すべきは、initだけが定義されていて、この初期化のCmdはport(elmToJS)でmySpecsというデータをJavaScriptに送っています。その他の、Model-View-Updateパタンの全てがダミーの定義ですね。このコードはmySpecsというデータを作るだけのものと言えます。updateもviewもありません。

 次にmySpecsというデータを受け取るJavaScriptをみます。JavaScriptはindex.htmlに埋め込んであります。Elmプログラムはviewを持ちませんので、index.htmlに表示する必要はありません。代わりにVega-Lite runtimeがElmから受け取ったmySpecsというデータを表示します。以下に少し説明します。

index.html
<!DOCTYPE html>

<head>
  <title>elm-vega Walkthrough Examples</title>
  <meta charset="utf-8">

  <link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
  <style>body {font-family: 'Roboto', sans-serif;} figcaption {padding-top: 0.5em;}.cell {  padding:1em; border-radius: 1em; margin:1.2em; background-color: rgb(251,247,238);}</style>

  <!-- Vega-Lite runtimeのJavaScriptライブラリを読み込んでいる個所です -->
  <script src="https://cdn.jsdelivr.net/npm/vega@3"></script>
  <script src="https://cdn.jsdelivr.net/npm/vega-lite@2"></script>
  <script src="https://cdn.jsdelivr.net/npm/vega-embed@3"></script>

  <!-- Walkthrough.elmのコンパイル結果を読み込んでいる個所です -->
  <script src="walkthrough.js"></script>
</head>

<body>
  <h1>elm-vega Walkthrough Examples</h1>

  <p>
    For details see the <a href="https://github.com/gicentre/elm-vega/tree/master/docs/walkthrough" target="_blank">elm-vega walkthrough</a>.
  </p>

  <h2>Single View Specifications</h2>

  <!-- 3つのグラフを表示する個所です -->
  <div id="singleView1"></div>A Simple Strip Plot
  <div id="singleView2"></div>A Simple Frequency Histogram
  <div id="singleView3"></div>A Stacked Frequency Histogram

  <script>
    Elm.Walkthrough.worker().ports.elmToJS.subscribe(function(namedSpecs) {
console.log(JSON.stringify(namedSpecs));
      for (let name of Object.keys(namedSpecs)) {
        vegaEmbed(`#${name}`, namedSpecs[name], {
          actions: true, logLevel:vega.Warn
        }).catch(console.warn);
      }
    });
  </script>

</body>

</html>

 以下がVega-Lite runtimeがElmから受け取ったmySpecsというデータを表示している箇所です。

    Elm.Walkthrough.worker().ports.elmToJS.subscribe(function(namedSpecs) {
      for (let name of Object.keys(namedSpecs)) {
        vegaEmbed(`#${name}`, namedSpecs[name], {
          actions: true, logLevel:vega.Warn
        }).catch(console.warn);
      }
    });

 Elm.Walkthrough.worker()はElmのヘッドレスプログラムをJavaScriptからアクセスするやり方です。以下のように分解して書けば、普通のヘッド有りプログラムの場合と似ていてわかりやすいかもしれません。

const app = Elm.Walkthrough.worker();
app.ports.elmToJS.subscribe(function(namedSpecs) {

namedSpecsは上で示したmySpecsのjsonデータそのものです。"singleView1"と"singleView2"、"singleView3"の3つのkeyでループしています。そこにおいてVega-Lite runtimeのvegaEmbed関数で、例えば"singleView1"のspecを、id="singleView1"のDOMにグラフ表示しています。

ちなみに以下はテンプレートリテラルでES6で利用可能になったものです。ヒアドキュメントですね。${}で変数も書けます。ここでは"#singleView1"などの文字列を表現しています。

`#${name}`

 以上でプログラムの説明は終わります。

3.exampleを動かしてみる

 それでは実際にプログラムを動かしてみましょう。

 まず、ディレクトリを作成し、パッケージをインストールします。

mkdir elm-vega-test
cd elm-vega-test
elm-package install gicentre/elm-vega

 環境設定は以上で終わりです。elm-vega はElm言語の宣言的な純粋性のみを必要としていますので、これで十分です。

 elm-vega-testディレクトリのトップにWalkthrough.elmとindex.htmlを配置しておきます。また忘れないでdataをexampleからディレクトリごとコピーしておいてください。

 次にElmプログラムをコンパイルしておきます。

elm-make Walkthrough.elm --output walkthrough.js

 

 さてここで重要な注意点があります。elm-reactorは使わずに、ちゃんとしたhttpサーバを使ってください。elm-reactorではVega-Lite runtimeがdataをurl取得できないようです。(これで嵌って数時間潰しました)。ここではhttp-serverを使います。まだインストールしてない場合は以下のようにして簡単にインストールできます。

npm install http-server -g

 Webサーバを起動します

http-server -p 3030

 ブラウザを開くと3個のグラフが表示されます。

image.png

以上です