packages feed

lhae-0.0.2: src/Controller/Menu/Statistics/Pivot.hs

module Controller.Menu.Statistics.Pivot (eventHandler)
    where

import Control.Monad (when,forM_)
import Data.List (nub)
import Data.Maybe (mapMaybe)
import Controller (Controller,onView,onGridView,onGridModel)
import Controller.Canonical (addColumn,addRow)
import qualified Controller.Grid as Grid
import Controller.Dialog (warning)
import View.Component.Grid (getColumnLabels)
import View.Dialog.Complex (Layout (..),Widget (..),Modifier (..)
                           ,showSimpleDialog,okButton,cancelButton)
import Model.Grid (getColumnValues)
import Model.CellContent (CellValue (NumberValue))
import qualified Model.CellExpression.Evaluator.Math as Math
import Model.CellExpression.Evaluator.Common (fLength,getNumList)
import Util (justWhen)
import I18n (__)

data Aggregator = Count
                | Numeric ([Double] -> Double)

data Parameters = Parameters { abscissaColumn :: Maybe Int
                             , ordinateColumn :: Maybe Int
                             , dataColumn :: Maybe Int
                             , aggregatorIndex :: Int
                             , makeTotals :: Bool
                             }

defaultParams :: Parameters
defaultParams = Parameters Nothing Nothing Nothing 0 True

aggregatorList :: [String]
aggregatorList = [ __ "Sum",__ "Mean",__ "Median",__ "Maximum"
                 , __ "Minimum",__ "Standard Deviation",__ "Count"]

aggregatorFromInt :: Int -> Aggregator
aggregatorFromInt i = 
    case i of 0 -> Numeric sum; 1 -> Numeric Math.mean
              2 -> Numeric Math.median; 3 -> Numeric maximum
              4 -> Numeric minimum; 5 -> Numeric Math.stdDeviation
              6 -> Count

typecheck :: Aggregator -> [CellValue] -> Bool
typecheck aggregator values = 
    case aggregator of
      Numeric _ -> either (\_ -> False) (\_ -> True) $ getNumList values
      _ -> True

apply :: Aggregator -> [CellValue] -> CellValue
apply aggregator values =
    case aggregator of
      Count -> NumberValue $ fLength values
      Numeric f -> let Right vs = getNumList values
                   in NumberValue $ f vs

eventHandler :: Controller ()
eventHandler = eventHandlerWith defaultParams

eventHandlerWith :: Parameters -> Controller ()
eventHandlerWith params = do
  labels <- onGridView getColumnLabels
  result <- onView $ showSimpleDialog (__ "Pivot table") 
               (dialog labels) params
  justWhen result $ \params' -> do success <- processParams params'
                                   when (not success) 
                                            $ eventHandlerWith params'

dialog :: [String] -> Layout Parameters ()
dialog labels =
    let setAbscissa p a = p {abscissaColumn = a}
        setOrdinate p a = p {ordinateColumn = a}
        setData p a = p {dataColumn = a}
        setAggregator p a = p {aggregatorIndex = a}
        setTotals p b = p {makeTotals = b}
    in
      Modifier Margin $ Column 
        [ Row [ Modifiers [Boxed $ __ "Abscissa column",HFill]
                  $ Widget $ SingleListBox labels abscissaColumn setAbscissa
              , Modifiers [Boxed $ __ "Ordinate column",HFill]
                  $ Widget $ SingleListBox labels ordinateColumn setOrdinate
              , Modifiers [Boxed $ __ "Data column",HFill]
                  $ Widget $ SingleListBox labels dataColumn setData]
        , Grid [[ Label $ __ "Aggregator"
                , Widget $ Choice aggregatorList aggregatorIndex setAggregator]
               ,[ Label $ __ "Make totals"
                , Widget $ CheckBox "" makeTotals setTotals]]
        , Modifier Center $ Row [okButton (),cancelButton]]

processParams :: Parameters -> Controller Bool
processParams params = 
    case (abscissaColumn params,ordinateColumn params,dataColumn params) of
      (Nothing,_,_) -> warning (__ "No abscissa column selected") >> return False
      (_,Nothing,_) -> warning (__ "No ordinate column selected") >> return False
      (_,_,Nothing) -> warning (__ "No data column selected") >> return False
      (Just absCol,Just ordCol,Just dataCol) ->
          do abscissa <- onGridModel $ getColumnValues absCol
             ordinate <- onGridModel $ getColumnValues ordCol
             datas <- onGridModel $ getColumnValues dataCol
             calcPivotTable abscissa ordinate datas 
               (aggregatorFromInt $ aggregatorIndex params) 
               (makeTotals params)

calcPivotTable :: [CellValue] -> [CellValue] -> [CellValue] 
               -> Aggregator -> Bool -> Controller Bool
calcPivotTable abscissa ordinate datas aggregator makeTotals = 
    if not $ typecheck aggregator datas 
    then warning (__ "Can not apply aggregator to selected data column (e.g. because of non-numeric data).")
             >> return False
    else do
      Grid.new Nothing

      let association = zip3 abscissa ordinate datas
          nubOrdinate = nub ordinate
          nubAbscissa = nub abscissa
          aggregateSome select =
              let values = mapMaybe (\(a,o,value) -> if select a o 
                                                     then Just value 
                                                     else Nothing
                                    ) association
              in apply aggregator values

          aggregate abscissaValue ordinateValue =
              aggregateSome $ \a o -> a == abscissaValue && o == ordinateValue
          aggregateAbs abscissaValue =
              aggregateSome $ \a _ -> a == abscissaValue
          aggregateOrd ordinateValue =
              aggregateSome $ \_ o -> o == ordinateValue

      forM_ nubAbscissa $ \abscissaValue -> 
          let column = map (show . aggregate abscissaValue) nubOrdinate
          in
            addColumn (Just $ show abscissaValue,column)

      Grid.updateRowLabels $ map show nubOrdinate

      when makeTotals $ 
           let column = map (show . aggregateOrd) nubOrdinate
               row = map (show . aggregateAbs) nubAbscissa
               all = show $ aggregateSome (\_ _ -> True)
           in do
             addColumn (Just $ __ "Total",column)
             addRow (Just $ __ "Total",row ++ [all])
      return True