A common pattern when using diagrams is to make a program that generates a
single diagram. Often there are details about the diagram that we would like
to delay in fixing. In particular the output file, image format, and final
size of the diagram are all standard options we would expect for making a
diagram that are relatively independent of the image being made. With the
Diagrams.Backend.SVG.CmdLine module (and related modules in other
backends) we provide easy creation of a command-line interface that supports
standard options as well as easy customization for additional parameters.
To give some concrete examples, the following are programs one can write using the tools already provided, and their interaction on the command-line. For an example of a custom extension to this framework, see the User Extensions section below.
First is the simplest case of generating a single diagram:
> -- Simple
>
> d :: Diagram SVG V2 Double
> d = ...
>
> main = mainWith dHere we just have a diagram and the standard options. We can invoke with just a width and the height is made to match the diagram.
$ ./Simple -o simple.svg -w 100If we have multiple diagrams with names we can use mainWith to give an
interface that allows the selection of a particular diagram by name.
> -- Multiple
>
> d1, d2, d3 :: Diagram SVG V2 Double
> ...
>
> main = mainWith [("First", d1),("Second", d2),("Third", d3)]The --list option just lists the available diagrams to render and
the -S selection option takes a name and renders the associated
diagram with the standard options.
$ ./Multiple --list
Available diagrams:
First Second Third
$ ./Multiple -o d1.svg -w 100 -S FirstSome backends support rendering animations (typically as individually indexed files of frames).
> -- Animation
>
> a :: Animation Cairo V2 Double
> a = ...
>
> main = mainWith aThe default options are expected, but the output file has an index appended
to the name for each frame. The --fpu option indicates the desired number
of frames per unit of time. If the a animation above is one second long
then the following will create files a01.png through a24.png.
$ ./Animation -o a.png -w 100 --fpu 24In backends that support multiple pages we can list all the diagrams and have each render on its own page.
> -- Pages
>
> d1, d2, d3 :: Diagram Postscript V2 Double
> ...
>
> main = mainWith [d1,d2,d3]We only need the default options here and the interface is the same as a single diagram.
$ ./Pages -o pages.ps -w 400To make things more interesting we could require additional arguments to build a diagram. We can take a function to build a diagram from some parameters and create an interface that fills those parameters with arguments from the command-line.
> -- Function
>
> f :: Colour Double -> Double -> Diagram SVG V2 Double
> f c x = ...
>
> main = mainWith fIn addition to the standard flags, we can provide arguments blue and 42.0 which
will be passed along to f.
$ ./Function -o blue.svg -w 400 blue 42.0In addition to Colours and Doubles, default command-line parsers are
provided for Int, String, and AlphaColour arguments. You can
also easily define your own parsers for additional argument types; see
the User Extensions section below.
Diagrams that are the result of executing an IO action can also be handled
by mainWith. This can be useful for reading input files or doing other
IO that the diagram depends on.
> -- IO-diagram
>
> d :: FilePath -> IO (Diagram SVG V2 Double)
> d file = do
> f <- readFile file
> ...
>
> main = mainWith dThe program will expect a file name on the command-line which it reads to generate a diagram.
$ ./IO-diagram -o file.svg -w 400 ~/data.logSee the Clock Example section below.
The standard options for diagram creation are found in the
Diagrams.Backend.CmdLine module of diagrams-lib and are
represented with the following record:
> data DiagramOpts = DiagramOpts
> { _width :: Maybe Int
> , _height :: Maybe Int
> , _output :: FilePath
> }This models having an optional width and height and a required FilePath.
We also need to have a parser for command-line arguments that results in a
value of this record. We use the optparse-applicative package for
command-line parsing and the parser for the standard options is the following:
> diagramOpts :: Parser DiagramOpts
> diagramOpts = DiagramOpts
> <$> (optional . option auto)
> ( long "width" <> short 'w'
> <> metavar "WIDTH"
> <> help "Desired WIDTH of the output image")
> <*> (optional . option auto)
> ( long "height" <> short 'h'
> <> metavar "HEIGHT"
> <> help "Desired HEIGHT of the output image")
> <*> strOption
> ( long "output" <> short 'o'
> <> value ""
> <> metavar "OUTPUT"
> <> help "OUTPUT file")This is written in applicative form, Constructor <$> ... <*> ... <*> ...,
where the values we fill in are the parsers for the fields of the record. The
parsers in optparse-applicative take as an argument a collection of
parameters.
This section walks through and motivates the design of the abstraction mechanisms that make possible the examples shown above. If you want to create your own custom command-line-driven diagram generation executables, you will likely find it helpful to understand this section. The truly impatient, however, may wish to skip directly to User Extensions and return to this section as necessary.
What work does the backend need to do to render a diagram? It depends
on the backend, of course, but there are several common tasks given
the standard options. To start with we need to parse the command-line
arguments. The optparse-applicative package provides all the
tools we need for this. Next we will need to translate the standard
arguments into something backend specific. Typically the extension on
the output filename will drive the format of the output and some
combination of the supplied width and height will dictate the final
scale of the diagram. Let's look at a full example of a backend doing
this work and try to see what parts we can abstract out for general
use (we will use the Cairo backend for this example).
> defaultMain :: Diagram Cairo V2 Double -> IO ()
> defaultMain d = do
> prog <- getProgName
> let p = info (helper' <*> diagramOpts)
> ( fullDesc
> <> progDesc "Command-line diagram generation."
> <> header prog)
> opts <- execParser p
> chooseRender opts d
>
> chooseRender :: DiagramOpts -> Diagram Cairo V2 Double -> IO ()
> chooseRender opts d =
> case splitOn "." (output opts) of
> [""] -> putStrLn "No output file given."
> ps | last ps `elem` ["png", "ps", "pdf", "svg"] -> do
> let outTy = case last ps of
> "png" -> PNG
> "ps" -> PS
> "pdf" -> PDF
> "svg" -> SVG
> _ -> PDF
> fst $ renderDia
> Cairo
> ( CairoOptions
> (output opts)
> (mkSizeSpec
> (fromIntegral <$> width opts)
> (fromIntegral <$> height opts)
> )
> outTy
> False
> )
> d
> | otherwise -> putStrLn $ "Unknown file type: " ++ last psThere are several things that make this structuring of the program inflexible.
Let's consider building a main where we accept a function that can produce a
diagram.
> functionMain :: (a -> Diagram Cairo V2 Double) -> IO ()Clearly we cannot use the given function as we have no way to produce an a.
So we provide a type class called Parseable for associating a parser with the
type that it parses:
> class Parseable a where
> parser :: Parser aNow we can make more progress.
> functionMain :: Parseable a => (a -> Diagram Cairo V2 Double) -> IO ()
> functionMain f = do
> prog <- getProgName
> let p = info (helper' <*> ((,) <$> diagramOpts <*> parser))
> ( fullDesc
> <> progDesc "Command-line diagram generation."
> <> header prog)
> (opts,a) <- execParser p
> chooseRender opts (f a)The only parts so far that are backend specific are the type of the final
diagram and chooseRender, though we may want other parts to be subject to
customization. We will split this into four parts, the type of the options
needed, the action of parsing the command-line, the backend specific rendering,
and an entry point for the library consumer. We will give this the brilliant
name Mainable.
> class Mainable d where
> type MainOpts d :: *
>
> mainArgs :: Parseable (MainOpts d) => proxy d -> IO (MainOpts d)
> mainRender :: MainOpts d -> d -> IO ()
> mainWith :: Parseable (MainOpts d) => d -> IO ()There is one associated type and three class methods. Let's consider the
instance of Mainable for a simple diagram with type Diagram Cairo V2 Double:
> instance Mainable (Diagram Cairo V2 Double) whereThe associated type indicates what options we will want to be parsed from the command-line. In this case we will just use the standard options:
> type MainOpts (Diagram Cairo V2 Double) = DiagramOptsThe mainArgs method is nearly what we had before. In this case there isn't
anything backend specific, so instead of an instance implementation we will
show the default implementation for mainArgs. Instead of a specific parser
diagramOpts we have a constraint Parseable (MainOpts d) allowing us to use
parser where we had diagramsOpts. The parser from the constraint is combined with some
additional standard configuration for the program name and the right kind of
help message. Running the mainArgs IO action results in either the program
quitting with a parse error or help message, or the program continuing with the
parsed value. Also note that we need the diagram to be passed to mainArgs
only to fix the type so we can use our associated type function MainOpts.
> mainArgs :: Parseable (MainOpts d) => proxy d -> IO (MainOpts d)
> mainArgs _ = do
> prog <- getProgName
> let p = info (helper' <*> parser)
> ( fullDesc
> <> progDesc "Command-line diagram generation."
> <> header prog)
> execParser pThe next method to implement is the mainRender method. Here we can just use
the chooseRender function we had before, handling all the backend specific
interpretation of the standard arguments.
> mainRender :: DiagramOpts -> Diagram Cairo V2 Double -> IO ()
> mainRender = chooseRenderFinally we have mainWith which joins the previous parts to make an entry point
for users of the backend to build their programs. In this example we take as an
argument the Diagram Cairo V2 Double and result in a complete program. Again, we can
get away with the default implementation.
> mainWith :: Parseable (MainOpts d) => d -> IO ()
> mainWith d = do
> opts <- mainArgs (Identity d)
> mainRender opts dNow let's try a much harder instance. We want to be able to handle
functions whose final result has a Mainable instance, but require
some Parseable arguments first. The tricky part of this instance is
that we need to know up front what all our arguments are going to be
in order to be able to parse them. It sounds tempting to peel off one
argument at a time, parse, apply, and recurse with one less argument;
but this does not work. To facilitate collecting the arguments, we
make a new type class that has associated types for all the
arguments of the type and the final result of the type. It will also
contain a function to perform the application of all the arguments and
give the final result.
> class ToResult d where
> type Args d :: *
> type ResultOf d :: *
>
> toResult :: d -> Args d -> ResultOf dWe will need a base case for when we have reached the final result. It needs
no arguments so we use the unit type for Args and the final result is just
the diagram itself.
> instance ToResult (Diagram b v n) where
> type Args (Diagram b v n) = ()
> type ResultOf (Diagram b v n) = Diagram b v n
>
> toResult d _ = dNow we can write the inductive case of a function resulting in something with
a ToResult instance.
> instance ToResult d => ToResult (a -> d) where
> type Args (a -> d) = (a, Args d)
> type ResultOf (a -> d) = ResultOf d
>
> toResult f (a,args) = toResult (f a) argsHere Args is the product of the argument and any arguments that d demands.
The final result is the final result of d and to produce a result we apply
one argument and recurse to d's ToResult instance.
Now that we have ToResult to work with, we can write the type for the instance
of Mainable that we want:
> instance (Parseable a, Parseable (Args d), ToResult d, Mainable (ResultOf d))
> => Mainable (a -> d) whereThink about this type for a bit.
Now we need a type for MainOpts (a -> d) and at least an implementation for
mainRender. Remember the purpose of MainOpts is to give a type for all
the arguments needed. We will need the MainOpts from the final result and
some structure containing all the function arguments. Note that we rely on
having a Parseable instance for products.
> type MainOpts (a -> d) = (MainOpts (ResultOf (a -> d)), Args (a -> d))Our mainRender will be handed a value of this type and a function of our
instance type. We can use toResult to apply the second part of the pair
to the function and hand the final result off to the final result's Mainable
instance along with its required options.
> mainRender (opts, a) f = mainRender opts (toResult f a)Now we compile and cross our fingers!
We can also handle IO with a couple more instances. First we will need a
ToResult instance that handles IO actions:
> instance ToResult d => ToResult (IO d) where
> type Args (IO d) = Args d
> type ResultOf (IO d) = IO (ResultOf d)
>
> toResult d args = flip toResult args <$> dThis states that the needed arguments are not affected by this being
an IO action and the final result is an IO action producing the final
result of the action's result type. Our Mainable instance can now
be written:
> instance Mainable d => Mainable (IO d) where
> type MainOpts (IO d) = MainOpts d
>
> mainRender opts dio = dio >>= mainRender optsHere we merely perform the diagram creating action and bind its value
to the Mainable instance that can handle it. For an example of using
these instances see the Clock Example section below.
You can easily build on top of this framework to create executables taking your own custom command-line flags. This section walks through a simple example.
Suppose we want to make "flippable" diagrams: a single executable that
can render either a diagram or its mirror image, depending on a
command-line flag. Of course we also want to support all the usual
command-line options like --width, --height, --output, and
so on. The framework described above—together with the
composability of optparse-applicative-based command-line
parsers—makes this very easy to do.
First, some pragmas and imports:
> {-# LANGUAGE FlexibleInstances #-}
> {-# LANGUAGE NoMonomorphismRestriction #-}
> {-# LANGUAGE TypeFamilies #-}
>
> import Diagrams.Backend.CmdLine
> import Diagrams.Backend.SVG.CmdLine
> import Diagrams.Prelude hiding ((<>))
> import Options.Applicative(Unfortunately, Options.Applicative re-exports the (<>) from
Data.Monoid, whereas Diagrams.Prelude re-exports the one
from Data.Semigroup.)
We now create a newtype for "flippable" things:
> newtype Flippable a = Flippable aWe need a newtype since we need to make a Mainable instance which is
different than the default instance for Diagram SVG V2 Double.
We create a data structure to contain our new command-line options,
along with a Parseable instance for it. In this case we just want a
single Bool value, corresponding to a new command-line switch
--flipped along with an appropriate help message
> data FlipOpts = FlipOpts Bool
>
> instance Parseable FlipOpts where
> parser = FlipOpts <$> switch (long "flipped" <> help "Flip the diagram L-R")For help on constructing command-line parsers, see the documentation
for the optparse-applicative package; you can also look at the
source code of Diagrams.Backend.CmdLine for some examples.
Finally, we create a Mainable instance for flippable diagrams. The
MainOpts for flippable diagrams consists of a pair of our new
FlipOpts along with the MainOpts for diagrams. To implement
mainRender, we take in our options and a flippable diagram, and pass
the diagram-specific options along to the mainRender method for
diagrams, flipping the diagram appropriately.
> instance Mainable (Flippable (Diagram SVG V2 Double)) where
> type MainOpts (Flippable (Diagram SVG V2 Double)) = (MainOpts (Diagram SVG V2 Double), FlipOpts)
>
> mainRender (opts, FlipOpts f) (Flippable d) = mainRender opts ((if f then reflectX else id) d)Let's try it out!
> d :: Diagram SVG V2 Double
> d = square 1 # fc red ||| square 1 # fc blue
>
> main = mainWith (Flippable d)Note the --flipped option in the help message:
$ ./Flippable --help
Flippable
Usage: Flippable [-w|--width WIDTH] [-h|--height HEIGHT] [-o|--output OUTPUT] [-l|--loop] [-s|--src ARG] [-i|--interval INTERVAL] [--flipped]
Command-line diagram generation.
Available options:
-?,--help Show this help text
-w,--width WIDTH Desired WIDTH of the output image (default 400)
-h,--height HEIGHT Desired HEIGHT of the output image (default 400)
-o,--output OUTPUT OUTPUT file
-l,--loop Run in a self-recompiling loop
-s,--src ARG Source file to watch
-i,--interval INTERVAL When running in a loop, check for changes every INTERVAL seconds.
--flipped Flip the diagram L-RAnd running it yields:
$ ./Flippable -o Flippable.svg -w 400
$ ./Flippable -o Flippable.svg -w 400 --flipped
It works!
It is also worth noting that for this simple example, we actually did
not need the Flippable wrapper or Mainable instance at all!
Given only the FlipOpts type and its Parseable instance, we can
simply write
> main = mainWith (\(FlipOpts f) -> (if f then reflectX else id) d)which gives us exactly the same program! Indeed, if you squint at
the function instance for Mainable and the instance we wrote for
Flippable, you can see that our instance is a direct specialization
of the more general one.
We may want to build diagrams based on the state of the world. For instance, if we want to build a diagram of a clock we will want to know what time it is. Consider the following program.
> import Diagrams.Prelude
> import Diagrams.Coordinates
> import Data.Time
>
> clock :: UTCTime -> Diagram B
> clock t = circle 0.35 # fc silver # lwG 0
> <> bigHand # f 12 h <> littleHand # f 60 m
> <> circle 1 # fc black # lwG 0
> <> circle 11 # lwG 1.5 # lc slategray # fc lightsteelblue
> where
> s = realToFrac $ utctDayTime t :: Double
> m = s / 60
> h = m / 60
>
> bigHand = (0 ^& (-1.5)) ~~ (0 ^& 7.5) # lwG 0.5
> littleHand = (0 ^& (-2)) ~~ (0 ^& 9.5) # lwG 0.2
> f n v = rotate (- v / n @@ turn)
>
> main = mainWith (clock <$> getCurrentTime)Running we get:

This uses the Mainable d => Mainable (IO d) instance to allow our
effectful clock generator. However, we could have just as well avoided
this instance, writing instead:
> main = do
> t <- getCurrentTime
> mainWith (clock t)This instance is quite convenient, however, especially when we want our IO action to depend on some command-line option. The following exercises should be helpful in gaining practice working with IO and options by modifying the clock example into a useful clock making program.
Modify the example so it can take a time as a command-line option, but if one is not given, it uses the current time.
Modify clock to take a ClockStyle argument that includes options for
various visual styles for the clock. For instance ClockStyle could
include a color for the clock background, a flag for turning on hour
marks, or a flag for including a second hand.