{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoFieldSelectors #-}

{- |
A plotly-express-style one-shot plotting API for terminal output.

Each chart kind has a small record spec (with sensible defaults) and a
single render function. Customise plots by record-update on the spec:

@
bar ((mkBar "Sex") { y = Just "Survived" }) titanic
bar ((mkBar "ocean_proximity")
      { y = Just "median_house_value", agg = Mean }) housing
@
-}
module DataFrame.Display.Terminal.Plot (
    -- * Aggregation
    Agg (..),

    -- * Bar charts
    Bar (..),
    mkBar,
    bar,

    -- * Histograms
    Histogram (..),
    mkHistogram,
    histogram,

    -- * Scatter plots
    Scatter (..),
    mkScatter,
    scatter,

    -- * Line charts
    Line (..),
    mkLine,
    line,

    -- * Pie charts
    Pie (..),
    mkPie,
    pie,

    -- * Box plots
    Box (..),
    mkBox,
    box,

    -- * Whole-frame plots
    heatmap,
    correlationMatrix,

    -- * Deprecated legacy entry points

    {- |
    These shims delegate to the parametrized API. They are kept for
    one release to ease migration; prefer the spec-based functions
    above for new code.
    -}
    plotHistogram,
    plotScatter,
    plotBars,
    plotLines,
    plotPie,
    plotBoxPlots,
    plotStackedBars,
    plotHeatmap,
    extractNumericColumn,
    extractStringColumn,
) where

import Control.Monad (forM)
import qualified Data.List as L
import qualified Data.Text as T
import qualified Data.Text.IO as T
import GHC.Stack (HasCallStack)

import DataFrame.Display.Internal.Common (
    Agg (..),
    aggLabel,
    aggregateByGroup,
    extractNumericColumn,
    extractStringColumn,
    groupWithOther,
    groupWithOtherForPie,
    isNumericColumn,
 )
import DataFrame.Internal.DataFrame (DataFrame (..), columnNames)
import Granite (Plot, defPlot)
import qualified Granite

-- ---------------------------------------------------------------------------
-- Bar
-- ---------------------------------------------------------------------------

{- | Bar-chart specification.

@x@ is required; everything else has a default. When @y@ is @Nothing@ the
chart counts rows per group; otherwise @y@ is aggregated using @agg@.
-}
data Bar = Bar
    { Bar -> Text
x :: T.Text
    -- ^ Grouping (categorical) column.
    , Bar -> Maybe Text
y :: Maybe T.Text
    -- ^ Value column; @Nothing@ counts rows.
    , Bar -> Agg
agg :: Agg
    -- ^ Aggregation applied to @y@. Defaults to 'Sum'.
    , Bar -> Maybe Int
topN :: Maybe Int
    -- ^ Optional cap on the number of bars (rest folded into "Other").
    , Bar -> Maybe Text
title :: Maybe T.Text
    -- ^ Chart title; auto-generated if @Nothing@.
    , Bar -> Plot
plot :: Plot
    -- ^ Granite plot configuration (size, palette, ticks, etc.).
    }

-- | Smart constructor: spec with sensible defaults, given the @x@ column.
mkBar :: T.Text -> Bar
mkBar :: Text -> Bar
mkBar Text
c =
    Bar
        { x :: Text
x = Text
c
        , y :: Maybe Text
y = Maybe Text
forall a. Maybe a
Nothing
        , agg :: Agg
agg = Agg
Sum
        , topN :: Maybe Int
topN = Maybe Int
forall a. Maybe a
Nothing
        , title :: Maybe Text
title = Maybe Text
forall a. Maybe a
Nothing
        , plot :: Plot
plot = Plot
defPlot
        }

-- | Render a 'Bar' spec to stdout.
bar :: (HasCallStack) => Bar -> DataFrame -> IO ()
bar :: HasCallStack => Bar -> DataFrame -> IO ()
bar Bar
spec DataFrame
df = do
    let effectiveAgg :: Agg
effectiveAgg = case Bar
spec.y of
            Maybe Text
Nothing -> Agg
Count
            Just Text
_ -> Bar
spec.agg
        rows :: [(Text, Double)]
rows = HasCallStack =>
Agg -> Text -> Maybe Text -> DataFrame -> [(Text, Double)]
Agg -> Text -> Maybe Text -> DataFrame -> [(Text, Double)]
aggregateByGroup Agg
effectiveAgg Bar
spec.x Bar
spec.y DataFrame
df
        rows' :: [(Text, Double)]
rows' = [(Text, Double)]
-> (Int -> [(Text, Double)]) -> Maybe Int -> [(Text, Double)]
forall b a. b -> (a -> b) -> Maybe a -> b
maybe [(Text, Double)]
rows (Int -> [(Text, Double)] -> [(Text, Double)]
`groupWithOther` [(Text, Double)]
rows) Bar
spec.topN
        t :: Text
t = case Bar
spec.title of
            Just Text
s -> Text
s
            Maybe Text
Nothing -> Agg -> Maybe Text -> Text -> Text
autoTitle Agg
effectiveAgg Bar
spec.y Bar
spec.x
        cfg :: Plot
cfg = Bar
spec.plot{Granite.plotTitle = t}
    Text -> IO ()
T.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ [(Text, Double)] -> Plot -> Text
Granite.bars [(Text, Double)]
rows' Plot
cfg

-- ---------------------------------------------------------------------------
-- Histogram
-- ---------------------------------------------------------------------------

-- | Histogram specification for a numeric column.
data Histogram = Histogram
    { Histogram -> Text
x :: T.Text
    -- ^ Numeric column to bin.
    , Histogram -> Int
bins :: Int
    -- ^ Number of bins (default 30).
    , Histogram -> Maybe Text
title :: Maybe T.Text
    , Histogram -> Plot
plot :: Plot
    }

mkHistogram :: T.Text -> Histogram
mkHistogram :: Text -> Histogram
mkHistogram Text
c =
    Histogram
        { x :: Text
x = Text
c
        , bins :: Int
bins = Int
30
        , title :: Maybe Text
title = Maybe Text
forall a. Maybe a
Nothing
        , plot :: Plot
plot = Plot
defPlot
        }

histogram :: (HasCallStack) => Histogram -> DataFrame -> IO ()
histogram :: HasCallStack => Histogram -> DataFrame -> IO ()
histogram Histogram
spec DataFrame
df = do
    let values :: [Double]
values = HasCallStack => Text -> DataFrame -> [Double]
Text -> DataFrame -> [Double]
extractNumericColumn Histogram
spec.x DataFrame
df
        (Double
lo, Double
hi) = if [Double] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Double]
values then (Double
0, Double
1) else ([Double] -> Double
forall a. Ord a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
minimum [Double]
values, [Double] -> Double
forall a. Ord a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
maximum [Double]
values)
        t :: Text
