{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Vega Lite Examples in Haskell\n", "\n", "The aim of this notebook is to use \n", "[`hvega`](http://hackage.haskell.org/package/hvega) - which is a Haskell\n", "port of the \n", "[Elm Vega](http://package.elm-lang.org/packages/gicentre/elm-vega/2.2.1) \n", "module (note, this is a link to an old version) - to describe\n", "the [Vega-Lite Gallery](https://vega.github.io/vega-lite/examples/)\n", "in an IHaskell notebook. I used ot have a single notebook with all the\n", "examples in it, but I have since split it out into multiple\n", "notebooks, called `VegaLiteGallery-XXXX.ipynb`. This notebook remains\n", "to explain how to load `hvega` and describes how the examples are\n", "created.\n", "\n", "## Jupyter Lab versus Notebook\n", "\n", "Jupyter Lab - which can be run using\n", "[Tweag's jupyterWith environment](https://github.com/tweag/jupyterWith) -\n", "has native support for Vega and Vega-Lite visualizations, and so is\n", "the *preferred* medium for `hvega`. This notebook was run using \n", "Jupter Lab, with the `shell.nix` file and the command\n", "\n", " nix-shell --command \"jupyter notebook\"\n", "\n", "When run using Jupyter notebooks, the visualizations are displayed\n", "using the VegaEmbed JavaScript library, and do not provide a PNG\n", "version for display outside of the browser (e.g. PDF or the various\n", "ipynb viewers).\n", "\n", "Unfortunately (as of version 0.3 of `ihaskell-hvega`), the support for\n", "`jupyter lab` is disappointing (due to a combination of IHaskell,\n", "jupyterWith, and the version 1.2 to 2 chanhes in Jupyter lab), so I\n", "am switching back to the notebook version for now.\n", "\n", "## Versions\n", "\n", "Let's check what version of [`hvega`](https://hackage.haskell.org/package/hvega), and\n", "other useful packages, were last used.\n", "\n", "Note that `hvega` supports version 4 of the Vega-Lite specification. Hopefully it is (mostly) forwards-compatible!" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "ihaskell-0.10.2.0" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "ghc-8.10.4" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "hvega-0.11.0.1" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "ihaskell-hvega-0.3.2.0" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ ":! ghc-pkg latest ihaskell\n", ":! ghc-pkg latest ghc\n", ":! ghc-pkg latest hvega\n", ":! ghc-pkg latest ihaskell-hvega" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Let's load up Vega-Lite\n", "\n", "Since many of the strings used in `hvega` are actually `Data.Text.Text`,\n", "we turn on `OverloadedStrings`." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ ":ext OverloadedStrings" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "-- VegaLite uses these names\n", "import Prelude hiding (filter, lookup, repeat)\n", "\n", "import Graphics.Vega.VegaLite\n", "\n", "-- IHaskell automatically imports this if the `ihaskell-vega` module is installed\n", "-- import IHaskell.Display.Hvega" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following imports are only to show the JSON representation of a Vega-Lite\n", "visualization, and are not needed in \"general\" use of `hvega`." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "import Data.Aeson.Encode.Pretty (encodePretty)\n", "import Data.ByteString.Lazy.Char8 as BL8 hiding (filter, map, repeat)\n", "\n", "-- Allow the VegaLite specification to be pretty-printed\n", "ppSpec = BL8.putStrLn . encodePretty . fromVL\n", "\n", "-- If you are viewing this in an IHaskell notebook rather than Jupyter Lab,\n", "-- use the following to see the visualizations (we don't actually need a\n", "-- function, but this is left in to make it easier to switch between\n", "-- lab and notebook interfaces).\n", "--\n", "vlShow = id" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As of version 0.3, there are now `toHtml` and `toHtmlFile` routines which create\n", "a HTML representation of the visualization - using Vega Embed as the viewer (this is\n", "similar to how the visualizations for notebooks is handled by `ihaskell-vega`)." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/html": [ "toHtml :: VegaLite -> Text" ], "text/plain": [ "toHtml :: VegaLite -> Text" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ ":type toHtml" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Simple Bar Chart\n", "\n", "From https://vega.github.io/vega-lite/examples/bar.html\n", "\n", "```\n", "{\n", " \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.json\",\n", " \"description\": \"A simple bar chart with embedded data.\",\n", " \"data\": {\n", " \"values\": [\n", " {\"a\": \"A\", \"b\": 28}, {\"a\": \"B\", \"b\": 55}, {\"a\": \"C\", \"b\": 43},\n", " {\"a\": \"D\", \"b\": 91}, {\"a\": \"E\", \"b\": 81}, {\"a\": \"F\", \"b\": 53},\n", " {\"a\": \"G\", \"b\": 19}, {\"a\": \"H\", \"b\": 87}, {\"a\": \"I\", \"b\": 52}\n", " ]\n", " },\n", " \"mark\": \"bar\",\n", " \"encoding\": {\n", " \"x\": {\"field\": \"a\", \"type\": \"nominal\", \"axis\": {\"labelAngle\": 0}},\n", " \"y\": {\"field\": \"b\", \"type\": \"quantitative\"}\n", " }\n", "}\n", "```" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "barFromColumns :: VegaLite\n", "barFromColumns = \n", " let desc = description \"A simple bar chart with embedded data.\"\n", " dvals = dataFromColumns []\n", " . dataColumn \"a\" (Strings [\"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\", \"H\", \"I\"])\n", " . dataColumn \"b\" (Numbers [28, 55, 43, 91, 81, 53, 19, 87, 52])\n", " \n", " enc = encoding\n", " . position X [PName \"a\", PmType Nominal, PAxis [AxLabelAngle 0]]\n", " . position Y [PName \"b\", PmType Quantitative]\n", "\n", " in toVegaLite [desc, dvals [], mark Bar [], enc []]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The specification can be displayed, since it is \"just\" JSON:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{\n", " \"mark\": \"bar\",\n", " \"data\": {\n", " \"values\": [\n", " {\n", " \"a\": \"A\",\n", " \"b\": 28\n", " },\n", " {\n", " \"a\": \"B\",\n", " \"b\": 55\n", " },\n", " {\n", " \"a\": \"C\",\n", " \"b\": 43\n", " },\n", " {\n", " \"a\": \"D\",\n", " \"b\": 91\n", " },\n", " {\n", " \"a\": \"E\",\n", " \"b\": 81\n", " },\n", " {\n", " \"a\": \"F\",\n", " \"b\": 53\n", " },\n", " {\n", " \"a\": \"G\",\n", " \"b\": 19\n", " },\n", " {\n", " \"a\": \"H\",\n", " \"b\": 87\n", " },\n", " {\n", " \"a\": \"I\",\n", " \"b\": 52\n", " }\n", " ]\n", " },\n", " \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.json\",\n", " \"encoding\": {\n", " \"x\": {\n", " \"field\": \"a\",\n", " \"type\": \"nominal\",\n", " \"axis\": {\n", " \"labelAngle\": 0\n", " }\n", " },\n", " \"y\": {\n", " \"field\": \"b\",\n", " \"type\": \"quantitative\"\n", " }\n", " },\n", " \"description\": \"A simple bar chart with embedded data.\"\n", "}" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "ppSpec barFromColumns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `toHtml` function creates the following HTML \"snippet\" from the visualization (`toHtmlFile` will write it to a file). These functions were added in version `0.2.1.0`." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "\"\\n\\n\\n \\n \\n \\n\\n\\n
\\n\\n\\n\\n\"" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "toHtml barFromColumns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now use Vega-Embed to view the visualization directly.\n", "\n", "This should be viewable as a PNG when viewing with a non-Javascript enabled viewer, at least for notebooks processed by Jupyter Lab, but appears to fail with IHaskell notebooks." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "scrolled": true }, "outputs": [ { "data": { "application/javascript": [ "requirejs.config({baseUrl: 'https://cdn.jsdelivr.net/npm/',paths: {'vega-embed': 'vega-embed@6?noext','vega-lib': 'vega-lib?noext','vega-lite': 'vega-lite@4?noext','vega': 'vega@5?noext'}});var ndiv = document.createElement('div');ndiv.innerHTML = 'Awesome Vega-Lite visualization to appear here';element[0].appendChild(ndiv);require(['vega-embed'],function(vegaEmbed){vegaEmbed(ndiv,{\"mark\":\"bar\",\"data\":{\"values\":[{\"a\":\"A\",\"b\":28},{\"a\":\"B\",\"b\":55},{\"a\":\"C\",\"b\":43},{\"a\":\"D\",\"b\":91},{\"a\":\"E\",\"b\":81},{\"a\":\"F\",\"b\":53},{\"a\":\"G\",\"b\":19},{\"a\":\"H\",\"b\":87},{\"a\":\"I\",\"b\":52}]},\"$schema\":\"https://vega.github.io/schema/vega-lite/v4.json\",\"encoding\":{\"x\":{\"field\":\"a\",\"type\":\"nominal\",\"axis\":{\"labelAngle\":0}},\"y\":{\"field\":\"b\",\"type\":\"quantitative\"}},\"description\":\"A simple bar chart with embedded data.\"}).then(function (result) { console.log(result); }).catch(function (error) { ndiv.innerHTML = 'There was an error: ' + error; });});" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "vlShow barFromColumns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The data can also be created using `dataFromRows`, as shown below:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "barFromRows = \n", " let desc = description \"A simple bar chart with embedded data.\"\n", " dvals = dataFromRows []\n", " . dataRow [(\"a\", Str \"A\"), (\"b\", Number 28)]\n", " . dataRow [(\"a\", Str \"B\"), (\"b\", Number 55)]\n", " . dataRow [(\"a\", Str \"C\"), (\"b\", Number 43)]\n", " . dataRow [(\"a\", Str \"D\"), (\"b\", Number 91)]\n", " . dataRow [(\"a\", Str \"E\"), (\"b\", Number 81)]\n", " . dataRow [(\"a\", Str \"F\"), (\"b\", Number 53)]\n", " . dataRow [(\"a\", Str \"G\"), (\"b\", Number 19)]\n", " . dataRow [(\"a\", Str \"H\"), (\"b\", Number 87)]\n", " . dataRow [(\"a\", Str \"I\"), (\"b\", Number 52)]\n", "\n", " enc = encoding\n", " . position X [PName \"a\", PmType Nominal, PAxis [AxLabelAngle 0]]\n", " . position Y [PName \"b\", PmType Quantitative]\n", "\n", " in toVegaLite [desc, dvals [], mark Bar [], enc []]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Are the two sepcifications correct (there is currently no `Eq` instance defined for the `VegaLite`\n", "type so the underlying JSON representation is extracted using `fromVL`)?" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fromVL barFromColumns == fromVL barFromRows" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "An important piece of information is the expected specification, given on the Vega-Lite example page. I use the\n", "Aeson Template-Haskell support to embed the specification as a Haskell value (I have taken to using the\n", "suffix `Spec` for the variable name). So for this example we have" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ ":ext QuasiQuotes" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "import Data.Aeson.QQ.Simple (aesonQQ)\n", "\n", "barSpec = [aesonQQ|\n", "{\n", " \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.json\",\n", " \"description\": \"A simple bar chart with embedded data.\",\n", " \"data\": {\n", " \"values\": [\n", " {\"a\": \"A\", \"b\": 28}, {\"a\": \"B\", \"b\": 55}, {\"a\": \"C\", \"b\": 43},\n", " {\"a\": \"D\", \"b\": 91}, {\"a\": \"E\", \"b\": 81}, {\"a\": \"F\", \"b\": 53},\n", " {\"a\": \"G\", \"b\": 19}, {\"a\": \"H\", \"b\": 87}, {\"a\": \"I\", \"b\": 52}\n", " ]\n", " },\n", " \"mark\": \"bar\",\n", " \"encoding\": {\n", " \"x\": {\"field\": \"a\", \"type\": \"nominal\", \"axis\": {\"labelAngle\": 0}},\n", " \"y\": {\"field\": \"b\", \"type\": \"quantitative\"}\n", " }\n", "}\n", "|]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This can then be compared to the output of `toVegaLite`:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fromVL barFromRows == barSpec" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the gallery example I use a more-complicated validation function, defined in each notebook\n", "and cleverly named `validate`, which will report - in a hopefully-readable\n", "manner - any differences between the expected and created specification. There are several differences why there may be a difference,\n", "such as\n", "\n", " - at the time I created the example, the gallery was using an old (pre version 3) version of the Vega-Lite specification\n", " (whereas now the examples are using a newer version, but I haven't updated them)\n", " \n", " - Vega-Lite is a \"flexible\" schema, in that there's often more-than-one-way to define the same thing (for example,\n", " setting a value to `\"\"` or `null` can often mean the same thing, and some settings are optional\n", " \n", " - `hvega` doesn't support one form of creating a visualization (normally a simpler, short cut) but you can create\n", " the same visualization\n", " \n", " - actual bugs or missing functionality in `hvega`: [issues welcome](https://github.com/DougBurke/hvega/issues)\n", " \n", "---\n", "\n", "If you are looking for the actual notebooks, then look for\n", "\n", " - `VegaLiteGallery-SingleViewPlots.ipynb` [nbviewer link](https://nbviewer.jupyter.org/github/DougBurke/hvega/blob/master/notebooks/VegaLiteGallery-SingleViewPlots.ipynb)\n", " \n", " - `VegaLiteGallery-CompositeMark.ipynb` [nbviewer link](https://nbviewer.jupyter.org/github/DougBurke/hvega/blob/master/notebooks/VegaLiteGallery-CompositeMark.ipynb)\n", " \n", " - `VegaLiteGallery-LayeredPlots.ipynb` [nbviewer link](https://nbviewer.jupyter.org/github/DougBurke/hvega/blob/master/notebooks/VegaLiteGallery-LayeredPlots.ipynb)\n", " \n", " - `VegaLiteGallery-MultiView.ipynb` [nbviewer link](https://nbviewer.jupyter.org/github/DougBurke/hvega/blob/master/notebooks/VegaLiteGallery-MultiView.ipynb)\n", " \n", " - `VegaLiteGallery-Maps.ipynb` [nbviewer link](https://nbviewer.jupyter.org/github/DougBurke/hvega/blob/master/notebooks/VegaLiteGallery-Maps.ipynb)\n", " \n", " - `VegaLiteGallery-Interactive.ipynb` [nbviewer link](https://nbviewer.jupyter.org/github/DougBurke/hvega/blob/master/notebooks/VegaLiteGallery-Interactive.ipynb)\n", " \n", "---" ] } ], "metadata": { "kernelspec": { "display_name": "Haskell", "language": "haskell", "name": "haskell" }, "language_info": { "codemirror_mode": "ihaskell", "file_extension": ".hs", "mimetype": "text/x-haskell", "name": "haskell", "pygments_lexer": "Haskell", "version": "8.10.4" } }, "nbformat": 4, "nbformat_minor": 4 }