{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-| Module : IHaskell.Display.Hvega Copyright : (c) Douglas Burke 2018, 2019, 2020, 2021 License : BSD3 Maintainer : dburke.gw@gmail.com Stability : unstable Portability : CPP, OverloadedStrings Allow VegaLite visualizations to be displayed directly in Jupyter notebooks or Jupyter Lab. For the moment they are handled separately, in that the Jupyter Lab version requires use of the `vlShow` routine. Jupyter Lab can be installed with nix using the <https://github.com/tweag/jupyterWith jupyterWith> setup. == Example If we have the following Vega-Lite definition: @ \{\-\# language OverloadedStrings \#\-\} import Graphics.Vega.VegaLite vl1 = 'Graphics.Vega.VegaLite.toVegaLite' ['Graphics.Vega.VegaLite.description' desc, 'Graphics.Vega.VegaLite.background' "white", dat [], 'Graphics.Vega.VegaLite.mark' 'Graphics.Vega.VegaLite.Bar' barOpts, enc []] where desc = "A very exciting bar chart" dat = 'Graphics.Vega.VegaLite.dataFromRows' ['Graphics.Vega.VegaLite.Parse' [("start", 'Graphics.Vega.VegaLite.FoDate' "%Y-%m-%d")]] . 'Graphics.Vega.VegaLite.dataRow' [("start", 'Graphics.Vega.VegaLite.Str' "2011-03-25"), ("count", 'Graphics.Vega.VegaLite.Number' 23)] . dataRow [("start", Str "2011-04-02"), ("count", Number 45)] . dataRow [("start", Str "2011-04-12"), ("count", Number 3)] barOpts = ['Graphics.Vega.VegaLite.MOpacity' 0.4, 'Graphics.Vega.VegaLite.MColor' "teal"] enc = 'Graphics.Vega.VegaLite.encoding' . 'Graphics.Vega.VegaLite.position' 'Graphics.Vega.VegaLite.X' ['Graphics.Vega.VegaLite.PName' "start", 'Graphics.Vega.VegaLite.PmType' 'Graphics.Vega.VegaLite.Temporal', 'Graphics.Vega.VegaLite.PAxis' ['Graphics.Vega.VegaLite.AxTitle' "Inception date"]] . position Y [PName "count", PmType Quantitative] @ then it can be displayed automatically in Jupyter Lab by > vlShow vl1 where 'vlShow' should be imported automatically by IHaskell. -} module IHaskell.Display.Hvega (vlShow, VegaLiteLab) where import qualified Data.Text.Lazy as LT import Data.Aeson.Text (encodeToLazyText) #if !(MIN_VERSION_base(4, 12, 0)) import Data.Monoid ((<>)) #endif import Graphics.Vega.VegaLite (VegaLite, fromVL) import IHaskell.Display (IHaskellDisplay(..), Display(..) , javascript, vegalite) -- ^ View a Vega-Lite visualization in a Jupyter *notebook*. Use 'vlShow' -- instead if you are using Jupyter *lab*. -- -- There is currently no way to pass -- <https://github.com/vega/vega-embed#options options> -- to the @vegaEmbed@ call. -- -- Note that local file access is __not__ guaranteed to work - e.g. -- 'Graphics.Vega.VegaLite.dataFromUrl' where the file name refers to a local file - -- since the JavaScript @fs@ module may not be loaded. -- instance IHaskellDisplay VegaLite where display :: VegaLite -> IO Display display VegaLite vl = let -- does https://github.com/vega/vega-embed/issues/8 help? config :: String config = String "requirejs.config({" forall a. Semigroup a => a -> a -> a <> String "baseUrl: 'https://cdn.jsdelivr.net/npm/'," forall a. Semigroup a => a -> a -> a <> String "paths: {" forall a. Semigroup a => a -> a -> a <> String "'vega-embed': 'vega-embed@6?noext'," forall a. Semigroup a => a -> a -> a <> String "'vega-lib': 'vega-lib?noext'," forall a. Semigroup a => a -> a -> a <> String "'vega-lite': 'vega-lite@4?noext'," forall a. Semigroup a => a -> a -> a <> String "'vega': 'vega@5?noext'" forall a. Semigroup a => a -> a -> a <> String "}});" -- rely on the element variable being set up; it appears to be an array -- so use the first element to add the div to. makeDiv :: String makeDiv = String "var ndiv = document.createElement('div');" forall a. Semigroup a => a -> a -> a <> String "ndiv.innerHTML = " forall a. Semigroup a => a -> a -> a <> String "'Awesome Vega-Lite visualization to appear here';" forall a. Semigroup a => a -> a -> a <> String "element[0].appendChild(ndiv);" js :: String js = Text -> String LT.unpack (forall a. ToJSON a => a -> Text encodeToLazyText (VegaLite -> Value fromVL VegaLite vl)) -- Use the div element we have just created for the plot. -- More options could be passed to vegaEmbed. -- plot :: String plot = String "require(['vega-embed'],function(vegaEmbed){" forall a. Semigroup a => a -> a -> a <> String "vegaEmbed(ndiv," forall a. Semigroup a => a -> a -> a <> String js forall a. Semigroup a => a -> a -> a <> String ").then(" forall a. Semigroup a => a -> a -> a <> String "function (result) { console.log(result); }).catch(" forall a. Semigroup a => a -> a -> a <> String "function (error) { ndiv.innerHTML = " forall a. Semigroup a => a -> a -> a <> String "'There was an error: ' + error; });" forall a. Semigroup a => a -> a -> a <> String "});" ds :: [DisplayData] ds = [String -> DisplayData javascript (String config forall a. Semigroup a => a -> a -> a <> String makeDiv forall a. Semigroup a => a -> a -> a <> String plot)] in forall (f :: * -> *) a. Applicative f => a -> f a pure ([DisplayData] -> Display Display [DisplayData] ds) -- | A wrapper around 'VegaLite' so that we can write a 'Display' -- instance for JupyterLab and not end up with "overlapping -- instances". -- -- Is there a better way to do this (other than drop support for -- Jupyter notebook users)? -- newtype VegaLiteLab = VLL VegaLite -- | Convert a VegaLite visualization so that it can be auto-displayed -- in Jupyter Lab. -- vlShow :: VegaLite -> VegaLiteLab vlShow :: VegaLite -> VegaLiteLab vlShow = VegaLite -> VegaLiteLab VLL -- ^ Display Vega-Lite visualizations in an IHaskell notebook when -- using Jupyter Lab. -- -- Use the @IHaskell.Display.Hvega@ module when using IHaskell from -- a Jupyter notebook. -- -- Note that local file access is __not__ guaranteed to work - e.g. -- 'Graphics.Vega.VegaLite.dataFromUrl' where the file name refers to a local file - -- since the JavaScript @fs@ module may not be loaded. -- -- It would be nice to create a PNG version for non-browser viewers, -- but I am not sure how to get Jupyter to do this (and if it would -- even do what I hope it does). -- instance IHaskellDisplay VegaLiteLab where display :: VegaLiteLab -> IO Display display (VLL VegaLite vl) = let js :: String js = Text -> String LT.unpack (forall a. ToJSON a => a -> Text encodeToLazyText (VegaLite -> Value fromVL VegaLite vl)) in forall (f :: * -> *) a. Applicative f => a -> f a pure ([DisplayData] -> Display Display [String -> DisplayData vegalite String js])