t = case Histogram
spec.title of
            Just Text
s -> Text
s
            Maybe Text
Nothing -> Text
"histogram of " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Histogram
spec.x
        cfg :: Plot
cfg = Histogram
spec.plot{Granite.plotTitle = t}
    Text -> IO ()
T.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ Bins -> [Double] -> Plot -> Text
Granite.histogram (Int -> Double -> Double -> Bins
Granite.bins Histogram
spec.bins Double
lo Double
hi) [Double]
values Plot
cfg

-- ---------------------------------------------------------------------------
-- Scatter
-- ---------------------------------------------------------------------------

-- | Scatter-plot specification.
data Scatter = Scatter
    { Scatter -> Text
x :: T.Text
    , Scatter -> Text
y :: T.Text
    , Scatter -> Maybe Text
color :: Maybe T.Text
    -- ^ Optional grouping column; produces a separate series per value.
    , Scatter -> Maybe Text
title :: Maybe T.Text
    , Scatter -> Plot
plot :: Plot
    }

mkScatter :: T.Text -> T.Text -> Scatter
mkScatter :: Text -> Text -> Scatter
mkScatter Text
xc Text
yc =
    Scatter
        { x :: Text
x = Text
xc
        , y :: Text
y = Text
yc
        , color :: Maybe Text
color = Maybe Text
forall a. Maybe a
Nothing
        , title :: Maybe Text
title = Maybe Text
forall a. Maybe a
Nothing
        , plot :: Plot
plot = Plot
defPlot
        }

