{-|
Module : Text.DocL
Copyright : 2015 Anders Jellinggaard
License : MIT
Maintainer : anders.jel@gmail.com
Stability : experimental
Haskell mini-language to create HTML documents with a mixture of markup and
plots based on the <http://c3js.org/ C3.js> library.
See the <https://github.com/andersjel/haskell-text-and-plots github page> for an
introduction.
Notes:
* The generated documents relies on <https://cdnjs.com/> to serve
<http://c3js.org/ C3.js> and friends.
* This library should not be used on a public facing server with input supplied
by the user. The library has not been properly vetted for security issues.
-}
module Text.DocL (
Doc,
-- * Markup
text, header, markdown, html,
-- * Output
render, renderToFile,
-- * Plotting
Column, col, col',
plot, plot', rawPlot,
-- * Utilities
linspace,
-- * Re-exports from "Text.DocL.Javascript"
(/:), javascript
) where
import Data.Foldable
import Data.HashMap.Strict (insert, unionWith)
import Data.Monoid
import Data.Sequence (Seq, singleton)
import Data.String (IsString (..))
import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
import Text.Blaze.Html5 ((!))
import Text.DocL.Javascript
import qualified Data.ByteString.Lazy as B
import qualified System.IO as IO
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
import qualified Text.Markdown as Markdown
data Node = Chart [Prop] | Html H.Html
-- | Base type representing a document. This type allows composition via the
-- 'Monoid' instance.
newtype Doc = Doc (Seq Node) deriving Monoid
-- | Represent a column of a dataset where each row has type /a/. See 'col' for
-- details.
data Column a = Column
{ _header :: String
, _extract :: a -> Obj
}
-- | Polymorphic version of 'col'. This allows, for instance, 'Int' and 'String'
-- values to be used in plots. It is up to the caller to ensure that the values
-- make sense to <http://c3js.org/ C3.js>.
col' :: ToObj b => String -> (a -> b) -> Column a
col' h f = Column h (toObj . f)
-- | Data sets are supplied to 'plot' as a collection of elements of type /a/.
-- This function sets up a 'Column', which knows how to extract one 'Double'
-- value from each value of type /a/ in the collection.
--
-- The first argument to 'col' is the header of the column, which is used in the
-- legend of the plot.
--
-- The simplest plot that can be created consist of one column for the /x/
-- values and one column for the /y/ values.
--
-- @
-- -- Plot x² vs x.
-- plot [1..10] (col "x" id) [col "x²" $ \\x -> x*x]
-- @
col :: String -> (a -> Double) -> Column a
col = col'
-- | > plot ds x ys
--
-- Plots the data in /ds/ using the column /x/ for the values on the /x/-axis
-- and with one line on the plot for each column in /ys/. See also 'col'.
plot :: Foldable f => f a -> Column a -> [Column a] -> Doc
plot d x ys = plot' d x ys []
-- | Same as 'plot', but takes a final argument which is merged with the
-- configuration object supplied to <http://c3js.org/ C3.js>. This allows the
-- caller to customize the plot.
--
-- @
-- -- Plot x² vs x with the points hidden.
-- plot' [1..10] (col "x" id) [col "x²" $ \\x -> x*x] $
-- ["point" //: ["show" //: False]]
-- @
--
-- See <http://c3js.org/reference.html> for the many properties that can be used
-- here.
plot' :: Foldable f => f a -> Column a -> [Column a] -> [Prop] -> Doc
plot' d x ys options = rawPlot $ obj ++ options
where
obj =
[ "data" /:
[ "rows" /: (toObj (_header x : map _header ys) : map f (toList d))
, "x" /: _header x
]
]
f p = toObj $ map (`_extract` p) (x:ys)
-- | This function sends an object with the supplied properties directly to
-- <http://c3js.org/ C3.js>. This is the do-it-yourself option which exposes all
-- of <http://c3js.org/ C3.js>. The object receives a
-- <http://c3js.org/reference.html#bindto "bindto"> property,
-- targeting a @\<div\>@ tag placed appropriately.
rawPlot :: [Prop] -> Doc
rawPlot = Doc . singleton . Chart
-- | Creates a 'Doc' representing raw html.
html :: H.Html -> Doc
html = Doc . singleton . Html
-- | Creates a 'Doc' representing plain text. The string will be properly
-- escaped.
text :: String -> Doc
text = html . H.p . H.toMarkup
-- | Creates a 'Doc' representing @\<h1\>@ header tag. The string will be
-- properly escaped.
header :: String -> Doc
header = html . H.h1 . H.toMarkup
-- | Converts a string in
-- <http://en.wikipedia.org/wiki/Markdown markdown syntax> to a 'Doc'.
markdown :: String -> Doc
markdown = html . Markdown.markdown Markdown.def . fromString
-- | Render a document to a lazy 'B.ByteString'.
render :: Doc -> B.ByteString
render (Doc doc) = renderHtml html
where
dns = "https://cdnjs.cloudflare.com/ajax/libs/"
html = H.docTypeHtml $ do
H.head $ do
H.link ! A.rel "stylesheet" ! A.href (dns <> "c3/0.4.9/c3.min.css")
H.script ! A.charset "utf-8" ! A.src (dns <> "d3/3.5.3/d3.min.js") $
mempty
H.script ! A.charset "utf-8" ! A.src (dns <> "c3/0.4.9/c3.min.js") $
mempty
H.body $
foldMap f (zip [(0::Int)..] $ toList doc)
f (_, (Html t)) = t
f (i, (Chart props)) = divTag <> scriptTag
where
name = "plot" <> show i
divTag = H.div ! A.id (fromString name) $ mempty
obj = props ++ ["bindto" /: ("#" ++ name)]
js = encode $ toObj obj
scriptTag = H.script ! A.type_ "text/javascript" $
H.preEscapedToMarkup $ "\nc3.generate(" <> js <> ");\n"
-- | If the file exists, it will be overwritten.
renderToFile :: FilePath -> Doc -> IO ()
renderToFile p = B.writeFile p . render
-- | /'linspace' x y n/ generates /n/ evenly spaced values from /x/ to /y/.
linspace :: Fractional a => a -> a -> Int -> [a]
linspace x y n = [x + (fromIntegral i)*d | i <- [0 .. n - 1]]
where d = (y - x) / (fromIntegral n - 1)