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.
I started with the Random Example from the elm guide and make the following changes.
- Make
dieFace
aMaybe Int
. - Add a
List(Int)
to the model, which will be used to capture the roll history. - Refactor the update function using extensible records.
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
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
- The scripts that load
vega-lite
andmain.js
(the file created in the last step). requestAnimationFrame
is used to sync the elm creation of the"vis"
element and the subsequent manipulation of thisdiv
byvega-lite
.- We pass
"#vis"
tovegaEmbed
to embed the visualization in the correct location.