scatter :: (HasCallStack) => Scatter -> DataFrame -> IO ()
scatter :: HasCallStack => Scatter -> DataFrame -> IO ()
scatter Scatter
spec DataFrame
df = do
    let t :: Text
t = case Scatter
spec.title of
            Just Text
s -> Text
s
            Maybe Text
Nothing -> Scatter
spec.x Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" vs " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Scatter
spec.y
        cfg :: Plot
cfg = Scatter
spec.plot{Granite.plotTitle = t}
        xVals :: [Double]
xVals = HasCallStack => Text -> DataFrame -> [Double]
Text -> DataFrame -> [Double]
extractNumericColumn Scatter
spec.x DataFrame
df
        yVals :: [Double]
yVals = HasCallStack => Text -> DataFrame -> [Double]
Text -> DataFrame -> [Double]
extractNumericColumn Scatter
spec.y DataFrame
df
    case Scatter
spec.color of
        Maybe Text
Nothing ->
            Text -> IO ()
T.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ [(Text, [(Double, Double)])] -> Plot -> Text
Granite.scatter [(Scatter
spec.x Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" vs " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Scatter
spec.y, [Double] -> [Double] -> [(Double, Double)]
forall a b. [a] -> [b] -> [(a, b)]
zip [Double]
xVals [Double]
yVals)] Plot
cfg
        Just Text
grp -> do
            let groupVals :: [Text]
groupVals = HasCallStack => Text -> DataFrame -> [Text]
Text -> DataFrame -> [Text]
extractStringColumn Text
grp DataFrame
df
                triples :: [(Text, Double, Double)]
triples = [Text] -> [Double] -> [Double] -> [(Text, Double, Double)]
forall a b c. [a] -> [b] -> [c] -> [(a, b, c)]
zip3 [Text]
groupVals [Double]
xVals [Double]
yVals
                uniqGroups :: [Text]
uniqGroups = [Text] -> [Text]
forall a. Eq a => [a] -> [a]
L.nub [Text]
groupVals
                series :: [(Text, [(Double, Double)])]
series =
                    [ (Text
g, [(Double
xv, Double
yv) | (Text
gv, Double
xv, Double
yv) <- [(Text, Double, Double)]
triples, Text
gv Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
g])
                    | Text
g <- [Text]
uniqGroups
                    ]
            Text -> IO ()
T.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ [(Text, [(Double, Double)])] -> Plot -> Text
Granite.scatter [(Text, [(Double, Double)])]
series Plot
cfg

-- ---------------------------------------------------------------------------
-- Line
-- ---------------------------------------------------------------------------

-- | Line-chart specification: one x column, one or more y series.
data Line = Line
    { Line -> Text
x :: T.Text
    , Line -> [Text]
y :: [T.Text]
    , Line -> Maybe Text
title :: Maybe T.Text
    , Line -> Plot
plot :: Plot
    }

mkLine :: T.Text -> [T.Text] -> Line
mkLine :: Text -> [Text] -> Line
mkLine Text
xc [Text]
ys =
    Line
        { x :: Text
x = Text
xc
        , y :: [Text]
y = [Text]
ys
        , title :: Maybe Text
title = Maybe Text
forall a. Maybe a
Nothing
        , plot :: Plot
plot = Plot
defPlot
        }

line :: (HasCallStack) => Line -> DataFrame -> IO ()
line :: HasCallStack => Line -> DataFrame -> IO ()
line Line
spec DataFrame
df = do
    let t :: Text
t = case Line
spec.title of
            Just Text
s -> Text
s
            Maybe Text
Nothing -> case Line
spec.y of
                [Text
single] -> Text
single Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" over " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Line
spec.x
                [Text]
_ -> Text -> [Text] -> Text
T.intercalate Text
", " Line
spec.y Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" over " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Line
spec.x
        cfg :: Plot
cfg = Line
spec.plot{Granite.plotTitle = t}
        xVals :: [Double]
