Elm

Elm Svg と elm-visualization

 elm-visualizationはD3.jsの置き換えを狙ったElmのライブラリです。まだ開発中ですが、D3.jsに比べればわかりやすい気がします。Elmのありがたみはこんなところにあるんだなと思います。明確でわかりやすい記述。またelm-visualizationははElmのSVGパッケージを使っています。以下の公式サイトを見ながら取り組んでみました。

gampleman/elm-visualization - A visualization package for Elm (D3-like)
elm-lang/svg - Fast SVG in Elm

 elm-visualizationでやれるところはelm-visualizationでやる。やれなければ多少の時間を覚悟してD3.jsを検討する。またはプロトタイプをelm-visualizationでつくる。もっと綺麗にとかスムーズにとかの必要があればD3.jsでの置き換えを考える。

1.サンプルプログラムの説明

 今回のサンプルプログラムは以下のようなSVGを生成し、ブラウザ上に折れ線グラフを描きます。リストしたデータは実際のものより少ないです。また1秒ごとにデータを削除・追加して、右から左に遷移させるようにします。

<svg width="900px" height="450px">
  <g transform="translate(29, 420)">
    <g fill="none" font-size="10" font-family="sans-serif" text-anchor="middle">
        <path class="domain" stroke="#000" d="M0.5,6V0.5H840.5V6"></path>
        <g class="tick" transform="translate(0, 0)"><line stroke="#000" y2="6" x1="0.5" x2="0.5"></line><text fill="#000" y="9" x="0.5" dy="0.71em">0</text></g>
        <g class="tick" transform="translate(210, 0)"><line stroke="#000" y2="6" x1="0.5" x2="0.5"></line><text fill="#000" y="9" x="0.5" dy="0.71em">1</text></g>
        <g class="tick" transform="translate(420, 0)"><line stroke="#000" y2="6" x1="0.5" x2="0.5"></line><text fill="#000" y="9" x="0.5" dy="0.71em">2</text></g>
        <g class="tick" transform="translate(630, 0)"><line stroke="#000" y2="6" x1="0.5" x2="0.5"></line><text fill="#000" y="9" x="0.5" dy="0.71em">3</text></g>
        <g class="tick" transform="translate(840, 0)"><line stroke="#000" y2="6" x1="0.5" x2="0.5"></line><text fill="#000" y="9" x="0.5" dy="0.71em">4</text></g>
    </g>
  </g>
  <g transform="translate(29, 30)">
    <g fill="none" font-size="10" font-family="sans-serif" text-anchor="end">
        <path class="domain" stroke="#000" d="M-6,390.5H0.5V0.5H-6"></path>
        <g class="tick" transform="translate(0, 390)"><line stroke="#000" x2="-6" y1="0.5" y2="0.5"></line><text fill="#000" x="-9" y="0.5" dy="0.32em">0</text></g>
        <g class="tick" transform="translate(0, 312)"><line stroke="#000" x2="-6" y1="0.5" y2="0.5"></line><text fill="#000" x="-9" y="0.5" dy="0.32em">1</text></g>
        <g class="tick" transform="translate(0, 234)"><line stroke="#000" x2="-6" y1="0.5" y2="0.5"></line><text fill="#000" x="-9" y="0.5" dy="0.32em">2</text></g>
        <g class="tick" transform="translate(0, 156)"><line stroke="#000" x2="-6" y1="0.5" y2="0.5"></line><text fill="#000" x="-9" y="0.5" dy="0.32em">3</text></g>
        <g class="tick" transform="translate(0, 78)"><line stroke="#000" x2="-6" y1="0.5" y2="0.5"></line><text fill="#000" x="-9" y="0.5" dy="0.32em">4</text></g>
        <g class="tick" transform="translate(0, 0)"><line stroke="#000" x2="-6" y1="0.5" y2="0.5"></line><text fill="#000" x="-9" y="0.5" dy="0.32em">5</text></g>
    </g>
  </g>
  <g transform="translate(30, 30)" class="series">
    <path d="M0,195 C70,214.5 140,234 210,234 C280,234 350,117 420,117 C490,117 560,234 630,234 C700,234 770,195 840,156" stroke="red" stroke-width="3px" fill="none"></path>
  </g>
</svg>

image.png

 AWS S3に今回のサンプルプログラムをアップロードしてあります。グラフが1秒ごとに更新され遷移していくさまが確認できます。
          =====> AWSサンプルプログラムのライブ!

2.Elmコード

2-1. プログラムの実行

 順番が逆ですけど、とりあえずコンパイル&実行のコマンドです。

