packages feed

ploterific-0.2.0.0: src/Ploterific/Plot/Plot.hs

{- Ploterific.Plot.Plot
Gregory W. Schwartz

Collects the functions pertaining to the plotting of figures.
-}

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}

module Ploterific.Plot.Plot
  ( plot
  ) where

-- Remote
import Control.Monad.Reader (ReaderT (..), asks, liftIO)
import Data.Bool (bool)
import Data.Char (ord)
import Data.Colour.Palette.BrewerSet ( brewerSet, ColorCat (..) )
import Data.Colour.Palette.Harmony (colorRamp)
import Data.Colour.SRGB (sRGB24show)
import Data.Either (rights)
import Data.List (foldl')
import Data.Maybe (fromMaybe, isJust)
import GHC.Natural (intToNatural)
import qualified Control.Lens as L
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.ByteString.Char8 as B
import qualified Data.Csv.Streaming as CSVStream
import qualified Data.Csv as CSV
import qualified Data.Foldable as F
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Data.Text.Lazy.IO as TL
import qualified Data.Text.Read as T
import qualified Graphics.Vega.VegaLite as VL
import qualified Graphics.Vega.VegaLite.Theme as VL

-- Local
import Ploterific.Plot.Types

-- | Split the measurement of a feature.
splitColMeasure :: T.Text -> Either T.Text (T.Text, VL.Measurement)
splitColMeasure feature = splitOrNot . T.breakOnEnd ":" $ feature
  where
    splitOrNot ("", _) = Left feature
    splitOrNot x = Right $ trueSplit x
    trueSplit = L.over L._1 (T.dropEnd 1) . L.over L._2 toMeasurement
    toMeasurement "N" = VL.Nominal
    toMeasurement "O" = VL.Ordinal
    toMeasurement "Q" = VL.Quantitative
    toMeasurement "T" = VL.Temporal

-- | Get the column name even with the measurement.
getColName :: T.Text -> T.Text
getColName = either id fst . splitColMeasure

-- | Get the measurement.
getColMeasurement :: T.Text -> Maybe VL.Measurement
getColMeasurement = either (const Nothing) (Just . snd) . splitColMeasure

-- | Parse rows of a CSV.
loadCsv :: Delimiter -> BL.ByteString -> [Map.Map T.Text T.Text]
loadCsv (Delimiter d) =
  either error (F.toList . snd)
    . CSVStream.decodeByNameWith ( CSV.defaultDecodeOptions
                                 { CSV.decDelimiter = fromIntegral (ord d) }
                                 )


-- | Convert rows to data columns.
rowsToDataColumns :: Maybe Color
                  -> Maybe Facet
                  -> [Feature]
                  -> [Map.Map T.Text T.Text]
                  -> [VL.DataColumn]
                  -> VL.Data
rowsToDataColumns color facet fs rows =
  VL.dataFromColumns []
    . maybe
        id
        (\ (Color x)
        -> VL.dataColumn
            (getColName x)
            (numOrString (textToString x) x rows)
        )
        color
    . maybe
        id
        (\ (Facet x)
        -> VL.dataColumn
            (getColName x)
            (numOrString (textToString x) x rows)
        )
        facet
    . foldl'
        (\ acc (Feature x)
        -> VL.dataColumn
            (getColName x)
            (numOrString (textToNumbers x) x rows)
         . acc
        )
        id
        fs
  where
    numOrString :: ([Map.Map T.Text T.Text] -> VL.DataValues) -> T.Text -> ([Map.Map T.Text T.Text] -> VL.DataValues)
    numOrString def x =
      maybe
          def
          (bool (textToString x) (textToNumbers x) . (== VL.Quantitative))
        . getColMeasurement
        $ x
    textToNumbers x = VL.Numbers
                    . fmap fst
                    . rights
                    . fmap ( maybe
                              ( error
                              $ "Column not in table: " <> show (getColName x))
                              T.double
                           . Map.lookup (getColName x)
                           )
    textToString x = VL.Strings . fmap (fromMaybe "" . Map.lookup (getColName x))

-- | Get color encoding for color ramps.
labelColorScale :: [ColorLabel] -> VL.MarkChannel
labelColorScale cs = VL.MScale [ VL.SDomain (VL.DStrings labels)
                               , VL.SRange (VL.RStrings colors)
                               ]
  where
    labels =
      Set.toAscList . Set.fromList . fmap unColorLabel $ cs
    colors =
      fmap (T.pack . sRGB24show) . colorRamp (length labels) . brewerSet Set1 $ 9

-- | Get the encoding.
enc :: Maybe (Color, [ColorLabel])
    -> [Feature]
    -> [VL.EncodingSpec]
    -> VL.PropertySpec
enc colorInfo fs =
  VL.encoding
    . maybe
        id
        (\ (Color c, ls)
        -> VL.color ( [VL.MName . getColName $ c, labelColorScale ls]
                   <> maybe [] (\x -> [VL.MmType x]) (getColMeasurement c)
                    )
        )
        colorInfo
    . VL.tooltips
        ( fmap (\ (Feature !f)
               -> [ VL.TName . getColName $ f ]
                 <> maybe [] (\x -> [VL.TmType x]) (getColMeasurement f)
               )
          fs
       <> [ maybe
              []
              (\ (Color c, _)
              -> [VL.TName . getColName $ c]
              <> maybe [] (\x -> [VL.TmType x]) (getColMeasurement c)
              )
              colorInfo
          ]
        )
    . foldl'
        (\ acc (!p, Feature !f)
        -> VL.position
            p
            ( [VL.PName . getColName $ f]
             <> maybe [] (\x -> [VL.PmType x]) (getColMeasurement f)
            )
         . acc
        )
        id
        (zip pos fs)
  where
    pos = [VL.X, VL.Y]

-- | Render plot.
plot :: ReaderT Opts IO ()
plot = do
  input' <- asks (maybe BL.getContents (BL.readFile . unInput) . _input)
  output' <- asks (maybe TL.putStrLn (TL.writeFile . unOutput) . _output)
  height' <- asks _height
  width' <- asks _width
  mark' <- asks _mark
  color' <- asks _color
  features' <- asks _features
  facet' <- asks _facet
  facetNum' <- asks _facetNum
  delimiter' <- asks _delimiter

  contents <- liftIO input'

  let rows = loadCsv delimiter' contents
      dataSet =
        rowsToDataColumns color' facet' features' $ rows
      colorLabels =
        fmap
          (\ (Color c)
          -> fmap (ColorLabel . fromMaybe "" . Map.lookup (getColName c)) rows
          )
          color'
      colorInfo = (,) <$> color' <*> colorLabels
      facetSpec (Facet x) = [ VL.facetFlow
                            $ [VL.FName . getColName $ x]
                           <> maybe [] (\y -> [VL.FmType y]) (getColMeasurement x)
                            ]
      plotSpec = [ enc colorInfo features' []
                 , VL.mark mark' []
                 , VL.selection . VL.select "view" VL.Interval [VL.BindScales]
                 $ []
                 ]
      p       = VL.toVegaLite
              $   [ dataSet []
                  , VL.theme VL.defaultConfig []
                  ]
                 <> bool plotSpec [VL.specification . VL.asSpec $ plotSpec] (isJust facet')
                 <> maybe [] (\(Height x) -> [VL.height x]) height'
                 <> maybe [] (\(Width x) -> [VL.width x]) width'
                 <> maybe [] facetSpec facet'
                 <> maybe [] (\(FacetNum x) -> [VL.columns $ intToNatural x]) facetNum'

  liftIO . output' $ VL.toHtml p

  return ()