hledger/bin/hledger-chart.hs
Simon Michael 7460aca70f chart: revive hledger-chart as an add-on
& drop the extra/ directory.
2017-01-09 07:31:45 -08:00

209 lines
7.4 KiB
Haskell
Executable File

#!/usr/bin/env stack
{- stack runghc --verbosity info
--package hledger-lib
--package hledger
--package Chart
--package Chart-diagrams
--package cmdargs
--package colour
--package data-default
--package safe
-}
{-
hledger-chart
Generates primitive pie charts, based on the old hledger-chart package.
Supposed to show only balances of one sign, but this might be broke.
Copyright (c) 2007-2017 Simon Michael <simon@joyful.com>
Released under GPL version 3 or later.
-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Monad
import Data.Colour
import Data.Colour.Names
import Data.Colour.RGBSpace
import Data.Colour.RGBSpace.HSL (hsl)
import Data.Colour.SRGB.Linear (rgb)
import Data.Default
import Data.List
import Data.Maybe
import Data.Ord
import qualified Data.Text as T
import Data.Tree
import Graphics.Rendering.Chart
import Graphics.Rendering.Chart.Backend.Diagrams
import Safe
import System.Console.CmdArgs hiding (def)
import System.Console.CmdArgs.Explicit
import System.Exit
import Text.Printf
import Hledger
import Hledger.Cli hiding (progname,progversion)
-- options
-- progname = "hledger-chart"
-- progversion = progname ++ " dev"
defchartoutput = "hledger.svg"
defchartitems = 10
defchartsize = "600x400"
chartmode = (defCommandMode ["hledger-chart"]) {
modeArgs = ([], Just $ argsFlag "[PATTERNS] --add-posting \"ACCT AMTEXPR\" ...")
,modeHelp = "generate a pie chart image for the top account balances (of one sign only)"
,modeHelpSuffix=[]
,modeGroupFlags = Group {
groupNamed = [generalflagsgroup1]
,groupUnnamed = [
flagReq ["chart-output","o"] (\s opts -> Right $ setopt "chart-output" s opts) "IMGFILE" ("output filename (default: "++defchartoutput++")")
,flagReq ["chart-items"] (\s opts -> Right $ setopt "chart-items" s opts) "N" ("number of accounts to show (default: "++show defchartitems++")")
,flagReq ["chart-size"] (\s opts -> Right $ setopt "chart-size" s opts) "WIDTHxHEIGHT" ("image size (default: "++defchartsize++")")
]
,groupHidden = []
}
}
-- hledger-chart options, used in hledger-chart and above
data ChartOpts = ChartOpts {
chart_output_ :: FilePath
,chart_items_ :: Int
,chart_size_ :: String
,cliopts_ :: CliOpts
} deriving (Show)
defchartopts = ChartOpts
def
def
def
defcliopts
-- instance Default CliOpts where def = defcliopts
toChartOpts :: RawOpts -> IO ChartOpts
toChartOpts rawopts = do
cliopts <- rawOptsToCliOpts rawopts
return defchartopts {
chart_output_ = fromMaybe defchartoutput $ maybestringopt "debug-chart" rawopts
,chart_items_ = fromMaybe defchartitems $ maybeintopt "debug-items" rawopts
,chart_size_ = fromMaybe defchartsize $ maybestringopt "debug-size" rawopts
,cliopts_ = cliopts
}
checkChartOpts :: ChartOpts -> IO ChartOpts
checkChartOpts opts = do
(checkCliOpts $ cliopts_ opts) `seq` return opts
getHledgerChartOpts :: IO ChartOpts
getHledgerChartOpts = processArgs chartmode >>= return . decodeRawOpts >>= toChartOpts >>= checkChartOpts
-- main
main :: IO ()
main = do
chopts <- getHledgerChartOpts
d <- getCurrentDay
j <- defaultJournal
let ropts = (reportopts_ $ cliopts_ chopts)
let balreport = singleBalanceReport ropts (queryFromOpts d ropts) j
let go -- | "--help" `elem` (rawopts_ $ cliopts_ chopts) = putStr (showModeHelp chartmode) >> exitSuccess
-- | "--version" `elem` (rawopts_ $ cliopts_ chopts) = putStrLn progversion >> exitSuccess
| otherwise = withJournalAndChartOptsDo chopts (writeChart balreport)
go
-- copied from hledger-web
withJournalAndChartOptsDo :: ChartOpts -> (ChartOpts -> Journal -> IO ()) -> IO ()
withJournalAndChartOptsDo opts cmd = do
f <- head `fmap` journalFilePathFromOpts (cliopts_ opts)
readJournalFile Nothing Nothing True f >>=
either error' (cmd opts . journalApplyAliases (aliasesFromOpts $ cliopts_ opts))
-- | Generate an image with the pie chart and write it to a file
writeChart :: BalanceReport -> ChartOpts -> Journal -> IO ()
writeChart balreport opts j = do
d <- getCurrentDay
if null $ jtxns j
then putStrLn "This journal has no transactions, can't make a chart." >> exitFailure
else do
let chart = genPie opts balreport
let fileoptions = def -- FileOptions (fromIntegral w, fromIntegral h) SVG loadSansSerifFonts
renderableToFile fileoptions filename (toRenderable chart)
return ()
where
filename = chart_output_ opts
(w,h) = parseSize $ chart_size_ opts
ropts = reportopts_ $ cliopts_ opts
-- | Parse image size from a command-line option
parseSize :: String -> (Int,Int)
parseSize str = (read w, read h)
where
x = fromMaybe (error' "Size should be in WIDTHxHEIGHT format") $ findIndex (=='x') str
(w,_:h) = splitAt x str
-- | Generate pie chart
genPie :: ChartOpts -> BalanceReport -> PieLayout
genPie opts (items, total) = def { _pie_background = solidFillStyle $ opaque $ white
, _pie_plot = pie_chart }
where
pie_chart = def { _pie_data = map (uncurry accountPieItem) chartitems
, _pie_start_angle = (-90)
, _pie_colors = mkColours hue
, _pie_label_style = def{_font_size=12}
}
chartitems = dbg1 "chart" $ top num samesignitems :: [(AccountName, Double)]
(samesignitems, sign) = sameSignNonZero items
top n t = topn ++ [other]
where
(topn,rest) = splitAt n $ reverse $ sortBy (comparing snd) t
other = ("other", sum $ map snd rest)
num = chart_items_ opts
hue = if sign > 0 then red else green where (red, green) = (0, 110)
copts = cliopts_ opts
ropts = reportopts_ copts
-- | Select the nonzero items with same sign as the first, and make
-- them positive. Also return a 1 or -1 corresponding to the original sign.
sameSignNonZero :: [BalanceReportItem] -> ([(AccountName, Double)], Int)
sameSignNonZero is
| null nzs = ([], 1)
| otherwise = (map pos $ filter (test.fourth4) nzs, sign)
where
nzs = filter ((/=0).fourth4) is
pos (acct,_,_,Mixed as) = (acct, abs $ read $ show $ maybe 0 aquantity $ headMay as)
sign = if fourth4 (head nzs) >= 0 then 1 else (-1)
test = if sign > 0 then (>0) else (<0)
-- | Convert all quantities of MixedAccount to a single commodity
-- amountValue :: MixedAmount -> Double
-- amountValue = quantity . mixedAmountWithCommodity unknown
-- | Generate a tree of account names together with their balances.
-- The balance of account is decremented by the balance of its subaccounts
-- which are drawn on the chart.
-- balances :: Tree Account -> Tree (AccountName, Double)
-- balances (Node rootAcc subAccs) = Node newroot newsubs
-- where
-- newroot = (aname rootAcc,
-- amountValue $
-- aibalance rootAcc - (sum . map (aibalance . root)) subAccs)
-- newsubs = map balances subAccs
-- | Build a single pie chart item
accountPieItem :: AccountName -> Double -> PieItem
accountPieItem accname balance = PieItem (T.unpack accname) offset balance where offset = 0
-- | Generate an infinite color list suitable for charts.
mkColours :: Double -> [AlphaColour Double]
mkColours hue = cycle $ [opaque $ rgbToColour $ hsl h s l | (h,s,l) <- liftM3 (,,)
[hue] [0.7] [0.1,0.2..0.7] ]
rgbToColour :: (Fractional a) => RGB a -> Colour a
rgbToColour (RGB r g b) = rgb r g b