packages feed

barchart (empty) → 0.1

raw patch · 11 files changed

+1411/−0 lines, 11 filesdep +basedep +cmdargsdep +csvsetup-changed

Dependencies added: base, cmdargs, csv, diagrams, filepath

Files

+ LICENSE view
@@ -0,0 +1,32 @@+Copyright (c) 2010, Sebastian Fischer++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++ 1. Redistributions of source code must retain the above copyright+    notice, this list of conditions and the following disclaimer.++ 2. Redistributions in binary form must reproduce the above copyright+    notice, this list of conditions and the following disclaimer in+    the documentation and/or other materials provided with the+    distribution.++ 3. Neither the name of the author nor the names of his contributors+    may be used to endorse or promote products derived from this+    software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+
+ README.markdown view
@@ -0,0 +1,330 @@+barchart is a command-line program for generating bar charts from CSV+files. It has special support for creating charts from data generated+by the Haskell benchmarking tools [criterion] and+[progression]. barchart can create PNG, SVG, PDF, and PS files using+the Haskell [Diagrams] library for rendering and, hence, depends on a+Haskell binding to Cairo which is part of [Gtk2Hs].++# installation++available soon. be patient.++# usage++Bar charts can be created from CSV files using the `barchart`+command-line utility. For example, if you want to track how many hours+you practice playing the guitar on each day of the week, you can+create a file `guitar.csv` with the following contents:++    Mon,1.2+    Tue,0.3+    Wed,2.1+    Thu,0.9+    Fri,1.1+    Sat,3.2+    Sun,3.1++The call++    # barchart --title="Practice hours" guitar.csv ++creates a file `guitar.png` which looks like this:++![Practice hours][guitar]++Each bar is labeled with a weekday and has an associated practice+hour. The chart is scaled automatically such that the largest bar+spans the (configurable, see below) height of the chart. The `--title`+flag passed to `barchart` in the above call is optional. If you do not+supply one, then barchart uses the basename of the CSV file as title+of the chart.++If you want to track practice hours over multiple weeks, you can+create a file with a _mean_, _minimal_, and _maximal_ values for each+day of the week:++    Mon,1.2,0.9,1.7+    Tue,0.3,0.1,0.5+    Wed,2.1,1.5,2.5+    Thu,0.9,0.4,1.0+    Fri,1.1,1.0,1.2+    Sat,3.2,1.5,5.2+    Sun,3.1,2.3,4.2++Calling `barchart` in the `interval` mode++    # barchart intervals --title="Mean practice hours" guitar-mean.csv++produces the following bar chart:++![Mean practice hours][guitar-mean]++In this chart, each bar represents the mean practice hour for a day+of the week and the minimum and maximum values are depicted with+intervals on the right edge of a bar.++If you want to compare your practice hours for each day of the week+and split it by months, you can create a CSV file like this:++    Mon,1.2,2.1,1.7+    Tue,0.6,0.7,0.8+    Wed,2.1,1.2,2.5+    Thu,0.9,1.5,1.7+    Fri,1.1,1.3,0.7+    Sat,3.2,1.7,4.3+    Sun,3.1,3.2,2.1++We can use `barchart` (in the default mode) ++    # barchart --title="Practice hours per month" --division="Jan Feb Mar" guitar-months.csv++to create the following diagram:++![Practice hours by month][guitar-months]++Each bar is divided into different blocks which all have an associated+amount of practice hours. Green blocks represent practice hours in+January, red blocks in February, and blue blocks represent practice+hours in March. The block labels are given to `barchart` via the+`--division` flag. You can also draw multiple blocks per bar in+`interval` mode but then three values (_mean_,_min_,_max_) are used+for each block. Hence, if you want to depict mean practice times with+deviations for January, February, and March, you must create a CSV+file where each day of the week is followed by nine practice times.++## flags++The `barchart` program can be configured using command-line flags. We+can use the `--help` flag to print a summary:++    # barchart --help+    Bar Chart 0.1+    +    barchart [blocks] [FLAG] [FILE]+    +    barchart intervals [FLAG] [FILE]+    +    barchart criterion [FLAG] [FILE]+    +         --summary               Show benchmark summary (default)+      -s --summary-comparison    Compare different benchmark summaries+      -b --benchmark-comparison  Compare different benchmarks+    +    barchart progression [FLAG] [FILE]+    +      -s --summary-comparison    Breakdown chart by benchmark summary (default)+      -b --benchmark-comparison  Breakdown chart by benchmarks+    +    Common flags:+      -? --help[=FORMAT]         Show usage information (optional format)+      -V --version               Show version information+      -v --verbose               Higher verbosity+      -q --quiet                 Lower verbosity+      -o --out-file=FILE         Name of generated file+      -t --title=STRING          Title of bar chart+      -x --xlabel=STRING         Label of x axis+      -y --ylabel=STRING         Label of y axis+      -g --guess-file-type       Guess output file type by name (default)+         --png                   Generate .png file+         --svg                   Generate .svg file+         --pdf                   Generate .pdf file+         --ps                    Generate .ps file+      -d --division=STRINGS      Labels separated by whitespace+      -c --colors=STRINGS        Color names separated by whitespace+      -w --width=NUM             Width of generated bar chart (default=600)+      -h --height=NUM            Height of generated bar chart (default=300)+      -l --label-size=NUM        Font size used for labels (default=12)+      -W --bar-width=FLOAT       Bar width between 0 and 1 (default=0.3)++`barchart` can be run in different modes. We have already seen the+default (`blocks`) mode and the `intervals` mode. The `criterion` and+`progression` modes are described below. Most command-line flags are+self explanatory. Apart from what we have seen in the example above,+the following options are particularly interesting:++  * `--xlabel` and `--ylabel` label the axis of the coordinate system.++  * `--colors` change the colors of the different blocks of a bar. You+    can use all color names listed in the [SVG 1.1+    Specification][Colors]. If you specify fewer colors than there are+    blocks, then colors are reused in a cyclic fashion. The default+    value for this argument is `--colors="seagreen firebrick+    midnightblue"`.++  * `--width` and `--height` specify the dimensions of the generated+    _chart_. The generated _picture_ is a little larger because of the+    title and bar labels. If you want to draw a chart with many bars,+    you should increase the width compared to the height or tweak the+    bar width.++  * `--bar-width` is a value between 0.0 and 1.0 hat specifies how+    thick the bars are compared to the bar distance. With a value of+    `1.0` the bars are drawn directly next to each other, a value of+    0.0 draws bars that are actually lines.++## criterion++[criterion] is a Haskell tool for statistically robust benchmarking+that can generate graphs which, for example, depict the densities of+execution times. criterion can also generate a CSV file summarising+all benchmarks but does not provide means to visualise these+summaries.++We can use `barchart` in `criterion` mode to draw graphs based on the+summary files generated by criterion. To demonstrate the criterion+mode, we write a small Haskell program that benchmarks a simple+definition of the [factorial] function:++    import Criterion.Main+    +    main = defaultMain [bgroup "fac" [bench (show n) (nf product [1..n]) | n <- ns]]+     where ns = [k*10^4 | k <- [1,2,3]] :: [Integer]++We can compile this program, generate a benchmark summary using+criterion, and visualise it using barchart as follows:++    # ghc -O2 --make factorial+    # ./factorial --summary=factorial.csv+    # barchart criterion factorial.csv++These three calls generate a bar chart with one bar for each benchmark+whose size is proportional to the corresponding run time.++![fac]++We can now modify the program to use an explicitly recursive+definition of the fibonacci function to see whether this affects the+run times.++    fac 0 = 1+    fac n = n * fac (n-1)++After generating another summary file `factorial2.csv` we could+generate another bar chart to visualise it and view both charts side+by side to compare the run times of the different implementations of+the fibonacci function. However, instead of generating two different+charts we can also generate a single chart that combines information+from multiple benchmark summaries.++    # barchart criterion --summary-comparison factorial.csv factorial2.csv++We can pass as many summary files as we like and barchart will draw a+bar for each summary file with blocks representing the different+benchmarks.++![fac-summaries]++We can see clearly that the original implementation using `product` is+faster than the explicitly recursive definition of the factorial+function. Instead of drawing different bars for different summaries,+barchart can also draw different bars for the different benchmarks+with blocks for the summaries.++![fac-benchmarks]++This is less useful for comparing different implementations but useful+to compare the different benchmarks using all implementations at once.++## progression++[progression] is an alternative tool to depict criterion's benchmark+results. It generates charts similar to the summary comparisons+discussed above but draws bars for different benchmarks next to each+other rather than as boxes on top of each other in a single+bar. Drawing the results of all benchmarks for a specific+implementation on top of each other lets you see immediately which+implementation is the fastest _in total_ because the total height of+the bar is the sum of all benchmarks for the corresponding+implementation. However, progression provides features that barchart+does not provide such as normalising run times and show them as+percentages. Hence, the barchart program has a `progression` mode to+generate bar charts from CSV files generated by progression.++We can alter the factorial program as follows to generate+progression's data:++    import Criterion+    import Progression.Main+    +    main = defaultMain (bgroup "fac" [bench (show n) (nf fac n) | n <- ns])+     where ns = [k*10^4 | k <- [1,2,3]] :: [Integer]+    +    fac 0 = 1+    fac n = n * fac (n-1)++We do not need to change the fibonacci function itself because we+won't execute benchmarks using progression. We only generate a+comparison of the previous benchmarks:++    # mv factorial.csv bench-factorial.csv+    # mv factorial2.csv bench-factorial2.csv+    # ./factorial --mode=graph --compare=factorial,factorial2++progression wants the summary files to have specific names, hence, we+rename them appropriately before generating a graph. The progression+tool generates two files `plot.csv` and `plot.png`. Here, we are not+interested in the generated PNG file but pass the CSV file to the+barchart program in `progression` mode.++    # barchart progression --division="product recursive" plot.csv++As the CSV file generated by progression does not include information+of the names of the summary files, we provide labels using the+`--division` flag. The generated bar chart loooks like this:++![progression-summaries]++The run times of the first implementation are normalised to 100 % and+the run times of the second implementation are depicted relative to+the first. Like in `criterion` mode we can also flip the axis in+`progression` mode.++    # barchart progression --benchmark-comparison --division="product recursive" plot.csv++Now the first benchmark is normalised to 100 % in each bar so this+diagram is of questionable use.++![progression-benchmarks]++# collaboration++The source code is on [GitHub] so you can create or vote on [issues]+to ask for extensions or fork this project to write extensions+yourself.++## limitations++Currently, barchart does not support negative quantities and draws+weird bars if you use them anyway. Also, the legend for blocks is+sometimes drawn suboptimally. I guess I made a mistake but I cannot+find it.++## thanks++I would not have written this program without the [Diagrams] library+by Brent Yorgey and the [CmdArgs] package by Neil Mitchell.++# contact++For questions or feedback email [Sebastian Fischer][email].++[email]: mailto:sebf@informatik.uni-kiel.de+[GitHub]: http://github.com/sebfisch/haskell-barchart +[issues]: http://github.com/sebfisch/haskell-barchart/issues++[guitar]: http://sebfisch.github.com/haskell-barchart/examples/guitar.png+[guitar-mean]: http://sebfisch.github.com/haskell-barchart/examples/guitar-mean.png+[guitar-months]: http://sebfisch.github.com/haskell-barchart/examples/guitar-months.png+[fac]: http://sebfisch.github.com/haskell-barchart/examples/factorial.png+[fac-summaries]: http://sebfisch.github.com/haskell-barchart/examples/fac-summaries.png+[fac-benchmarks]: http://sebfisch.github.com/haskell-barchart/examples/fac-benchmarks.png+[progression-summaries]: http://sebfisch.github.com/haskell-barchart/examples/progression-summaries.png+[progression-benchmarks]: http://sebfisch.github.com/haskell-barchart/examples/progression-benchmarks.png++[criterion]: http://hackage.haskell.org/package/criterion+[progression]: http://hackage.haskell.org/package/progression+[Diagrams]: http://code.haskell.org/diagrams/+[CmdArgs]: http://community.haskell.org/~ndm/cmdargs/+[Gtk2Hs]: http://www.haskell.org/gtk2hs/+[Colors]: http://www.w3.org/TR/SVG11/types.html#ColorKeywords+[factorial]: http://en.wikipedia.org/wiki/Factorial
+ Setup.lhs view
@@ -0,0 +1,8 @@+#! /usr/bin/env runhaskell++> module Main (main) where+>+> import Distribution.Simple (defaultMain)+>+> main :: IO ()+> main = defaultMain
+ barchart.cabal view
@@ -0,0 +1,58 @@+Name:          barchart+Version:       0.1+Cabal-Version: >= 1.6+Synopsis:      Creating Bar Charts in Haskell++Description:++  This package provides a program that generates bar charts from CSV+  files and has special support for visualising the benchmark results+  of the criterion and progression tools. It comes with an associated+  API that can be used to generate custom bar charts in case the+  charts generated by the command-line program are insufficient for+  your needs.++Category:      Graphics+License:       BSD3+License-File:  LICENSE+Author:        Sebastian Fischer+Maintainer:    Sebastian Fischer+Bug-Reports:   http://github.com/sebfisch/haskell-barchart/issues+Homepage:      http://sebfish.github.com/haskell-barchart+Build-Type:    Simple+Stability:     experimental++Extra-Source-Files:     README.markdown++Executable barchart+  Main-is:              barchart.hs+  Hs-Source-Dirs:       src+  Build-Depends:        base >= 3 && < 5,+                        cmdargs >= 0.1 && < 0.2, +                        csv >= 0.1 && < 1.2, +                        diagrams >= 0.2 && < 0.3,+                        filepath >= 1.1 && < 1.2+  Extensions:           DeriveDataTypeable,+                        FlexibleContexts,+                        GeneralizedNewtypeDeriving,+                        NamedFieldPuns, +                        RecordWildCards,+                        TypeFamilies++Library+  Exposed-Modules:      Graphics.BarChart+  Other-Modules:        Graphics.BarChart.Types,+                        Graphics.BarChart.Rendering,+                        Graphics.BarChart.Parser,+                        Graphics.BarChart.Parser.Criterion,+                        Graphics.BarChart.Parser.Progression+  Hs-Source-Dirs:       src+  Build-Depends:        base >= 3 && < 5,+                        csv >= 0.1 && < 1.2, +                        diagrams >= 0.2 && < 0.3,+                        filepath >= 1.1 && < 1.2+  Extensions:           FlexibleContexts,+                        GeneralizedNewtypeDeriving,+                        NamedFieldPuns, +                        RecordWildCards,+                        TypeFamilies
+ src/Graphics/BarChart.hs view
@@ -0,0 +1,99 @@+-- |+-- Module      : Graphics.BarChart+-- Copyright   : Sebastian Fischer+-- License     : BSD3+-- Maintainer  : Sebastian Fischer <mailto:sebf@informatik.uni-kiel.de>+-- Stability   : experimental+-- +-- This library provides functions and data type for generating bar+-- charts, for example, from CSV files. If the functionality provided+-- by the command-line program is insufficient for your needs, you can+-- use the API provided by this library to draw custom bar charts.+-- +-- The library provides high-level functions to generate bar-chart+-- values from different types but also exports the underlying+-- datatype and all functions used for rendering such that you have+-- access to all internal details.+-- +module Graphics.BarChart (++  -- * Reading from CSV files++  -- | There are three different modes for parsing simple CSV files:+  --   treating different values in one row as blocks of one bar,+  --   treating them as deviations from a mean value, or both. ++  -- ** Writing images++  -- | The following three functions implement these modes parsing the+  --   input from a CSV file and producing output in an image file.++  writeMultiBarChart, writeIntervalChart, writeMultiBarIntervalChart,++  Config(..), Label,++  -- ** Creating bar charts++  -- | The previous functions use the following helper functions to+  --   create a 'BarChart' from a CSV file.++  multiBarChart, intervalChart, multiBarIntervalChart,++  Measurable(..),++  -- ** Bar chart representations++  -- | The three different parsing modes are reflected by three data+  --   types that can be parsed from a CSV file.++  MultiBars(..), parseMultiBars, drawMultiBars,+  Intervals(..), parseIntervals, drawIntervals,+  MultiBarIntervals(..), parseMultiBarIntervals, drawMultiBarIntervals,++  -- | When you have multiple CSV files representing an interval+  --   chart, you can merge them to a single multi-bar interval+  --   chart. Such charts can be flipped such that bars and blocks+  --   change their roles.++  mergeIntervals, flipMultiBarIntervals,++  -- | Bar charts are represented as values of type 'BarChart' @a@.++  BarChart(..), Bar(..), Block(..),++  -- * Rendering++  -- | Values of type 'BarChart' @a@ can be rendered to PNG, SVG, PDF,+  --   and PS files.++  render, renderWith, conf,++  -- ** Creating diagrams++  -- | Bar charts are rendered by conversion into 'Diagram's.++  diagram,++  -- * Benchmarking visualisation++  -- | There are special functions for parsing data generated by the+  --   benchmarking tools criterion and progression.++  -- ** Criterion++  writeCriterionChart, writeComparisonChart,+  criterionChart, comparisonChart,+  RunTime(..),++  -- ** Progression++  writeProgressionChart, progressionChart,+  Ratio(..)++  ) where++import Graphics.BarChart.Types+import Graphics.BarChart.Rendering+import Graphics.BarChart.Parser+import Graphics.BarChart.Parser.Criterion+import Graphics.BarChart.Parser.Progression
+ src/Graphics/BarChart/Parser.hs view
@@ -0,0 +1,108 @@+module Graphics.BarChart.Parser where++import Text.CSV++import Data.List ( nub )+import Data.Maybe ( fromJust, fromMaybe )++import System.FilePath++import Graphics.BarChart.Types+import Graphics.BarChart.Rendering++-- | Converts a CSV file to be drawn as a chart where each bar may+--   consist of multiple blocks.+-- +parseMultiBars :: Read a => [Label] -> CSV -> MultiBars a+parseMultiBars block_labels = MultiBars block_labels . map parseRecord+ where parseRecord (label:values) = (label,map read values)++-- | Converts a CSV file to be drawn as a chart where each bar has an+--   attached deviation depicted as an interval next to the bar.+-- +parseIntervals :: Read a => CSV -> Intervals a+parseIntervals = Intervals . map parseRecord+ where parseRecord [label,m,l,u] = (label,(read m, read l, read u))++-- | Converts a CSV file to be drawn as a chart where each bar may+--   consist of multiple blocks which have an attached deviation+--   depicted as an interval next to them.+-- +parseMultiBarIntervals :: Read a => [Label] -> CSV -> MultiBarIntervals a+parseMultiBarIntervals block_labels =+  MBIntervals block_labels . map parseRecord+ where parseRecord (label:fields) = (label, triples fields)+       triples (x:y:z:xs) = (read x,read y,read z) : triples xs+       triples _          = []++-- | Used by 'writeMultiBarChart' to create a 'BarChart' from a CSV file.+-- +multiBarChart :: (Measurable a, Read a)+              => [Label]    -- ^ legend for blocks+              -> CSV        -- ^ comma separated values to depict+              -> BarChart a+multiBarChart block_labels = drawMultiBars . parseMultiBars block_labels++-- | Used by 'writeIntervalChart' to create a 'BarChart' from a CSV file.+-- +intervalChart :: (Measurable a, Read a)+              => CSV        -- ^ comma separated values to depict +              -> BarChart a+intervalChart = drawIntervals . parseIntervals++-- | Used by 'writeMultiBarIntervalChart' to create a 'BarChart' from+-- | a CSV file.+-- +multiBarIntervalChart :: (Measurable a, Read a)+                      => [Label]    -- ^ legend for blocks+                      -> CSV        -- ^ comma separated values to depict+                      -> BarChart a+multiBarIntervalChart block_labels =+  drawMultiBarIntervals . parseMultiBarIntervals block_labels++-- | The first column of the CSV file is parsed as names of the+--   bars. The height of each bar corresponds to the sum of all+--   subsequent entries. If there is more than one entry, the bars are+--   split into blocks.+-- +writeMultiBarChart :: Config   -- ^ where and how to draw the bar chart+                   -> FilePath -- ^ CSV file to read+                   -> [Label]  -- ^ if non-empty, used as legend for blocks+                   -> IO ()+writeMultiBarChart config file block_labels =+  do csv <- readCSV file+     let chart = multiBarChart block_labels csv :: BarChart Double+     renderWith config chart++-- | The first column of the CSV file is parsed as names of the+--   bars. Three entries following each bar name are parsed as mean,+--   minimum, and maximum value and depicted using an interval next to+--   the bar.+-- +writeIntervalChart :: Config   -- ^ where and how to draw the bar chart+                   -> FilePath -- ^ CSV file to read+                   -> IO ()+writeIntervalChart config file =+  do csv <- readCSV file+     let chart = intervalChart csv :: BarChart Double+     renderWith config chart++-- | The first column of the CSV file is parsed as names of the+--   bars. The entries following each bar name are parsed as triples+--   of mean, minimum, and maximum value and depicted using an+--   interval next to the bar. The number of subsequent entries must+--   be a multiple of three and each bar is divided into a+--   corresponding number of blocks.+-- +writeMultiBarIntervalChart :: Config   -- ^ where and how to draw the bar chart+                           -> FilePath -- ^ CSV file to read+                           -> [Label]  -- ^ legend for blocks+                           -> IO ()+writeMultiBarIntervalChart config file block_labels =+  do csv <- readCSV file+     let chart = multiBarIntervalChart block_labels csv :: BarChart Double+     renderWith config chart++readCSV :: FilePath -> IO CSV+readCSV file = either (fail . show) (return . clean) =<< parseCSVFromFile file+ where clean = filter (not . all null)
+ src/Graphics/BarChart/Parser/Criterion.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE NamedFieldPuns, RecordWildCards #-}++module Graphics.BarChart.Parser.Criterion where++import Text.CSV++import System.FilePath++import Graphics.BarChart.Types+import Graphics.BarChart.Parser+import Graphics.BarChart.Rendering++-- | Used by 'writeCriterionChart' to generate a bar chart from+--   criterion's summary file.+-- +criterionChart :: CSV -> BarChart RunTime+criterionChart (_:csv) = intervalChart $ map (take 4) csv++-- | Used by 'writeComparisonChart' to generate a bar chart from+--   multiple summary files generated by criterion.+-- +comparisonChart :: Bool -> [(Label,CSV)] -> BarChart RunTime+comparisonChart flip+  = drawMultiBarIntervals+  . (if flip then flipMultiBarIntervals else id)+  . mergeIntervals+  . map (\ (label,_:csv) -> (label, parseIntervals $ map (take 4) csv))++-- | Reads a summary file generated by criterion and writes a+--   corresponding bar chart.+-- +writeCriterionChart :: Config -> FilePath -> IO ()+writeCriterionChart config file =+  renderWith config . criterionChart =<< readCSV file++-- | Reads multiple summary files generated by criterion and creates a+--   bar chart to compare them. If the first argument is 'True' the+--   chart is flipped such that the bars represent different+--   benchmarks rather than summaries.+-- +writeComparisonChart :: Bool -> Config -> [FilePath] -> IO ()+writeComparisonChart flip config@Config{..} files =+  do csvs <- mapM readCSV files+     renderWith config . comparisonChart flip $+       zip (map dropExtension files) csvs
+ src/Graphics/BarChart/Parser/Progression.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE NamedFieldPuns, RecordWildCards #-}++module Graphics.BarChart.Parser.Progression where++import Text.CSV++import System.FilePath++import Graphics.BarChart.Types+import Graphics.BarChart.Parser+import Graphics.BarChart.Rendering++-- | Used by 'writeProgressionChart' to generate a bar chart from+--   progression's @plot.csv@ file.+-- +progressionChart :: Bool -> [Label] -> CSV -> BarChart Ratio+progressionChart flip labels csv+  = drawMultiBarIntervals+  . (if flip then flipMultiBarIntervals else id)+  . parseMultiBarIntervals block_labels+  $ csv+ where block_labels | null labels = replicate (length csv) ""+                    | otherwise   = labels++-- | Reads the @plot.csv@ file generated by progression and creates a+--   corresponding bar chart.+-- +writeProgressionChart :: Bool -> Config -> FilePath -> [Label] -> IO ()+writeProgressionChart flip config@Config{..} file block_labels =+  do csv <- readCSV file+     let chart = progressionChart flip block_labels csv+     renderWith config chart
+ src/Graphics/BarChart/Rendering.hs view
@@ -0,0 +1,127 @@+{-# LANGUAGE NamedFieldPuns, RecordWildCards #-}++module Graphics.BarChart.Rendering where++import Data.List ( genericLength )++import Graphics.Rendering.Diagrams+import Graphics.Rendering.Diagrams.Types ( SomeColor )++import Graphics.BarChart.Types++-- | Renders a bar chart as @barchart.png@ according to the default+--   configuration 'conf'.+-- +render :: Measurable a => BarChart a -> IO ()+render = renderWith conf{ outFile = "barchart.png" }++-- | Renders a bar chart according to a custom configuration.+-- +renderWith :: Measurable a => Config -> BarChart a -> IO ()+renderWith config@Config{..} chart =+  do renderAs outputType outFile (Width (fromIntegral width))+       . diagram config+       $ chart+     putStrLn $ "Chart written in file " ++ outFile+ where (width,_) = dimensions++-- | This function can be used to embed bar charts into bigger+--   'Diagram's.+-- +diagram :: Measurable a => Config -> BarChart a -> Diagram+diagram config@Config{..} chart@BarChart{..} =+  drawBarChart config{ ratio     = hratio / wratio,+                       fontSize = fontSize / wratio }+               chart+ where+  (width,height) = dimensions+  wratio = fromIntegral width / genericLength bars+  hratio = fromIntegral height / barChartHeight chart++barChartHeight :: Measurable a => BarChart a -> Double+barChartHeight BarChart{..} | null bars = 0+                            | otherwise = maximum (map barSize bars)++barSize :: Measurable a => Bar a -> Double+barSize Bar{..} | null blocks = 0+                | otherwise   = sum (upperSize b:map valueSize bs)+ where (b:bs) = reverse blocks++valueSize :: Measurable a => Block a -> Double+valueSize (Value x)    = size x+valueSize Interval{..} = size mean++upperSize :: Measurable a => Block a -> Double+upperSize (Value x)    = size x+upperSize Interval{..} = size upper++drawBarChart :: Measurable a => Config -> BarChart a -> Diagram+drawBarChart config@Config{..} chart@BarChart{..} =+  pad fontSize fontSize $ vsepA fontSize hcenter [header,body]+ where+  width   = genericLength bars+  height  = ratio * barChartHeight chart+  bar_sep = 1 - barRatio++  header  = vsepA fontSize hcenter [title,legend]+  title   = text (1.5*fontSize) caption+  legend  = hcat (zipWith (drawDescr config) (roll barColors) block_labels)++  body    = vcat [yBars, xaxis, xLabels]++  yBars   = hdistribA (bar_sep/2) left bottom [yaxis, cols]+  yaxis   = vsep (fontSize/2)+              [text fontSize yLabel,+               straight (pathFromVectors [(0,height)])]+  cols    = sideBySide left (map (drawBar config) bars)++  xaxis   = hsep (fontSize/2)+              [straight (pathFromVectors [(width,0)]), text fontSize xLabel]+  xLabels = vspace (fontSize/2)+         // sideBySide vcenter (map (drawBarLabel config) bars)++roll :: [SomeColor] -> [SomeColor]+roll colors | null colors = cycle [readColor "black"]+            | otherwise   = cycle colors+                                +drawDescr :: Config -> SomeColor -> Label -> Diagram+drawDescr Config{..} color string =+  block color fontSize fontSize <> text fontSize string++block :: SomeColor -> Double -> Double -> Diagram+block color width height = fillColor color $ roundRectF width height 0.1++drawBar :: Measurable a => Config -> Bar a -> Diagram+drawBar config@Config{..} Bar{..} =+  vcat . reverse $ zipWith (drawBlock config) (roll barColors) blocks++drawBlock :: Measurable a => Config -> SomeColor -> Block a -> Diagram++drawBlock Config{..} color (Value x) = hcatA bottom [bar, label]+ where+  bar   = block color barRatio (ratio * size x)+  label = translateX (fontSize/2)+        . translateY (fontSize - ratio * size x)+        . ctext fontSize+        $ show x++drawBlock Config{..} color Interval{..} = hcatA bottom [bar, deviation, label]+ where+  bar       = block color barRatio (ratio * size mean)+  deviation = translateY (-ratio * size lower) interval+  interval  = vcat [bound,line,bound]+  bound     = translateX (-fontSize/2) . straight $+                pathFromVectors [(fontSize,0)]+  line      = straight $ pathFromVectors [(0,ratio * size (upper-lower))]+  label     = translateX (-fontSize/2)+            . translateY (fontSize - ratio * size mean)+            . ctext fontSize+            $ show mean++ctext :: Double -> String -> Diagram+ctext size string = translateY (-size/2) $ text size string++drawBarLabel :: Config -> Bar a -> Diagram+drawBarLabel Config{..} Bar{..} = text fontSize label++sideBySide valign ds = hdistribA 1 valign bottom ds
+ src/Graphics/BarChart/Types.hs view
@@ -0,0 +1,225 @@+{-# LANGUAGE TypeFamilies, FlexibleContexts, NamedFieldPuns, RecordWildCards,+             GeneralizedNewtypeDeriving #-}++module Graphics.BarChart.Types where++import System.IO ( FilePath )+import Graphics.Rendering.Diagrams+import Graphics.Rendering.Diagrams.Types ( SomeColor(..) )++import Data.Data     ( Data, Typeable )+import Data.List     ( nub, transpose )+import Data.Maybe    ( fromJust, fromMaybe )+import Control.Arrow ( first )++type Label = String++-- | Bar charts consist of a (possibly empty) list of labels for the+--   diferent blcks of bars and the bars themselves.+-- +data BarChart a = BarChart {++  -- | Labels of blocks in bars. Drawn as a legend if non-empty.+  block_labels :: [Label],++  -- | The different bars of the chart.+  bars :: [Bar a] }+ deriving Show++-- | Represents one bar of a bar chart.+data Bar a = Bar {++  -- | Label written underneath+  label :: Label,++  -- | Different blocks of the bar. Simple charts contain only one+  --   block per bar.+  blocks :: [Block a] }+ deriving Show++-- | Bocks either have a single associated value or a mean value along+--   with minimum and maximum deviation.+-- +data Block a = Value a | Interval { mean :: a, lower :: a, upper :: a }+ deriving Show++-- | Instances of this class can be depicted in bar charts.+-- +class Num a => Measurable a where++  -- | Measures the given value to figure out the correponding height+  --   of the bar.+  -- +  size :: a -> Double++instance Measurable Double  where size = id+instance Measurable Float   where size = realToFrac+instance Measurable Integer where size = fromIntegral+instance Measurable Int     where size = fromIntegral++type ColorName = String++readColor :: ColorName -> SomeColor+readColor color = SomeColor . fromMaybe (error $ "ivalid color: " ++ color) $+                    readColourName color++-- | Specifies how bar charts are rendered+data Config = Config {+  -- | file to which the bar chart is written+  outFile :: FilePath,++  -- | Type of generated file+  outputType :: OutputType,++  -- | Title of the generated chart+  caption :: Label, ++  -- | Label of the x-axis+  xLabel :: Label, ++  -- | label of the y-axis+  yLabel :: Label,++  -- | Colors for the different blocks of a bar. If there are fewer+  --   colors than blocks, then colors are reused in a cyclic fashion.+  barColors :: [SomeColor],++  -- | Dimensions of the generated chart. The image will be a bit+  --   larger because of additiona space used for labels.+  dimensions :: (Int,Int),++  -- | Scales the height of the chart. The given ratio is multiplied+  --   with the 'size' of bars as given by the corresponding+  --   'Measurable' instance.+  ratio :: Double, ++  -- | Specifies the size of fonts used for labels.+  fontSize :: Double, ++  -- | Value between 0.0 and 1.0 which pecifies the width of+  --   bars. Zero means that the bars are lines and 1.0 means that the+  --   is no space between bars.+  barRatio :: Double }++-- | The default configuration generates a PNG file with a chart of+--   size 600x300 pixels. The output file is left unspecified and you+--   should provide one if you use a cstom configuration.+-- +conf :: Config+conf = Config { outFile = "", outputType = PNG,+                caption = "", xLabel = "", yLabel = "",+                barColors = map SomeColor [forestgreen,firebrick,midnightblue],+                dimensions = (600,300),+                ratio = 1, fontSize = 12, barRatio = 0.3 }++-- | Wrapper around the 'Double' type used in bar charts for criterion+--   summary files. It has a custom 'Show' instance to produce labels+--   like @10ms@ or @2h@ rather than showing the plain 'Double'+--   values.+-- +newtype RunTime = RunTime Double+ deriving (Eq,Num,Measurable)++instance Read RunTime where+  readsPrec n = map (first RunTime) . readsPrec n++instance Show RunTime where+  show (RunTime t) | t >= hours   = display (t/hours)   ++ " h"+                   | t >= minutes = display (t/minutes) ++ " m"+                   | t >= seconds = display (t/seconds) ++ " s"+                   | t >= millis  = display (t/millis)  ++ " ms"+                   | t >= micros  = display (t/micros)  ++ " us"+                   | otherwise    = display (t/nanos)   ++ " ns"+   where hours   = 60 * minutes+         minutes = 60 * seconds+         seconds = 1+         millis  = seconds / 1000+         micros  = millis  / 1000+         nanos   = micros  / 1000++display :: Double -> String+display x | x >= 100  = show (fromIntegral (round x))+          | x >= 10   = show (fromIntegral (round (10*x)) / 10)+          | otherwise = show (fromIntegral (round (100*x)) / 100)++-- | Wrapper around the double type used in bar charts for progression+--   summaries. It has a custom 'Show' instance that shows the 'Double'+--   values as percentages.+-- +newtype Ratio = Ratio Double+ deriving (Eq,Num,Measurable)++instance Read Ratio where+  readsPrec n = map (first Ratio) . readsPrec n++instance Show Ratio where+  show (Ratio r) = display (100*r) ++ " %"++-- | Values of this type are drawn as charts where each bar may+--   consist of multiple blocks.+-- +data MultiBars a = MultiBars [Label] [(Label,[a])]+ deriving Show++-- | Converts bars with multiple blocks into their 'BarChart'+--   representation.+drawMultiBars :: Measurable a => MultiBars a -> BarChart a+drawMultiBars (MultiBars block_labels pairs) = BarChart{..}+ where bars               = map (uncurry mkBar) pairs+       mkBar label values = Bar{..} where blocks = map Value values++-- | Values of this type are drawn as charts where each bar has an+--   associated deviation depicted as an interval next to the bar.+-- +newtype Intervals a = Intervals [(Label,(a,a,a))]+ deriving Show++-- | Converts bars with associated deviation into their 'BarChart'+--   representation.+-- +drawIntervals :: Measurable a => Intervals a -> BarChart a+drawIntervals (Intervals pairs) = BarChart{..}+ where block_labels                   = []+       bars                           = map (uncurry mkBar) pairs+       mkBar label (mean,lower,upper) = Bar{..} where blocks = [Interval{..}]++-- | Values of this type are drawn as charts where each bar may be+--   divided into multiple blocks with an associated deviation+--   depicted as intervals next to them.+-- +data MultiBarIntervals a = MBIntervals [Label] [(Label,[(a,a,a)])]+ deriving Show++-- | Merges several interval charts into a chart where each bar has+--   multiple blocks that represent the different interval charts.+-- +mergeIntervals :: Num a => [(Label,Intervals a)] -> MultiBarIntervals a+mergeIntervals xs =+  MBIntervals block_labels [ (label,intervals label) | label <- bar_labels ]+ where+  bar_labels   = map fst xs+  block_labels = nub (concatMap ((\ (Intervals ys) -> map fst ys) . snd) xs)++  intervals l  = map (fromMaybe (0,0,0) . flip lookup ys) block_labels+   where Intervals ys = fromJust (lookup l xs)++-- | Swaps bars and blocks of a chart that contains both and+--   associated deviations.+-- +flipMultiBarIntervals :: MultiBarIntervals a -> MultiBarIntervals a+flipMultiBarIntervals (MBIntervals old_block_labels old_bars) =+  MBIntervals new_block_labels new_bars+ where+  new_block_labels = map fst old_bars+  new_bars = zip old_block_labels . transpose . map snd $ old_bars++-- | Converts bars with multiple blocks and associated deviations into+--   their 'BarChart' representation.+-- +drawMultiBarIntervals :: Measurable a => MultiBarIntervals a -> BarChart a+drawMultiBarIntervals (MBIntervals block_labels pairs) = BarChart{..}+ where bars = map (uncurry mkBar) pairs++       mkBar label ints = Bar{..}+        where blocks = map mkInterval ints+              mkInterval (mean,lower,upper) = Interval{..}
+ src/barchart.hs view
@@ -0,0 +1,347 @@+{-# LANGUAGE NamedFieldPuns, RecordWildCards, DeriveDataTypeable #-}++import Graphics.BarChart hiding ( Intervals )+import Graphics.BarChart.Types ( readColor )++import Data.Char ( toLower )+import Data.Maybe ( fromMaybe )++import Control.Monad ( when, forM_ )++import System.FilePath++import System.Console.CmdArgs++import qualified Graphics.Rendering.Diagrams as D++data ExecMode+  = Blocks      { out_file, title, xlabel, ylabel :: String,+                  file_type :: FileType,+                  division, colors :: String,+                  width, height, label_size :: Int,+                  bar_width :: Double,+                  in_files :: [String] }+  | Intervals   { out_file, title, xlabel, ylabel :: String,+                  file_type :: FileType,+                  division, colors :: String,+                  width, height, label_size :: Int,+                  bar_width :: Double,+                  in_files :: [String] }+  | Criterion   { breakdown :: Breakdown,++                  out_file, title, xlabel, ylabel :: String,+                  file_type :: FileType,+                  division, colors :: String,+                  width, height, label_size :: Int,+                  bar_width :: Double,+                  in_files :: [String] }+  | Progression { breakdown :: Breakdown,++                  out_file, title, xlabel, ylabel :: String,+                  file_type :: FileType,+                  division, colors :: String,+                  width, height, label_size :: Int,+                  bar_width :: Double,+                  in_files :: [String] }+ deriving (Show,Data,Typeable)++data FileType = Guess_File_Type | PNG | SVG | PDF | PS+ deriving (Eq,Show,Data,Typeable)++suffixOf :: FileType -> String+suffixOf PNG = "png"+suffixOf SVG = "svg"+suffixOf PDF = "pdf"+suffixOf PS  = "ps"+suffixOf _   = ""++fromFileType :: FileType -> D.OutputType+fromFileType PNG = D.PNG+fromFileType SVG = D.SVG+fromFileType PDF = D.PDF+fromFileType PS  = D.PS+fromFileType _   = error "fromFileType: cannot convert guessed file type"++replaceUnknownFileType :: FileType -> FileType -> FileType+replaceUnknownFileType Guess_File_Type t = t+replaceUnknownFileType t               _ = t++data Breakdown = Summary | Summary_Comparison | Benchmark_Comparison+ deriving (Eq,Show,Data,Typeable)++blocksMode =+  Blocks {+    out_file   = outFile conf+                    &= text "Name of generated file"+                    &  typFile,+    file_type  = Guess_File_Type `enum`+                    [Guess_File_Type+                         &= text "Guess output file type by name (default)",+                     PNG &= text "Generate .png file",+                     SVG &= text "Generate .svg file",+                     PDF &= text "Generate .pdf file",+                     PS  &= text "Generate .ps file"],+    title      = caption conf+                    &= text "Title of bar chart"+                    &  typString,+    xlabel     = xLabel conf+                    &= text "Label of x axis"+                    &  typString,+    ylabel     = xLabel conf+                    &= text "Label of y axis"+                    &  typString,+    division  = "" &= text "Labels separated by whitespace"+                    &  typStrings,+    colors     = "" &= text "Color names separated by whitespace"+                    &  typStrings,+    width      = width+                    &= text "Width of generated bar chart"+                    &  typ "NUM",+    height     = height+                    &= text "Height of generated bar chart"+                    &  typ "NUM",+    label_size = 12 &= text "Font size used for labels"+                    &  typ "NUM",+    bar_width  = barRatio conf+                    &= text "Bar width between 0 and 1"+                    &  flag "W"+                    &  typ "FLOAT",+    in_files   = [] &= text "CVS files with data to visualise"+                    &  args }+ where (width,height) = dimensions conf++-- cannot reuse flag attributes in cmdargs :(+intervalsMode =+  Intervals {+    out_file   = outFile conf+                    &= text "Name of generated file"+                    &  typFile,+    file_type  = Guess_File_Type `enum`+                    [Guess_File_Type+                         &= text "Guess output file type by name (default)",+                     PNG &= text "Generate .png file",+                     SVG &= text "Generate .svg file",+                     PDF &= text "Generate .pdf file",+                     PS  &= text "Generate .ps file"],+    title      = caption conf+                    &= text "Title of bar chart"+                    &  typString,+    xlabel     = xLabel conf+                    &= text "Label of x axis"+                    &  typString,+    ylabel     = xLabel conf+                    &= text "Label of y axis"+                    &  typString,+    division  = "" &= text "Labels separated by whitespace"+                    &  typStrings,+    colors     = "" &= text "Color names separated by whitespace"+                    &  typStrings,+    width      = width+                    &= text "Width of generated bar chart"+                    &  typ "NUM",+    height     = height+                    &= text "Height of generated bar chart"+                    &  typ "NUM",+    label_size = 12 &= text "Font size used for labels"+                    &  typ "NUM",+    bar_width  = barRatio conf+                    &= text "Bar width between 0 and 1"+                    &  flag "W"+                    &  typ "FLOAT",+    in_files   = [] &= text "CVS files with data to visualise"+                    &  args }+ where (width,height) = dimensions conf++criterionMode =+  Criterion {+    out_file   = outFile conf+                    &= text "Name of generated file"+                    &  typFile,+    file_type  = Guess_File_Type `enum`+                    [Guess_File_Type+                         &= text "Guess output file type by name (default)",+                     PNG &= text "Generate .png file",+                     SVG &= text "Generate .svg file",+                     PDF &= text "Generate .pdf file",+                     PS  &= text "Generate .ps file"],+    title      = caption conf+                    &= text "Title of bar chart"+                    &  typString,+    xlabel     = xLabel conf+                    &= text "Label of x axis"+                    &  typString,+    ylabel     = xLabel conf+                    &= text "Label of y axis"+                    &  typString,+    division  = "" &= text "Labels separated by whitespace"+                    &  typStrings,+    colors     = "" &= text "Color names separated by whitespace"+                    &  typStrings,+    width      = width+                    &= text "Width of generated bar chart"+                    &  typ "NUM",+    height     = height+                    &= text "Height of generated bar chart"+                    &  typ "NUM",+    label_size = 12 &= text "Font size used for labels"+                    &  typ "NUM",+    bar_width  = barRatio conf+                    &= text "Bar width between 0 and 1"+                    &  flag "W"+                    &  typ "FLOAT",+    in_files   = [] &= text "CVS files with data to visualise"+                    &  args,+    breakdown  = Summary `enum`+                  [Summary+                    &= text "Show benchmark summary (default)",+                   Summary_Comparison+                    &= text "Compare different benchmark summaries"+                    &  flag "s",+                   Benchmark_Comparison+                    &= text "Compare different benchmarks"+                    &  flag "b"] }+ where (width,height) = dimensions conf++progressionMode =+  Progression {+    out_file   = outFile conf+                    &= text "Name of generated file"+                    &  typFile,+    file_type  = Guess_File_Type `enum`+                    [Guess_File_Type+                         &= text "Guess output file type by name (default)",+                     PNG &= text "Generate .png file",+                     SVG &= text "Generate .svg file",+                     PDF &= text "Generate .pdf file",+                     PS  &= text "Generate .ps file"],+    title      = caption conf+                    &= text "Title of bar chart"+                    &  typString,+    xlabel     = xLabel conf+                    &= text "Label of x axis"+                    &  typString,+    ylabel     = xLabel conf+                    &= text "Label of y axis"+                    &  typString,+    division  = "" &= text "Labels separated by whitespace"+                    &  typStrings,+    colors     = "" &= text "Color names separated by whitespace"+                    &  typStrings,+    width      = width+                    &= text "Width of generated bar chart"+                    &  typ "NUM",+    height     = height+                    &= text "Height of generated bar chart"+                    &  typ "NUM",+    label_size = 12 &= text "Font size used for labels"+                    &  typ "NUM",+    bar_width  = barRatio conf+                    &= text "Bar width between 0 and 1"+                    &  flag "W"+                    &  typ "FLOAT",+    in_files   = [] &= text "CVS files with data to visualise"+                    &  args,+    breakdown  = Summary_Comparison `enum`+                  [Summary_Comparison+                    &= text "Breakdown chart by benchmark summary (default)"+                    & flag "s",+                   Benchmark_Comparison+                    &= text "Breakdown chart by benchmarks"+                    &  flag "b"] }+ where (width,height) = dimensions conf++typString  = typ "STRING"+typStrings = typ "STRINGS"++execModes = [blocksMode &= defMode,+             intervalsMode, criterionMode, progressionMode]++exitIf msg cond = when cond (error msg)++main = do execMode <- cmdArgs "Bar Chart 0.0" (map mode execModes)+          exitIf "no input files given" $ null (in_files execMode)+          dispatch execMode++dispatch mode@Blocks{..} =+  forM_ in_files $ \in_file ->+    writeMultiBarChart+      (config (guessDefaults in_file mode))+      in_file+      (words division)++dispatch mode@Intervals{..} =+  forM_ in_files $ \in_file ->+    writeMultiBarIntervalChart+      (config (guessDefaults in_file mode))+      in_file+      (words division)++dispatch mode@Criterion{..} =+  case breakdown of+    Summary ->+      forM_ in_files $ \in_file ->+        writeCriterionChart+          (config (guessBenchmarkDefaults in_file mode))+          in_file+    Summary_Comparison ->+      writeComparisonChart False+        (config (guessBenchmarkDefaults "summaries.csv" mode))+        in_files+    Benchmark_Comparison ->+      writeComparisonChart True+        (config (guessBenchmarkDefaults "benchmarks.csv" mode))+        in_files++dispatch mode@Progression{..} =+  case breakdown of+    Summary_Comparison ->+      forM_ in_files $ \in_file ->+        writeProgressionChart True+          (config (guessBenchmarkDefaults in_file mode))+          in_file+          (words division)+    Benchmark_Comparison ->+      forM_ in_files $ \in_file ->+        writeProgressionChart False+          (config (guessBenchmarkDefaults in_file mode))+          in_file+          (words division)++guessDefaults :: FilePath -> ExecMode -> ExecMode+guessDefaults in_file = guessColors . guessTitle . guessFileType . guessOutFile+ where+  guessOutFile mode =+    mode { out_file = out_file mode ? replaceExtension in_file suffix }+   where suffix = suffixOf (file_type mode) ? ".png"++  guessFileType mode =+    mode { file_type = replaceUnknownFileType (file_type mode) $+                         fromMaybe (error $ "unsupported type: " ++ suffix) $+                           lookup suffix fileTypes }+   where suffix    = map toLower $ takeExtension (out_file mode)+         fileTypes = [(".png",PNG),(".svg",SVG),(".pdf",PDF),(".ps",PS)]++  guessTitle mode =+    mode { title = title mode ? takeBaseName (out_file mode) }++  guessColors mode =+    mode { colors = colors mode ? "forestgreen firebrick midnightblue" }++guessBenchmarkDefaults in_file = guessAxis . guessDefaults in_file+ where+  guessAxis mode = mode { xlabel = xlabel mode ? "benchmark",+                          ylabel = ylabel mode ? "run time" }++config mode = Config {+  outFile = out_file mode,+  outputType = fromFileType $ file_type mode,+  caption = title mode, xLabel = xlabel mode, yLabel = ylabel mode,+  barColors = map readColor . words $ colors mode,+  dimensions = (width mode,height mode),+  ratio = 1.0,+  fontSize = fromIntegral $ label_size mode,+  barRatio = bar_width mode }++(?) :: String -> String -> String+"" ? s = s+s  ? _ = s