packages feed

happy-hour-0.0.0.4: library/Graphics/HappyHour.hs

module Graphics.HappyHour (writeBarGraphSvgFile) where

import Data.Functor (void)
import Data.List (genericLength)
import Graphics.Rendering.Chart.Easy hiding (bars)
import Graphics.Rendering.Chart.Backend.Diagrams
    (renderableToFile, FileOptions, fo_size)

-- | Create an SVG file containing a bar graph
writeBarGraphSvgFile ::
    FilePath -- ^ Where the file will be written. The containing directory
             -- must already exist. If there already exists a file at this path,
             -- it will be overwritten. If there exists a directory at this path,
             -- the action will fail.
    -> [(String, Int)]
          -- ^ The data to visualize. Each list entry represents a bar on the
          -- plot, ordered from left to right. The @String@ is the label on the
          -- X axis, and the @Int@ is the height of the bar.
    -> IO ()
writeBarGraphSvgFile path bars = void $ renderableToFile fo path r
  where
    layout = applyBarPlot bars def :: Layout PlotIndex Int
    r = toRenderable layout :: Renderable ()
    fo = applyFileOptions bars def :: FileOptions

type Bars = [(String, Int)]

type Endo a = a -> a

foldEndo :: [a -> a] -> (a -> a)
foldEndo = foldr (.) id

applyFileOptions :: Bars -> Endo FileOptions
applyFileOptions bars =
    set fo_size (maximum ([l x | (x, _) <- bars]) * l bars * 50, 600)
  where
    l = genericLength

-- | Add a single bar plot to a layout, configure its X axis labels to
-- match the data, and style the layout to look nice with a bar plot
applyBarPlot :: Bars -> Endo (Layout PlotIndex Int)
applyBarPlot bars = foldEndo
    [ applyLayoutStyle
    , applyLabels bars
    , set layout_plots [plotBars (( applyBarStyle . applyValues bars ) def)]
    ]

-- | Set the data values on a bar plot
applyValues :: Bars -> Endo (PlotBars PlotIndex Int)
applyValues bars =
    set plot_bars_values (addIndexes [ [y] | (_, y) <- bars ])

-- | Set up the X axis of a layout in preparation for adding the bar plot
applyLabels :: Integral x => Bars -> Endo (Layout x y)
applyLabels bars =
    set (layout_x_axis . laxis_generate) (autoIndexAxis [ x | (x, _) <- bars ])

-- | Set some layout styles that look nice
applyLayoutStyle :: Endo (Layout x y)
applyLayoutStyle = foldEndo
    [ set layout_margin 40
    , setAxisLineWidth 5
    , over yGridStyle (set line_width 5 . set line_dashes [40, 20])
    , set (layout_y_axis . laxis_style . axis_label_gap) 25
    , set (layout_title_style . font_size) 80
    , setAxisFontSize 40
    ]

-- | Lens for the grid style on the Y axis
yGridStyle :: Lens' (Layout x y) LineStyle
yGridStyle = layout_y_axis . laxis_style . axis_grid_style

-- | Lens for the width of an axis
axisLineWidth :: Lens' (LayoutAxis x) Double
axisLineWidth = laxis_style . axis_line_style . line_width

-- | Lens for the label font size of an axis
axisLabelSize :: Lens' (LayoutAxis x) Double
axisLabelSize = laxis_style . axis_label_style . font_size

-- | Set the line width on both axes
setAxisLineWidth :: Double -> Endo (Layout x y)
setAxisLineWidth v =
  foldEndo [ set l v | l <- [ layout_x_axis . axisLineWidth
                            , layout_y_axis . axisLineWidth ] ]

-- | Set the label size on both axes
setAxisFontSize :: Double -> Endo (Layout x y)
setAxisFontSize v =
  foldEndo [ set l v | l <- [ layout_x_axis . axisLabelSize
                            , layout_y_axis . axisLabelSize ] ]

-- | Set some bar styles that look nice
applyBarStyle :: Endo (PlotBars x y)
applyBarStyle = foldEndo
    [ set plot_bars_spacing (BarsFixGap 80 20)
    , set plot_bars_item_styles [(FillStyleSolid (opaque steelblue), Nothing)]
    ]