xVals = HasCallStack => Text -> DataFrame -> [Double]
Text -> DataFrame -> [Double]
extractNumericColumn Line
spec.x DataFrame
df
    [(Text, [(Double, Double)])]
seriesData <- [Text]
-> (Text -> IO (Text, [(Double, Double)]))
-> IO [(Text, [(Double, Double)])]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
t a -> (a -> m b) -> m (t b)
forM Line
spec.y ((Text -> IO (Text, [(Double, Double)]))
 -> IO [(Text, [(Double, Double)])])
-> (Text -> IO (Text, [(Double, Double)]))
-> IO [(Text, [(Double, Double)])]
forall a b. (a -> b) -> a -> b
$ \Text
col -> do
        let values :: [Double]
values = HasCallStack => Text -> DataFrame -> [Double]
Text -> DataFrame -> [Double]
extractNumericColumn Text
col DataFrame
df
        (Text, [(Double, Double)]) -> IO (Text, [(Double, Double)])
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Text
col, [Double] -> [Double] -> [(Double, Double)]
forall a b. [a] -> [b] -> [(a, b)]
zip [Double]
xVals [Double]
values)
    Text -> IO ()
T.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ [(Text, [(Double, Double)])] -> Plot -> Text
Granite.lineGraph [(Text, [(Double, Double)])]
seriesData Plot
cfg

-- ---------------------------------------------------------------------------
-- Pie
-- ---------------------------------------------------------------------------

-- | Pie-chart specification.
data Pie = Pie
    { Pie -> Text
values :: T.Text
    {- ^ Slice-defining column. For 'Count', categorical; for the
    numeric aggs, the value column.
    -}
    , Pie -> Maybe Text
names :: Maybe T.Text
    {- ^ Optional label column. If @Nothing@, slices are labelled by
    the value column directly (for 'Count') or by row index.
    -}
    , Pie -> Agg
agg :: Agg
    , Pie -> Maybe Int
topN :: Maybe Int
    , Pie -> Maybe Text
title :: Maybe T.Text
    , Pie -> Plot
plot :: Plot
    }

mkPie :: T.Text -> Pie
mkPie :: Text -> Pie
mkPie Text
c =
    Pie
        { values :: Text
values = Text
c
        , names :: Maybe Text
names = Maybe Text
forall a. Maybe a
Nothing
        , agg :: Agg
agg = Agg
Count
        , topN :: Maybe Int
topN = Int -> Maybe Int
forall a. a -> Maybe a
Just Int
8
        , title :: Maybe Text
title = Maybe Text
forall a. Maybe a
Nothing
        , plot :: Plot
plot = Plot
defPlot
        }

pie :: (HasCallStack) => Pie -> DataFrame -> IO ()
pie :: HasCallStack => Pie -> DataFrame -> IO ()
pie Pie
spec DataFrame
df = do
    let vCol :: Text
vCol = Pie
spec.values
        mNames :: Maybe Text
mNames = Pie
spec.names
        a :: Agg
a = Pie
spec.agg
        t :: Text
t = case Pie
spec.title of
            Just Text
s -> Text
s
            Maybe Text
Nothing -> case Maybe Text
mNames of
                Just Text
n -> Agg -> Text
aggLabel Agg
a Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"(" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
vCol Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
") by " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
n
                Maybe Text
Nothing -> Agg -> Text
aggLabel Agg
a Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" of " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
vCol
        cfg :: Plot
cfg = Pie
spec.plot{Granite.plotTitle = t}
        rows :: [(Text, Double)]
rows = case (Agg
a, Maybe Text
mNames) of
            (Agg
Count, Maybe Text
Nothing) -> HasCallStack =>
Agg -> Text -> Maybe Text -> DataFrame -> [(Text, Double)]
Agg -> Text -> Maybe Text -> DataFrame -> [(Text, Double)]
aggregateByGroup Agg
Count Text
vCol Maybe Text
forall a. Maybe a
Nothing DataFrame
df
            (Agg
Count, Just Text
n) -> HasCallStack =>
Agg -> Text -> Maybe Text -> DataFrame -> [(Text, Double)]
Agg -> Text -> Maybe Text -> DataFrame -> [(Text, Double)]
aggregateByGroup Agg
Count Text
n Maybe Text
forall a. Maybe a
Nothing DataFrame
df
            (Agg
_, Maybe Text
Nothing) ->
                let xs :: [Double]
