packages feed

granite-0.6.0.0: src/Granite/Data/Frame.hs

{-# LANGUAGE Strict #-}

{- |
Module      : Granite.Data.Frame
Copyright   : (c) 2025
License     : MIT
Maintainer  : mschavinda@gmail.com

Column-oriented data frame used as input to the chart IR.
-}
module Granite.Data.Frame (
    Column (..),
    columnLength,
    columnAsNum,
    columnAsText,
    DataFrame (..),
    emptyFrame,
    addColumn,
    lookupColumn,
    columnNames,
    frameLength,
    fromColumns,
    filterByRows,
    uniqueText,
    pickRows,
) where

import Data.Int (Int64)
import Data.List qualified as List
import Data.Text (Text)
import Data.Text qualified as Text

data Column
    = ColNum ![Double]
    | ColCat ![Text]
    | ColTime ![Int64]
    | ColBool ![Bool]
    deriving (Eq, Show, Read)

columnLength :: Column -> Int
columnLength col = case col of
    ColNum xs -> length xs
    ColCat xs -> length xs
    ColTime xs -> length xs
    ColBool xs -> length xs

columnAsNum :: Column -> Maybe [Double]
columnAsNum col = case col of
    ColNum xs -> Just xs
    ColBool xs -> Just (map (\b -> if b then 1 else 0) xs)
    ColTime xs -> Just (map fromIntegral xs)
    ColCat _ -> Nothing

columnAsText :: Column -> [Text]
columnAsText col = case col of
    ColCat xs -> xs
    ColNum xs -> map showD xs
    ColTime xs -> map (Text.pack . show) xs
    ColBool xs -> map (Text.pack . show) xs
  where
    showD :: Double -> Text
    showD d = Text.pack (show d)

newtype DataFrame = DataFrame {unDataFrame :: [(Text, Column)]}
    deriving (Eq, Show, Read)

emptyFrame :: DataFrame
emptyFrame = DataFrame []

addColumn :: Text -> Column -> DataFrame -> DataFrame
addColumn name col (DataFrame xs)
    | any ((== name) . fst) xs =
        DataFrame [(n, if n == name then col else c) | (n, c) <- xs]
    | otherwise = DataFrame (xs ++ [(name, col)])

lookupColumn :: Text -> DataFrame -> Maybe Column
lookupColumn name (DataFrame xs) = lookup name xs

columnNames :: DataFrame -> [Text]
columnNames (DataFrame xs) = map fst xs

frameLength :: DataFrame -> Int
frameLength (DataFrame xs) = foldr (max . columnLength . snd) 0 xs

fromColumns :: [(Text, Column)] -> DataFrame
fromColumns = DataFrame

pickRows :: [Int] -> Column -> Column
pickRows ixs col = case col of
    ColNum xs -> ColNum (pick xs)
    ColCat xs -> ColCat (pick xs)
    ColTime xs -> ColTime (pick xs)
    ColBool xs -> ColBool (pick xs)
  where
    pick :: [a] -> [a]
    pick xs = [xs !! i | i <- ixs, i < length xs]

filterByRows :: [Int] -> DataFrame -> DataFrame
filterByRows ixs (DataFrame cols) =
    DataFrame [(n, pickRows ixs c) | (n, c) <- cols]

uniqueText :: [Text] -> [Text]
uniqueText = List.nub