chart-cli (empty) → 0.1.0.0
raw patch · 10 files changed
+763/−0 lines, 10 filesdep +Chartdep +Chart-cairodep +attoparsecsetup-changed
Dependencies added: Chart, Chart-cairo, attoparsec, base, colour, data-default-class, dates, filepath, hashable, lens, optparse-applicative, text, time
Files
- ChangeLog.md +3/−0
- LICENSE +30/−0
- README.md +132/−0
- Setup.hs +2/−0
- chart-cli.cabal +55/−0
- src/Chart.hs +143/−0
- src/CmdLine.hs +152/−0
- src/Main.hs +47/−0
- src/Parser.hs +90/−0
- src/Types.hs +109/−0
+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for hchart++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2019++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * 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.++ * Neither the name of Author name here nor the names of other+ 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 COPYRIGHT+OWNER 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.md view
@@ -0,0 +1,132 @@+chart-cli README+================++This repo provides a `chart` command-line utility. `chart` is aimed to generate+basic 2D charts from command line easily.++The `chart` utility is not aimed to give full control over generated charts, or+to generate all possible types of charts - you may anytime do that by use of+[gnuplot][1] or [Chart package][2]. This utility, instead, gives you a very+quick way of generating some most useful types of charts.++Usage example:++```+$ cat > input.txt+X Y1 Y2+1 1 3+2 4 6+3 7 4+4 5 8+6 3 2+^D++$ chart --header -o lines.png input.txt+```++Result:++++Supported chart types for now are:++* Line charts (the default one)+* Bar charts (clustered and stacked)+* Area charts+* Point charts++Other chart types may be added later.++Supported output formats are:+ +* PNG (the default one)+* SVG+* PS (PostScript)+* PDF++Output file format is automatically detected by specified output file name. If+output file name is not specified explicitly, `chart.png` will be used.++Expected input format+---------------------++Input data are expected to be provided as a series of lines; each line may+represent either one point to plot, or several points at the same X coordinate.++The first line may optionally represent names of the columns.++Other lines should consist of several numbers. Numbers are separated with a+delimiter. Default delimiter is TAB characater; you may specify another with+`-d` command line option.++If only one number per line is provided, then these numbers will be used as Y+values; for X values, line numbers (starting with 1) will be used.++If more than one number per line is provided, then for most chart types (except+for bar charts) the first column will be used as X values, and other will be+used as Y values.++In the first column, date/time values may be provided instead of numbers (to be+used as values along X axis). Supported date/time formats are:++* DD.MM.YYYY+* YYYY/MM/DD+* `12 September 2012'+* today, tomorrow, yesterday+* `in 2 days', '3 weeks ago'+* `last monday', 'next friday'+* `last month' (1th of this month), `next year' (1th of January of next year)++(thanks to [dates package][3]).++Command-line interface+----------------------++I'll put it here for quick reference; more actual information is always+accessible with `chart --help`:++```+Usage: chart [-o|--output OUTPUT.png] [COMMAND] [-1|--header]+ [-d|--delimiter CHAR] [-i|--index] [-t|--title TITLE]+ [-w|--width WIDTH] [-h|--height HEIGHT] [-b|--background COLOR]+ [-f|--foreground COLOR] [-L|--legend ON|OFF] [INPUT.txt]+ Make a chart++Available options:+ -o,--output OUTPUT.png write output to OUTPUT.png+ -1,--header first line contains column headers+ -d,--delimiter CHAR specify fields delimiter ('\t' by default)+ -i,--index if enabled, treat input data as if there was an+ additional first column, containing line numbers,+ starting from 1+ -t,--title TITLE set chart title to TITLE+ -w,--width WIDTH specify chart width, in pixels (default: 800)+ -h,--height HEIGHT specify chart height, in pixels (default: 600)+ -b,--background COLOR specify background color name (see SVG 1.1 spec)+ -f,--foreground COLOR specify foreround color name (see SVG 1.1 spec)+ -L,--legend ON|OFF enable or disable the legend (default: True)+ -h,--help Show this help text++Available commands:+ line Make a line chart+ area Make an area chart+ points Make a points chart+ bar Make a bar chart+```++Installation+------------++Install it by stack:++```+$ sudo apt-get install stack+$ git clone https://github.com/portnov/chart-cli.git+$ cd chart-cli/+$ stack install+```++[1]: http://www.gnuplot.info/+[2]: http://hackage.haskell.org/package/Chart+[3]: http://hackage.haskell.org/package/dates+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ chart-cli.cabal view
@@ -0,0 +1,55 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: bfc69b38044f97f2a32e60cf1bd54c96f8880da1ba3b3b6c1f3525368808b237++name: chart-cli+version: 0.1.0.0+synopsis: Command-line utility to draw charts from input data easily+description: Please see the README on GitHub at <https://github.com/portnov/chart-cli#readme>+category: Graphics+homepage: https://github.com/portnov/chart-cli#readme+bug-reports: https://github.com/portnov/chart-cli/issues+author: Ilya V. Portnov+maintainer: portnov84@rambler.ru+copyright: 2019 Ilya V. Portnov+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ ChangeLog.md++source-repository head+ type: git+ location: https://github.com/portnov/chart-cli++executable chart+ main-is: Main.hs+ other-modules:+ Chart+ CmdLine+ Parser+ Types+ Paths_chart_cli+ hs-source-dirs:+ src+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ Chart+ , Chart-cairo+ , attoparsec+ , base >=4.7 && <5+ , colour+ , data-default-class+ , dates+ , filepath+ , hashable+ , lens+ , optparse-applicative+ , text+ , time+ default-language: Haskell2010
+ src/Chart.hs view
@@ -0,0 +1,143 @@+{-# LANGUAGE RecordWildCards #-}++module Chart where++import Control.Lens+import qualified Data.Text as T+import Data.Colour+import Data.Colour.SRGB+import Data.Colour.RGBSpace.HSL+import Data.Word+import Data.Hashable+import Data.List+import Data.Default.Class+import Graphics.Rendering.Chart++import Types++nameColor :: T.Text -> Colour Double+nameColor name =+ let h = fromIntegral (hash name `mod` 255) :: Word8+ hue = fromIntegral (hash name `mod` 360)+ r = (fromIntegral h / 255)+ v = (1 - r) * 0.5 + r*0.9++ hslColor = hsl hue 0.5 v+ in sRGB (channelRed hslColor) (channelGreen hslColor) (channelBlue hslColor)++toPairs :: [[Value]] -> [[(Value, Double)]]+toPairs = transpose . zipWith go [1..]+ where+ go i [y] = [(Number (fromIntegral i), toDouble y)]+ go _ [x, y] = [(x, toDouble y)]+ go _ (x : ys) = [(x, toDouble y) | y <- ys]++toSeries :: [[Value]] -> [(Value, [Double])]+toSeries = zipWith go [1..]+ where+ go _ (Date dt : ys) = (Date dt, map toDouble ys)+ go i ys = (Index (PlotIndex i), map toDouble ys)++toAreas :: [[Value]] -> [[(Value, (Double, Double))]]+toAreas = transpose . go 1+ where+ go :: Int -> [[Value]] -> [[(Value, (Double, Double))]]+ go _ [] = []+ go i ([y] : rest) = [(Number (fromIntegral i), (0, toDouble y))] : go (i+1) rest+ go i ((x : ys) : rest) =+ let runningSums = tail $ scanl (+) 0 $ map toDouble ys+ pairs = zip (0 : runningSums) runningSums+ in [(x, pair) | pair <- pairs] : go (i+1) rest++mkLineStyle :: T.Text -> LineStyle+mkLineStyle name =+ line_color .~ opaque (darken 0.2 $ nameColor name) $ def++mkFillStyle :: T.Text -> FillStyle+mkFillStyle name =+ fill_color .~ opaque (nameColor name) $ def++mkPointStyle :: T.Text -> PointStyle+mkPointStyle name =+ point_color .~ opaque (nameColor name)+ $ point_border_color .~ opaque (darken 0.2 $ nameColor name)+ $ point_border_width .~ 1+ $ point_radius .~ 4+ $ def++tailOrSingle :: [a] -> [a]+tailOrSingle [] = []+tailOrSingle [x] = [x]+tailOrSingle (x:xs) = xs++convertChart :: ChartConfig -> ChartData -> AnyChart+convertChart Line cht =+ LineChart [+ plot_lines_title .~ T.unpack title+ $ plot_lines_style .~ mkLineStyle title+ $ plot_lines_values .~ [pairs]+ $ def+ | (pairs, NumberColumn title) <- zip (toPairs (chtValues cht)) (tailOrSingle $ chtColumns cht)+ ]+convertChart (Bar {..}) cht =+ BarChart $+ plot_bars_titles .~ [T.unpack title | NumberColumn title <- chtColumns cht]+ $ plot_bars_item_styles .~ [(mkFillStyle title, Just (mkLineStyle title)) | NumberColumn title <- chtColumns cht]+ $ plot_bars_style .~ barStyle+ $ plot_bars_values .~ toSeries (chtValues cht)+ $ def+convertChart Area cht =+ AreaChart [+ plot_fillbetween_title .~ T.unpack title+ $ plot_fillbetween_style .~ mkFillStyle title+ $ plot_fillbetween_values .~ area+ $ def+ | (area, NumberColumn title) <- zip (toAreas (chtValues cht)) (tailOrSingle $ chtColumns cht)+ ]+convertChart Points cht =+ PointsChart [+ plot_points_title .~ T.unpack title+ $ plot_points_style .~ mkPointStyle title+ $ plot_points_values .~ pairs+ $ def+ | (pairs, NumberColumn title) <- zip (toPairs (chtValues cht)) (tail $ chtColumns cht)+ ]++chartToPlots :: AnyChart -> [Plot Value Double]+chartToPlots (LineChart plots) = map toPlot plots+chartToPlots (BarChart plot) = [plotBars plot]+chartToPlots (AreaChart plots) = map toPlot plots+chartToPlots (PointsChart plots) = map toPlot plots++makeChart :: ChartConfig -> ChartData -> Layout Value Double+makeChart chtype cht =+ let title = chtTitle cht+ foreground = chtForeground cht+ background = chtBackground cht+ setFontStyle = font_color .~ opaque foreground+ setLineColor = line_color .~ opaque foreground+ gridStyle = dashedLine 1 [5, 5] $ dissolve 0.5 $ opaque foreground+ setAxisColor =+ laxis_style %~ (axis_label_style %~ setFontStyle) .+ (axis_line_style %~ setLineColor) .+ (axis_grid_style .~ gridStyle)+ + setLegendFont = legend_label_style %~ setFontStyle+ setBackground = fill_color .~ opaque background++ yAxis = setAxisColor def+ xAxis = setAxisColor def++ legend = if chtLegend cht+ then Just $ setLegendFont $ legend_orientation .~ LORows 4 $ def+ else Nothing++ in layout_title .~ T.unpack title+ $ layout_background %~ setBackground+ $ layout_title_style %~ setFontStyle+ $ layout_x_axis .~ xAxis+ $ layout_y_axis .~ yAxis+ $ layout_legend .~ legend+ $ layout_plots .~ chartToPlots (convertChart chtype cht)+ $ def+
+ src/CmdLine.hs view
@@ -0,0 +1,152 @@++module CmdLine where++import qualified Data.Text as T+import Data.Char (toUpper)+import Data.Colour+import Data.Colour.Names+import Options.Applicative+import Graphics.Rendering.Chart+import Graphics.Rendering.Chart.Backend.Cairo+import System.FilePath++import Types++pCmdLine :: Parser CmdLine+pCmdLine =+ CmdLine+ <$> strOption+ ( long "output"+ <> short 'o'+ <> metavar "OUTPUT.png"+ <> value "chart.png"+ <> help "write output to OUTPUT.png"+ )+ <*> pChart+ <*> pParseOpts+ <*> (optional $ strOption+ ( long "title"+ <> short 't'+ <> metavar "TITLE"+ <> help "set chart title to TITLE"+ )+ )+ <*> option auto+ ( long "width"+ <> short 'w'+ <> value 800+ <> metavar "WIDTH"+ <> showDefault+ <> help "specify chart width, in pixels"+ )+ <*> option auto+ ( long "height"+ <> short 'h'+ <> value 600+ <> metavar "HEIGHT"+ <> showDefault+ <> help "specify chart height, in pixels"+ )+ <*> option colour+ ( long "background"+ <> short 'b'+ <> value white+ <> metavar "COLOR"+ <> help "specify background color name (see SVG 1.1 spec)"+ )+ <*> option colour+ ( long "foreground"+ <> short 'f'+ <> value black+ <> metavar "COLOR"+ <> help "specify foreround color name (see SVG 1.1 spec)"+ )+ <*> option bool+ ( long "legend"+ <> short 'L'+ <> value True+ <> metavar "ON|OFF"+ <> showDefault+ <> help "enable or disable the legend"+ )+ <*> (optional $ strArgument (metavar "INPUT.txt"))++colour :: ReadM (Colour Double)+colour = maybeReader readColourName++bool :: ReadM Bool+bool = maybeReader $ \str -> + case map toUpper str of+ "TRUE" -> Just True+ "ON" -> Just True+ "YES" -> Just True+ "Y" -> Just True+ "FALSE" -> Just False+ "OFF" -> Just False+ "NO" -> Just False+ "N" -> Just False+ _ -> Nothing++pChart :: Parser ChartConfig+pChart =+ hsubparser (+ command "line" (info (pure Line) (progDesc "Make a line chart"))+ <> command "area" (info (pure Area) (progDesc "Make an area chart"))+ <> command "points" (info (pure Points) (progDesc "Make a points chart"))+ <> command "bar" (info pBar (progDesc "Make a bar chart"))+ )+ <|> pure Line++pBar :: Parser ChartConfig+pBar =+ Bar+ <$> ( flag' BarsStacked+ (long "stack"+ <> help "stack bars of different columns on top of each other (by default)")+ <|> flag' BarsClustered+ (long "cluster"+ <> help "place bars of different columns near each other")+ <|> pure BarsStacked+ )++pParseOpts :: Parser ParseOptions+pParseOpts =+ ParseOptions+ <$> switch+ ( long "header"+ <> short '1'+ <> help "first line contains column headers"+ )+ <*> (T.pack <$> strOption+ ( long "delimiter"+ <> short 'd'+ <> metavar "CHAR"+ <> value "\t"+ <> help "specify fields delimiter ('\\t' by default)"+ )+ )+ <*> switch+ ( long "index"+ <> short 'i'+ <> showDefault+ <> help "if enabled, treat input data as if there was an additional first column, containing line numbers, starting from 1"+ )+ +parseCmdLine :: IO CmdLine+parseCmdLine = execParser opts+ where+ opts = info (pCmdLine <**> helper)+ ( fullDesc+ <> progDesc "Make a chart"+ <> header "chart - plot charts from input data"+ )++detectFormat :: FilePath -> FileFormat+detectFormat path =+ case map toUpper (takeExtension path) of+ ".PNG" -> PNG+ ".SVG" -> SVG+ ".PS" -> PS+ ".PDF" -> PDF+ ext -> error $ "unsupported output file format: " ++ ext+
+ src/Main.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Control.Lens+import qualified Data.Text as T+import Data.Default.Class+import Graphics.Rendering.Chart+import Graphics.Rendering.Chart.Backend.Cairo+import System.Environment+import System.FilePath++import Types+import Chart+import Parser+import CmdLine++main :: IO ()+main = do+ cmd <- parseCmdLine+-- print cmd+ let path = cmdInput cmd+ outPath = cmdOutput cmd+ let opts = cmdParse cmd+ cht <- parseFile opts path+-- print $ toPairs $ chtValues cht+ + let cht' =+ case cmdTitle cmd of+ Nothing -> cht+ Just title -> cht {chtTitle = T.pack title}++ chart = cht' {+ chtForeground = cmdForeground cmd+ , chtBackground = cmdBackground cmd+ , chtLegend = cmdLegend cmd+ }++ let width = cmdWidth cmd+ height = cmdHeight cmd+ fileOpts = fo_size .~ (width, height)+ $ fo_format .~ detectFormat outPath+ $ def++ renderableToFile fileOpts outPath $ toRenderable $ makeChart (cmdChart cmd) chart+ return ()+
+ src/Parser.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE OverloadedStrings #-}++module Parser where++import Control.Monad+import Data.Colour.Names+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import qualified Data.Attoparsec.Text as A+import Data.Dates+import System.IO.Unsafe (unsafePerformIO)++import Types++now :: DateTime+now = unsafePerformIO getCurrentDateTime++parseText :: ParseOptions -> T.Text -> T.Text -> Either String ChartData+parseText opts title text = do+ let lines = T.lines text+ splitLine line = T.splitOn (poSeparator opts) line++ inputColsCount = length (splitLine firstLine)++ parseLine :: Int -> T.Text -> Either String [Value]+ parseLine lineNo line =+ let items = splitLine line+ in if length items == inputColsCount+ then zipWithM (parseValue lineNo) [1..] items+ else Left $ "Line " ++ show lineNo ++ ": number of columns is not equal to number of columns in the first line."++ parseValue lineNo colNo s =+ case A.parseOnly (A.double <* A.endOfInput) s of+ Right value -> Right $ Number value+ Left numberErr ->+ case parseDateTime now (T.unpack s) of+ Right dt -> Right $ Date $ toLocalTime dt+ Left dateErr -> Left $ "Line " ++ show lineNo ++ ", column " ++ show colNo ++ ":\n" +++ "Can't parse `" ++ T.unpack s +++ "` as a number: " ++ numberErr +++ ";\nCan't parse it as a date/time value: " ++ show dateErr++ firstLine = head lines++ defHeader n = "Column " <> T.pack (show n)++ dataLines =+ if poHeader opts+ then tail lines+ else lines++ inputValues <- zipWithM parseLine [1..] dataLines++ let values = if poIndex opts+ then zipWith (:) (map Index [1..]) inputValues+ else inputValues+ + colsCount =+ if poIndex opts+ then inputColsCount + 1+ else inputColsCount++ columnNames =+ if poHeader opts+ then splitLine firstLine+ else map defHeader [0 .. colsCount - 1]++ columns = map NumberColumn columnNames++ return $ ChartData {+ chtTitle = title+ , chtColumns = columns+ , chtBackground = white+ , chtForeground = black+ , chtLegend = True+ , chtValues = values+ }++parseFile :: ParseOptions -> Maybe FilePath -> IO ChartData+parseFile opts mbPath = do+ text <- case mbPath of+ Nothing -> TIO.getContents+ Just path -> TIO.readFile path+ let title = case mbPath of+ Nothing -> "stdin"+ Just path -> T.pack path+ case parseText opts title text of+ Right chart -> return chart+ Left err -> fail err+
+ src/Types.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE StandaloneDeriving #-}+module Types where++import qualified Data.Text as T+import Data.Dates+import Data.Time+import Data.Time.Calendar+import Data.Time.LocalTime+import Data.Colour+import Graphics.Rendering.Chart++data ParseOptions = ParseOptions {+ poHeader :: Bool+ , poSeparator :: T.Text+ , poIndex :: Bool+ }+ deriving (Eq, Show)++data Value =+ Number Double+ | Index PlotIndex+ | Date { toTime :: LocalTime }+ deriving (Eq, Show, Ord)++toDouble :: Value -> Double+toDouble (Number x) = x+toDouble (Index i) = toValue i+toDouble (Date dt) = error $ "can't convert date/time to double"++toIndex :: Value -> PlotIndex+toIndex (Number x) = PlotIndex $ round x+toIndex (Index i) = i+toINdex (Date dt) = error $ "can't convert date/time to index"++data Column =+ NumberColumn T.Text+ | DateColumn T.Text+ deriving (Eq, Show)++data ChartData = ChartData {+ chtTitle :: T.Text+ , chtBackground :: Colour Double+ , chtForeground :: Colour Double+ , chtLegend :: Bool+ , chtColumns :: [Column]+ , chtValues :: [[Value]]+ }+ deriving (Eq, Show)++deriving instance Eq PlotBarsStyle++data ChartConfig =+ Line+ | Bar { barStyle :: PlotBarsStyle }+ | Area+ | Points+ deriving (Eq, Show)++data AnyChart =+ LineChart [PlotLines Value Double]+ | BarChart (PlotBars Value Double)+ | AreaChart [PlotFillBetween Value Double]+ | PointsChart [PlotPoints Value Double]++toLocalTime :: DateTime -> LocalTime+toLocalTime dt =+ let localDay = fromGregorian (fromIntegral $ year dt) (month dt) (day dt)+ time = TimeOfDay (hour dt) (minute dt) (fromIntegral $ second dt)+ in LocalTime localDay time++mapAxisData :: (x -> y) -> (y -> x) -> AxisData x -> AxisData y+mapAxisData to from axis =+ axis {+ _axis_viewport = \range x -> _axis_viewport axis range (from x)+ , _axis_tropweiv = \range d -> to (_axis_tropweiv axis range d)+ , _axis_ticks = [(to x, d) | (x, d) <- _axis_ticks axis]+ , _axis_labels = [[(to x, s) | (x, s) <- pairs] | pairs <- _axis_labels axis]+ , _axis_grid = map to (_axis_grid axis)+ }++instance PlotValue Value where+ toValue (Number x) = x+ toValue (Date dt) = toValue dt+ toValue (Index i) = toValue i++ fromValue x = Number x+ + autoAxis = \list ->+ case list of+ [] -> mapAxisData Number toDouble $ autoAxis $ map toDouble list+ (Number x : _) -> mapAxisData Number toDouble $ autoAxis $ map toDouble list+ (Date dt : _) -> mapAxisData Date toTime $ autoAxis $ map toTime list+ (Index i : _) -> mapAxisData Index toIndex $ autoAxis $ map toIndex list++data CmdLine = CmdLine {+ cmdOutput :: FilePath+ , cmdChart :: ChartConfig+ , cmdParse :: ParseOptions+ , cmdTitle :: Maybe String+ , cmdWidth :: Int+ , cmdHeight :: Int+ , cmdBackground :: Colour Double+ , cmdForeground :: Colour Double+ , cmdLegend :: Bool+ , cmdInput :: Maybe FilePath+ }+ deriving (Show)+