Home

Awesome

Embedding an elm-vega plot in an elm application

The current documentation for elm-vega instructs the user to attach Vega-Lite to a static div defined an html file. This example illustrates one approach to embedding an elm-vega visualization in a node defined in an elm application.

Base Application

I started with the Random Example from the elm guide and make the following changes.

Embedding an elm-vega histogram.

The first step in embedding the histogram is setting up the Vega spec. We will use the following function to set up the spec, which sets model.rolls as a data column labeled X, then sets of the proper encoding and specification.

-- Vega Spec

spec model =
    let

        d = dataFromColumns []
            << dataColumn "X" (nums (List.map toFloat model.rolls))

        enc =
            encoding
                << position X [ pName "X", pMType Quantitative, pBin [] ]
                << position Y [ pAggregate Count, pMType Quantitative]
    in
    toVegaLite
        [ d []
        , bar []
        , enc []
        ]

The port, named elmToJs, is set up as follows.

-- send spec to vega
port elmToJS : Spec -> Cmd msg

Next, we need to make a port that will be used to pass this spec to the vega-lite runtime. First, change the module to a port module by changing the first line of the EmbedVega.elm file as follows

port module EmbedVega exposing(elmToJS)

To make use of this port, we need to add a command to the update function. This can be accomplished while updating a NewRoll by passing the updated model through spec and elmToJs., as shown below.

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Roll ->
      (model, Random.generate NewFace (Random.int 1 6))

    NewFace newFace ->
        let
            newModel = 
                model
                |> updateFace newFace
                |> updateRolls newFace
        in
            (newModel, elmToJS (spec newModel))


updateFace face model =
    {model | dieFace = Just face}

updateRolls face model =
    {model | rolls = face :: model.rolls}

Finally, we need to create a labeled div in the view which will be used to attach the histogram. We give this div the id of "vis", which will be referenced in the associated html file.

view : Model -> Html Msg
view model =
  div []
    [ showCurrentRoll model
    , display "20 Rolls" model.rolls
    , button [ onClick Roll ] [ Html.text "Roll" ]
    , div [id "vis"][]
    ]

showCurrentRoll model =
    case model.dieFace of
        Just i ->
            h1 [] [ Html.text (toString i) ]
        Nothing ->
            h1 [] [ Html.text "Click Roll to roll the die"]

This program will be compile to main.js using

elm-make src/EmbedVega.elm --output main.js

Setting up index.html

Following the advice found in an answer to this Stack Overflow question, I was able to get this all working using the following set up in html.

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Embed Vega</title>

  </head>

    <!-- These scripts link to the Vega-Lite runtime -->
  <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>
  <script src="main.js"></script>

</head>
<body>

<div id="app"></div>
<script>
    var node = document.getElementById('app');
    var app = Elm.EmbedVega.embed(node);

    var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;

  let updateChart = function(spec){
    console.log(spec);
    requestAnimationFrame(function(){
      vegaEmbed("#vis", spec, {actions: false}).catch(console.warn);
      });
   }

    app.ports.elmToJS.subscribe(updateChart);
</script>
    
</body>
</html>

The key things to note are

  1. The scripts that load vega-lite and main.js (the file created in the last step).
  2. requestAnimationFrame is used to sync the elm creation of the "vis" element and the subsequent manipulation of this div by vega-lite.
  3. We pass "#vis" to vegaEmbed to embed the visualization in the correct location.