xs = HasCallStack => Text -> DataFrame -> [Double]
Text -> DataFrame -> [Double]
extractNumericColumn Text
vCol DataFrame
df
                 in [Text] -> [Double] -> [(Text, Double)]
forall a b. [a] -> [b] -> [(a, b)]
zip [[Char] -> Text
T.pack ([Char]
"Item " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ Int -> [Char]
forall a. Show a => a -> [Char]
show Int
i) | Int
i <- [Int
1 .. [Double] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Double]
xs :: Int]] [Double]
xs
            (Agg
_, Just Text
n) -> HasCallStack =>
Agg -> Text -> Maybe Text -> DataFrame -> [(Text, Double)]
Agg -> Text -> Maybe Text -> DataFrame -> [(Text, Double)]
aggregateByGroup Agg
a Text
n (Text -> Maybe Text
forall a. a -> Maybe a
Just Text
vCol) DataFrame
df
        rows' :: [(Text, Double)]
rows' = [(Text, Double)]
-> (Int -> [(Text, Double)]) -> Maybe Int -> [(Text, Double)]
forall b a. b -> (a -> b) -> Maybe a -> b
maybe [(Text, Double)]
rows (Int -> [(Text, Double)] -> [(Text, Double)]
`groupWithOtherForPie` [(Text, Double)]
rows) Pie
spec.topN
    Text -> IO ()
T.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ [(Text, Double)] -> Plot -> Text
Granite.pie [(Text, Double)]
rows' Plot
cfg

-- ---------------------------------------------------------------------------
-- Box
-- ---------------------------------------------------------------------------

-- | Box-plot specification across one or more numeric columns.
data Box = Box
    { Box -> [Text]
y :: [T.Text]
    , Box -> Maybe Text
title :: Maybe T.Text
    , Box -> Plot
plot :: Plot
    }

mkBox :: [T.Text] -> Box
mkBox :: [Text] -> Box
mkBox [Text]
ys =
    Box
        { y :: [Text]
y = [Text]
ys
        , title :: Maybe Text
title = Maybe Text
forall a. Maybe a
Nothing
        , plot :: Plot
plot = Plot
defPlot
        }

box :: (HasCallStack) => Box -> DataFrame -> IO ()
box :: HasCallStack => Box -> DataFrame -> IO ()
box Box
spec DataFrame
df = do
    let t :: Text
t = case Box
spec.title of
            Just Text
s -> Text
s
            Maybe Text
Nothing -> Text
"box plot of " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> [Text] -> Text
T.intercalate Text
", " Box
spec.y
        cfg :: Plot
cfg = Box
spec.plot{Granite.plotTitle = t}
    [(Text, [Double])]
boxData <- [Text] -> (Text -> IO (Text, [Double])) -> IO [(Text, [Double])]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
t a -> (a -> m b) -> m (t b)
forM Box
spec.y ((Text -> IO (Text, [Double])) -> IO [(Text, [Double])])
-> (Text -> IO (Text, [Double])) -> IO [(Text, [Double])]
forall a b. (a -> b) -> a -> b
$ \Text
col -> (Text, [Double]) -> IO (Text, [Double])
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Text
col, HasCallStack => Text -> DataFrame -> [Double]
Text -> DataFrame -> [Double]
extractNumericColumn Text
col DataFrame
df)
    Text -> IO ()
T.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ [(Text, [Double])] -> Plot -> Text
Granite.boxPlot [(Text, [Double])]
boxData Plot
cfg

-- ---------------------------------------------------------------------------
-- Whole-frame plots
-- ---------------------------------------------------------------------------

{- | Heatmap of all numeric columns in the dataframe; each numeric column
becomes a column of the matrix.
-}
heatmap :: (HasCallStack) => DataFrame -> IO ()
heatmap :: HasCallStack => DataFrame -> IO ()
heatmap DataFrame
df = do
    let numericCols :: [Text]