cd visualization-line
elm-package install gampleman/elm-visualization
elm-make Line.elm --output elm.js
elm-reactor -a=www.mypress.jp -p=3030

2-2. 全ソースコード

 以下にElmの全ソースを示します。

Line.elm
module Line exposing (main)

import Html exposing (..)
import Html.Events exposing (onClick)
import Random
import Time exposing (Time, second)

import Svg exposing (..)
import Svg.Attributes exposing (..)
import Visualization.Axis as Axis exposing (defaultOptions)
import Visualization.List as List
import Visualization.Scale as Scale exposing (ContinuousScale, ContinuousTimeScale)
import Visualization.Shape as Shape


-- Main
main =
  Html.program
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }


-- Model
type alias Model =
  List ( Float, Float )



timeSeries : Model
timeSeries =
    [ ( 0, 2.5 )
    , ( 1, 2 )
    , ( 2, 3.5 )
    , ( 3, 2 )
    , ( 4, 3 )
    , ( 5, 1 )
    , ( 6, 1.2 )
    , ( 7, 1.2 )
    , ( 8, 1.2 )
    , ( 9, 1.2 )
    , ( 10, 1.2 )
    , ( 11, 1.2 )
    , ( 12, 1.2 )
    , ( 13, 1.2 )
    , ( 14, 1.2 )
    , ( 15, 1.2 )
    , ( 16, 1.2 )
    , ( 17, 1.2 )
    , ( 18, 1.2 )
    , ( 19, 1.2 )
    , ( 20, 1.2 )
    ]

maxX = (List.length timeSeries) - 1
lastXAxis = maxX |> toFloat
maxY = 5

init : (Model, Cmd Msg)
init =
  (timeSeries, Cmd.none)



-- Update
type Msg
  = Roll
  | NewFace Int
  | Tick Time

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Tick _ ->
      (model, Random.generate NewFace (Random.int 1 (maxY*10)))
    Roll ->
      (model, Random.generate NewFace (Random.int 1 (maxY*10)))
    NewFace newFace ->
      (listUpdate model newFace, Cmd.none)

listUpdate : Model -> Int -> Model
listUpdate model new =
    let
      newval =  new |> toFloat |> (\n->n/10.0)
      cdr = List.map (\(x,y)->(x-1,y)) (List.drop 1 model)
    in
      List.append cdr [(lastXAxis, newval)]



-- Subscriptions
subscriptions : Model -> Sub Msg
subscriptions model =
  --Sub.none
  Time.every second Tick


----- View

w : Float
w =
    900


h : Float
h =
    450


padding : Float
padding =
    30


xScale : ContinuousScale
xScale =
    Scale.linear ( 0, toFloat(maxX) ) ( 0, w - 2 * padding )


yScale : ContinuousScale
yScale =
    Scale.linear ( 0, toFloat(maxY) ) ( h - 2 * padding, 0 )


xAxis : Svg Msg
xAxis  =
    Axis.axis { defaultOptions | orientation = Axis.Bottom, tickCount = maxX } xScale


yAxis : Svg Msg
yAxis =
    Axis.axis { defaultOptions | orientation = Axis.Left, tickCount = maxY } yScale


transformToLineData : ( Float, Float ) -> Maybe ( Float, Float )
transformToLineData ( x, y ) =
    Just ( Scale.convert xScale x, Scale.convert yScale y )


line : List ( Float, Float ) -> Svg.Attribute Msg
line model =
    List.map transformToLineData model
        |> Shape.line Shape.monotoneInXCurve
        |> d


view : Model -> Html Msg
view model =
    div [] [ viewButton, viewSvg model ]

viewButton : Html Msg
viewButton =
    div [] [button [ onClick Roll ] [ Html.text "Roll" ]]

viewSvg : Model -> Svg Msg
viewSvg model =
    svg [ width (toString w ++ "px"), height (toString h ++ "px") ]
        [ g [ transform ("translate(" ++ toString (padding - 1) ++ ", " ++ toString (h - padding) ++ ")") ]
            [ xAxis ]
        , g [ transform ("translate(" ++ toString (padding - 1) ++ ", " ++ toString padding ++ ")") ]
            [ yAxis ]
        , g [ transform ("translate(" ++ toString padding ++ ", " ++ toString padding ++ ")"), class "series" ]
            [ Svg.path [ line model, stroke "red", strokeWidth "3px", fill "none" ] []
            ]
        ]

2-3. update関数

 とりあえず本プログラムの動作を説明するためにupdate関数を取り上げます。

