packages feed

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 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:++![lines.png](https://user-images.githubusercontent.com/284644/63638172-1f134600-c69e-11e9-98ce-4c274ca423f3.png)++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)+