numericCols = (Text -> Bool) -> [Text] -> [Text]
forall a. (a -> Bool) -> [a] -> [a]
filter (DataFrame -> Text -> Bool
isNumericColumn DataFrame
df) (DataFrame -> [Text]
columnNames DataFrame
df)
        matrix :: [[Double]]
matrix = (Text -> [Double]) -> [Text] -> [[Double]]
forall a b. (a -> b) -> [a] -> [b]
map (HasCallStack => Text -> DataFrame -> [Double]
Text -> DataFrame -> [Double]
`extractNumericColumn` DataFrame
df) [Text]
numericCols
    Text -> IO ()
T.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ [[Double]] -> Plot -> Text
Granite.heatmap [[Double]]
matrix Plot
defPlot

-- | Pearson correlation heatmap across all numeric columns.
correlationMatrix :: (HasCallStack) => DataFrame -> IO ()
correlationMatrix :: HasCallStack => DataFrame -> IO ()
correlationMatrix DataFrame
df = do
    let numericCols :: [Text]
numericCols = (Text -> Bool) -> [Text] -> [Text]
forall a. (a -> Bool) -> [a] -> [a]
filter (DataFrame -> Text -> Bool
isNumericColumn DataFrame
df) (DataFrame -> [Text]
columnNames DataFrame
df)
        cols :: [[Double]]
cols = (Text -> [Double]) -> [Text] -> [[Double]]
forall a b. (a -> b) -> [a] -> [b]
map (HasCallStack => Text -> DataFrame -> [Double]
Text -> DataFrame -> [Double]
`extractNumericColumn` DataFrame
df) [Text]
numericCols
        correlations :: [[Double]]
correlations =
            [ [[Double] -> [Double] -> Double
forall {a}. Floating a => [a] -> [a] -> a
corr [Double]
xs [Double]
ys | [Double]
ys <- [[Double]]
cols]
            | [Double]
xs <- [[Double]]
cols
            ]
    [(Int, Text)] -> IO ()
forall a. Show a => a -> IO ()
print ([Int] -> [Text] -> [(Int, Text)]
forall a b. [a] -> [b] -> [(a, b)]
zip [(Int
0 :: Int) ..] [Text]
numericCols)
    Text -> IO ()
T.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$
        [[Double]] -> Plot -> Text
Granite.heatmap [[Double]]
correlations (Plot
defPlot{Granite.plotTitle = "correlation matrix"})
  where
    corr :: [a] -> [a] -> a
corr [a]
xs [a]
ys =
        let n :: a
n = Int -> a
forall a b. (Integral a, Num b) => a -> b
fromIntegral (Int -> a) -> Int -> a
forall a b. (a -> b) -> a -> b
$ [a] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [a]
xs
            meanX :: a
meanX = [a] -> a
forall a. Num a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum [a]
xs a -> a -> a
forall a. Fractional a => a -> a -> a
/ a
n
            meanY :: a
meanY = [a] -> a
forall a. Num a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum [a]
ys a -> a -> a
forall a. Fractional a => a -> a -> a
/ a
n
            covXY :: a
covXY = [a] -> a
forall a. Num a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum [(a
a a -> a -> a
forall a. Num a => a -> a -> a
- a
meanX) a -> a -> a
forall a. Num a => a -> a -> a
* (a
b a -> a -> a
forall a. Num a => a -> a -> a
- a
meanY) | (a
a, a
b) <- [a] -> [a] -> [(a, a)]
forall a b. [a] -> [b] -> [(a, b)]
zip [a]
xs [a]
ys] a -> a -> a
forall a. Fractional a => a -> a -> a
/ a
n
            stdX :: a
stdX = a -> a
forall a. Floating a => a -> a
sqrt (a -> a) -> a -> a
forall a b. (a -> b) -> a -> b
$ [a] -> a
forall a. Num a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum [(a
a a -> a -> a
forall a. Num a => a -> a -> a
- a
meanX) a -> Int -> a
forall a b. (Num a, Integral b) => a -> b -> a
^ (Int
2 :: Int) | a
a <- [a]
xs] a -> a -> a
forall a. Fractional a => a -> a -> a
/ a
n
            stdY :: a