update
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Tick _ ->
      (model, Random.generate NewFace (Random.int 1 (maxY*10)))
    Roll ->
      (model, Random.generate NewFace (Random.int 1 (maxY*10)))
    NewFace newFace ->
      (listUpdate model newFace, Cmd.none)

listUpdate : Model -> Int -> Model
listUpdate model new =
    let
      newval =  new |> toFloat |> (\n->n/10.0)
      cdr = List.map (\(x,y)->(x-1,y)) (List.drop 1 model)
    in
      List.append cdr [(lastXAxis, newval)]

-- Subscriptions
subscriptions : Model -> Sub Msg
subscriptions model =
  --Sub.none
  Time.every second Tick

 modelはList ( x, y )の形をしています。updateはイベントに応じてmodelを更新します。イベントは2種類あって、ボタンをクリックされた時と、subscriptionsでリッスンした時です。どちらの場合も、乱数で1-50のIntを生成し、それを1-5のFloatに変換しています。この時、modelの先頭から要素を削除し、お尻に乱数で生成した要素を追加します。listUpdate関数がこれを処理しています。

2-4. SVG と elm-visualization

 以下に SVG と elm-visualizationの部分の説明を行います。以下の2サイトを参照します。

gampleman/elm-visualization - A visualization package for Elm (D3-like)
elm-lang/svg - Fast SVG in Elm

 ソースコードでわかるだろう箇所は省きましたが、多少細かいかもしれません。

 以下が折れ線グラフのlineのパスデータ文字列を生成する部分です。

lineのパスデータ文字列を生成
line : List ( Float, Float ) -> Svg.Attribute Msg
line model =
    List.map transformToLineData model
        |> Shape.line Shape.monotoneInXCurve
        |> d

 以下のline関数は、(x,y)ポイントのリストからline(パスデータ文字列)を生成します。一般的に、SVGではd属性に曲線を引く際のパスコマンドのリスト(パスデータ文字列)を指定します。

Visualization.Shape.line
line :  (Curve -> List PathSegment) -> List (Maybe Point) -> String

 line関数の第一引数はcurve関数を指定します。

Visualization.Shape.monotoneInXCurve
monotoneInXCurve : Curve -> List PathSegment

 d関数は引数のパスデータ文字列を持つd属性を作ります。

Svg.Attributes.d
d : String -> Attribute msg

 以下のviewSvgがSVGを生成している箇所ですが、冒頭に示したSVGにきれいに対応しています。この辺の宣言的記述のわかりやすさが、Elm言語(関数型言語)の素晴らしさだと思います。

SVGの表示
viewSvg : Model -> Svg Msg
viewSvg model =
    svg [ width (toString w ++ "px"), height (toString h ++ "px") ]
        [ g [ transform ("translate(" ++ toString (padding - 1) ++ ", " ++ toString (h - padding) ++ ")") ]
            [ xAxis ]
        , g [ transform ("translate(" ++ toString (padding - 1) ++ ", " ++ toString padding ++ ")") ]
            [ yAxis ]
        , g [ transform ("translate(" ++ toString padding ++ ", " ++ toString padding ++ ")"), class "series" ]
            [ Svg.path [ line model, stroke "red", strokeWidth "3px", fill "none" ] []
            ]
        ]

 SVGではg 要素はオブジェクトをグループ化するためのコンテナです。またSVGではtransform属性で座標の調整を行います。x軸を以下のように定義します。

x軸
 g [ transform ("translate(" ++ toString (padding - 1) ++ ", " ++ toString (h - padding) ++ ")") ]
            [ xAxis ]

 g要素は以下のようなSVGに展開されます。x軸の開始ポイントはSVGの原点から右に29、下に420移動した地点になります。

<g transform="translate(29, 420)">

 次に以下のlineのパスデータ文字列を生成している箇所に注目します。複数の属性を指定してpathタグを生成しています。line modelはd属性にパスデータ文字列を指定したものでした。

lineのパスデータ文字列を生成
            [ Svg.path [ line model, stroke "red", strokeWidth "3px", fill "none" ] []

 path関数の型です。

Svg.path
path : List (Attribute msg) -> List (Svg msg) -> Svg msg

 ここで使用している属性関数の型です。

Svg.Attributes
stroke : String -> Attribute msg
strokeWidth : String -> Attribute msg
fill : String -> Attribute msg

3.index.html

一応、index.htmlのコードもリストしておきます。

index.html
<!doctype html>
<html>
    <head>
    </head>
    <body>
        <div id="elm-area"></div>
        <script src="elm.js"></script>
        <script>
            Elm.Line.embed(document.getElementById("elm-area"));
        </script>
    </body>
</html>

 今回は以上で終わりです。