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>
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の全ソースを示します。
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 : 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 : List ( Float, Float ) -> Svg.Attribute Msg
line model =
List.map transformToLineData model
|> Shape.line Shape.monotoneInXCurve
|> d
以下のline関数は、(x,y)ポイントのリストからline(パスデータ文字列)を生成します。一般的に、SVGではd属性に曲線を引く際のパスコマンドのリスト(パスデータ文字列)を指定します。
line : (Curve -> List PathSegment) -> List (Maybe Point) -> String
line関数の第一引数はcurve関数を指定します。
monotoneInXCurve : Curve -> List PathSegment
d関数は引数のパスデータ文字列を持つd属性を作ります。
d : String -> Attribute msg
以下のviewSvgがSVGを生成している箇所ですが、冒頭に示したSVGにきれいに対応しています。この辺の宣言的記述のわかりやすさが、Elm言語(関数型言語)の素晴らしさだと思います。
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軸を以下のように定義します。
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属性にパスデータ文字列を指定したものでした。
[ Svg.path [ line model, stroke "red", strokeWidth "3px", fill "none" ] []
path関数の型です。
path : List (Attribute msg) -> List (Svg msg) -> Svg msg
ここで使用している属性関数の型です。
stroke : String -> Attribute msg
strokeWidth : String -> Attribute msg
fill : String -> Attribute msg
#3.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>
今回は以上で終わりです。
#付録:D3.js関連の過去記事
D3.jsで気温グラフを描く - Flask + PostgreSQL
D3.jsで日本地図を描くときの基本(geojson)
D3.jsで埼玉県地図を描くときの基本(topojson)
D3.jsで埼玉県の地図上に市町村ラベルを描く
React+D3.jsアプリ作成の基本
埼玉県の市町村別人口をD3.jsのツリーマップで表現してみる
D3.jsの enter-updata-exit パタンでLive Data表示