stdY = a -> a
forall a. Floating a => a -> a
sqrt (a -> a) -> a -> a
forall a b. (a -> b) -> a -> b
$ [a] -> a
forall a. Num a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum [(a
b a -> a -> a
forall a. Num a => a -> a -> a
- a
meanY) a -> Int -> a
forall a b. (Num a, Integral b) => a -> b -> a
^ (Int
2 :: Int) | a
b <- [a]
ys] a -> a -> a
forall a. Fractional a => a -> a -> a
/ a
n
         in a
covXY a -> a -> a
forall a. Fractional a => a -> a -> a
/ (a
stdX a -> a -> a
forall a. Num a => a -> a -> a
* a
stdY)

-- ---------------------------------------------------------------------------
-- Title helper
-- ---------------------------------------------------------------------------

autoTitle :: Agg -> Maybe T.Text -> T.Text -> T.Text
autoTitle :: Agg -> Maybe Text -> Text -> Text
autoTitle Agg
Count Maybe Text
_ Text
groupCol = Text
"count by " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
groupCol
autoTitle Agg
a (Just Text
yCol) Text
groupCol = Agg -> Text
aggLabel Agg
a Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"(" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
yCol Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
") by " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
groupCol
autoTitle Agg
a Maybe Text
Nothing Text
groupCol = Agg -> Text
aggLabel Agg
a Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" by " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
groupCol

-- ---------------------------------------------------------------------------
-- Deprecated legacy entry points
-- ---------------------------------------------------------------------------

-- | Use 'histogram' with 'mkHistogram' instead.
plotHistogram :: (HasCallStack) => T.Text -> DataFrame -> IO ()
plotHistogram :: HasCallStack => Text -> DataFrame -> IO ()
plotHistogram Text
c = HasCallStack => Histogram -> DataFrame -> IO ()
Histogram -> DataFrame -> IO ()
histogram (Text -> Histogram
mkHistogram Text
c)
{-# DEPRECATED plotHistogram "use 'histogram (mkHistogram col)' instead" #-}

-- | Use 'scatter' with 'mkScatter' instead.
plotScatter :: (HasCallStack) => T.Text -> T.Text -> DataFrame -> IO ()
plotScatter :: HasCallStack => Text -> Text -> DataFrame -> IO ()
plotScatter Text
xc Text
yc = HasCallStack => Scatter -> DataFrame -> IO ()
Scatter -> DataFrame -> IO ()
scatter (Text -> Text -> Scatter
mkScatter Text
xc Text
yc)
{-# DEPRECATED plotScatter "use 'scatter (mkScatter xCol yCol)' instead" #-}

-- | Use 'bar' with 'mkBar' instead.
plotBars :: (HasCallStack) => T.Text -> DataFrame -> IO ()
plotBars :: HasCallStack => Text -> DataFrame -> IO ()
plotBars Text
c = HasCallStack => Bar -> DataFrame -> IO ()
Bar -> DataFrame -> IO ()
bar (Text -> Bar
mkBar Text
c)
{-# DEPRECATED plotBars "use 'bar (mkBar col)' instead" #-}

-- | Use 'line' with 'mkLine' instead.
plotLines :: (HasCallStack) => T.Text -> [T.Text] -> DataFrame -> IO ()
plotLines :: HasCallStack => Text -> [Text] -> DataFrame -> IO ()
plotLines Text
xc [Text]
ys = HasCallStack => Line -> DataFrame -> IO ()
Line -> DataFrame -> IO ()
line (Text -> [Text] -> Line
mkLine Text
xc [Text]
ys)
{-# DEPRECATED plotLines "use 'line (mkLine xCol yCols)' instead" #-}

-- | Use 'pie' with 'mkPie' instead.
plotPie :: (HasCallStack) => T.Text -> Maybe T.Text -> DataFrame -> IO ()
plotPie :: HasCallStack => Text -> Maybe Text -> DataFrame -> IO ()
plotPie Text
c Maybe Text
mLabel =
    let s :: Pie
s = Text -> Pie
mkPie Text
c
     in HasCallStack => Pie -> DataFrame -> IO ()
Pie -> DataFrame -> IO ()
pie Pie
s{names = mLabel}
{-# DEPRECATED plotPie "use 'pie (mkPie col)' instead" #-}

-- | Use 'box' with 'mkBox' instead.
plotBoxPlots :: (HasCallStack) => [T.Text] -> DataFrame -> IO ()
plotBoxPlots :: HasCallStack => [Text] -> DataFrame -> IO ()
plotBoxPlots [Text]
ys = HasCallStack => Box -> DataFrame -> IO ()
Box -> DataFrame -> IO ()
box ([Text] -> Box
mkBox [Text]
ys)
{-# DEPRECATED plotBoxPlots "use 'box (mkBox cols)' instead" #-}

{- | Stacked-bar plot: one bar per category, segments from the listed value
columns. Kept as a direct passthrough to granite — has no spec record
because no aggregation is involved (segments are summed per category).
-}
plotStackedBars :: (HasCallStack) => T.Text -> [T.Text] -> DataFrame -> IO ()
plotStackedBars :: HasCallStack => Text -> [Text] -> DataFrame -> IO ()
plotStackedBars Text
categoryCol [Text]
valueColumns DataFrame
df = do
    let categories :: [Text]
categories = HasCallStack => Text -> DataFrame -> [Text]
Text -> DataFrame -> [Text]
extractStringColumn Text
categoryCol DataFrame
df
        uniqueCategories :: [Text]
uniqueCategories = [Text] -> [Text]
forall a. Eq a => [a] -> [a]
L.nub [Text]
categories
    [(Text, [(Text, Double)])]
stackData <- [Text]
-> (Text -> IO (Text, [(Text, Double)]))
-> IO [(Text, [(Text, Double)])]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
t a -> (a -> m b) -> m (t b)
forM [Text]
uniqueCategories ((Text -> IO (Text, [(Text, Double)]))
 -> IO [(Text, [(Text, Double)])])
-> (Text -> IO (Text, [(Text, Double)]))
-> IO [(Text, [(Text, Double)])]
forall a b. (a -> b) -> a -> b
$ \Text
cat -> do
        let indices :: [Int]
indices = [Int
i | (Int
i, Text
c) <- [Int] -> [Text] -> [(Int, Text)]
forall a b. [a] -> [b] -> [(a, b)]
zip [Int
0 ..] [Text]
categories, Text
c Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
cat]
        [(Text, Double)]
seriesData <- [Text] -> (Text -> IO (Text, Double)) -> IO [(Text, Double)]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
t a -> (a -> m b) -> m (t b)
forM [Text]
valueColumns ((Text -> IO (Text, Double)) -> IO [(Text, Double)])
-> (Text -> IO (Text, Double)) -> IO [(Text, Double)]
forall a b. (a -> b) -> a -> b
$ \Text
col -> do
            let allValues :: [Double]
allValues = HasCallStack => Text -> DataFrame -> [Double]
Text -> DataFrame -> [Double]
extractNumericColumn Text
col DataFrame
df
                vs :: [Double]
vs = [[Double]
allValues [Double] -> Int -> Double
forall a. HasCallStack => [a] -> Int -> a
!! Int
i | Int
i <- [Int]
indices, Int
i Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
< [Double] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Double]
allValues]
            (Text, Double) -> IO (Text, Double)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Text
col, [Double] -> Double
forall a. Num a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum [Double]
vs)
        (Text, [(Text, Double)]) -> IO (Text, [(Text, Double)])
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Text
cat, [(Text, Double)]
seriesData)
    Text -> IO ()
T.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ [(Text, [(Text, Double)])] -> Plot -> Text
Granite.stackedBars [(Text, [(Text, Double)])]
stackData Plot
defPlot

-- | Use 'heatmap' instead.
plotHeatmap :: (HasCallStack) => DataFrame -> IO ()
plotHeatmap :: HasCallStack => DataFrame -> IO ()
plotHeatmap = HasCallStack => DataFrame -> IO ()
DataFrame -> IO ()
heatmap
{-# DEPRECATED plotHeatmap "use 'heatmap' instead" #-}