dataframe-0.3.0.2: src/DataFrame/Display/Terminal/Plot.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE GADTs #-}
module DataFrame.Display.Terminal.Plot where
import Control.Monad
import qualified Data.List as L
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Vector.Generic as VG
import qualified Data.Vector.Unboxed as VU
import Data.Maybe (fromMaybe, catMaybes)
import Data.Typeable (Typeable)
import Data.Type.Equality (type (:~:)(Refl), TestEquality(testEquality))
import Type.Reflection (typeRep)
import GHC.Stack (HasCallStack)
import DataFrame.Internal.Column (Column(..), Columnable)
import DataFrame.Internal.DataFrame (DataFrame(..))
import DataFrame.Operations.Core
import Granite
data PlotConfig = PlotConfig
{ plotType :: PlotType
, plotTitle :: String
, plotSettings :: Plot
}
data PlotType
= Histogram'
| Scatter'
| Line'
| Bar'
| BoxPlot'
| Pie'
| StackedBar'
| Heatmap'
deriving (Eq, Show)
defaultPlotConfig :: PlotType -> PlotConfig
defaultPlotConfig ptype = PlotConfig
{ plotType = ptype
, plotTitle = ""
, plotSettings = defPlot
}
plotHistogram :: HasCallStack => T.Text -> DataFrame -> IO ()
plotHistogram colName df = plotHistogramWith colName (defaultPlotConfig Histogram') df
plotHistogramWith :: HasCallStack => T.Text -> PlotConfig -> DataFrame -> IO ()
plotHistogramWith colName config df = do
let values = extractNumericColumn colName df
(minVal, maxVal) = if null values then (0, 1) else (minimum values, maximum values)
putStrLn $ histogram (plotTitle config) (bins 30 minVal maxVal) values (plotSettings config)
plotScatter :: HasCallStack => T.Text -> T.Text -> DataFrame -> IO ()
plotScatter xCol yCol df = plotScatterWith xCol yCol (defaultPlotConfig Scatter') df
plotScatterWith :: HasCallStack => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()
plotScatterWith xCol yCol config df = do
let xVals = extractNumericColumn xCol df
yVals = extractNumericColumn yCol df
points = zip xVals yVals
putStrLn $ scatter (plotTitle config) [(T.unpack xCol ++ " vs " ++ T.unpack yCol, points)] (plotSettings config)
plotLines :: HasCallStack => [T.Text] -> DataFrame -> IO ()
plotLines colNames df = plotLinesWith colNames (defaultPlotConfig Line') df
plotLinesWith :: HasCallStack => [T.Text] -> PlotConfig -> DataFrame -> IO ()
plotLinesWith colNames config df = do
seriesData <- forM colNames $ \col -> do
let values = extractNumericColumn col df
indices = map fromIntegral [0..length values - 1]
return (T.unpack col, zip indices values)
putStrLn $ lineGraph (plotTitle config) seriesData (plotSettings config)
plotBoxPlots :: HasCallStack => [T.Text] -> DataFrame -> IO ()
plotBoxPlots colNames df = plotBoxPlotsWith colNames (defaultPlotConfig BoxPlot') df
plotBoxPlotsWith :: HasCallStack => [T.Text] -> PlotConfig -> DataFrame -> IO ()
plotBoxPlotsWith colNames config df = do
boxData <- forM colNames $ \col -> do
let values = extractNumericColumn col df
return (T.unpack col, values)
putStrLn $ boxPlot (plotTitle config) boxData (plotSettings config)
plotStackedBars :: HasCallStack => T.Text -> [T.Text] -> DataFrame -> IO ()
plotStackedBars categoryCol valueColumns df =
plotStackedBarsWith categoryCol valueColumns (defaultPlotConfig StackedBar') df
plotStackedBarsWith :: HasCallStack => T.Text -> [T.Text] -> PlotConfig -> DataFrame -> IO ()
plotStackedBarsWith categoryCol valueColumns config df = do
let categories = extractStringColumn categoryCol df
uniqueCategories = L.nub categories
stackData <- forM uniqueCategories $ \cat -> do
let indices = [i | (i, c) <- zip [0..] categories, c == cat]
seriesData <- forM valueColumns $ \col -> do
let allValues = extractNumericColumn col df
values = [allValues !! i | i <- indices, i < length allValues]
return (T.unpack col, sum values)
return (cat, seriesData)
putStrLn $ stackedBars (plotTitle config) stackData (plotSettings config)
plotHeatmap :: HasCallStack => DataFrame -> IO ()
plotHeatmap df = plotHeatmapWith (defaultPlotConfig Heatmap') df
plotHeatmapWith :: HasCallStack => PlotConfig -> DataFrame -> IO ()
plotHeatmapWith config df = do
let numericCols = filter (isNumericColumn df) (columnNames df)
matrix = map (\col -> extractNumericColumn col df) numericCols
putStrLn $ heatmap (plotTitle config) matrix (plotSettings config)
isNumericColumn :: DataFrame -> T.Text -> Bool
isNumericColumn df colName =
case M.lookup colName (columnIndices df) of
Nothing -> False
Just idx ->
let col = columns df V.! idx
in case col of
BoxedColumn (vec :: V.Vector a) ->
case testEquality (typeRep @a) (typeRep @Double) of
Just _ -> True
Nothing -> case testEquality (typeRep @a) (typeRep @Int) of
Just _ -> True
Nothing -> case testEquality (typeRep @a) (typeRep @Float) of
Just _ -> True
Nothing -> False
UnboxedColumn (vec :: VU.Vector a) ->
case testEquality (typeRep @a) (typeRep @Double) of
Just _ -> True
Nothing -> case testEquality (typeRep @a) (typeRep @Int) of
Just _ -> True
Nothing -> case testEquality (typeRep @a) (typeRep @Float) of
Just _ -> True
Nothing -> False
-- Haven't dealt with optionals yet.
_ -> False
plotAllHistograms :: HasCallStack => DataFrame -> IO ()
plotAllHistograms df = do
let numericCols = filter (isNumericColumn df) (columnNames df)
forM_ numericCols $ \col -> do
plotHistogram col df
plotCorrelationMatrix :: HasCallStack => DataFrame -> IO ()
plotCorrelationMatrix df = do
let numericCols = filter (isNumericColumn df) (columnNames df)
let correlations = map (\col1 ->
map (\col2 -> let
vals1 = extractNumericColumn col1 df
vals2 = extractNumericColumn col2 df
in correlation vals1 vals2) numericCols) numericCols
putStrLn $ heatmap "Correlation Matrix" correlations defPlot
where
correlation xs ys =
let n = fromIntegral $ length xs
meanX = sum xs / n
meanY = sum ys / n
covXY = sum [(x - meanX) * (y - meanY) | (x,y) <- zip xs ys] / n
stdX = sqrt $ sum [(x - meanX)^2 | x <- xs] / n
stdY = sqrt $ sum [(y - meanY)^2 | y <- ys] / n
in covXY / (stdX * stdY)
quickPlot :: HasCallStack => [T.Text] -> DataFrame -> IO ()
quickPlot [] df = plotAllHistograms df >> putStrLn "Plotted all numeric columns"
quickPlot [col] df = plotHistogram col df
quickPlot [col1, col2] df = plotScatter col1 col2 df
quickPlot cols df = plotLines cols df
plotBars :: HasCallStack => T.Text -> DataFrame -> IO ()
plotBars colName df = plotBarsWith colName Nothing (defaultPlotConfig Bar') df
plotBarsWith :: HasCallStack => T.Text -> Maybe T.Text -> PlotConfig -> DataFrame -> IO ()
plotBarsWith colName groupByCol config df =
case groupByCol of
Nothing -> plotSingleBars colName config df
Just grpCol -> plotGroupedBarsWith grpCol colName config df
plotSingleBars :: HasCallStack => T.Text -> PlotConfig -> DataFrame -> IO ()
plotSingleBars colName config df = do
let barData = getCategoricalCounts colName df
case barData of
Just counts -> do
let grouped = groupWithOther 10 counts
putStrLn $ bars (plotTitle config) grouped (plotSettings config)
Nothing -> do
let values = extractNumericColumn colName df
if length values > 20
then do
let labels = map (\i -> "Item " ++ show i) [1..length values]
paired = zip labels values
grouped = groupWithOther 10 paired
putStrLn $ bars (plotTitle config) grouped (plotSettings config)
else do
let labels = map (\i -> "Item " ++ show i) [1..length values]
putStrLn $ bars (plotTitle config) (zip labels values) (plotSettings config)
plotBarsTopN :: HasCallStack => Int -> T.Text -> DataFrame -> IO ()
plotBarsTopN n colName df = plotBarsTopNWith n colName (defaultPlotConfig Bar') df
plotBarsTopNWith :: HasCallStack => Int -> T.Text -> PlotConfig -> DataFrame -> IO ()
plotBarsTopNWith n colName config df = do
let barData = getCategoricalCounts colName df
case barData of
Just counts -> do
let grouped = groupWithOther n counts
putStrLn $ bars (plotTitle config) grouped (plotSettings config)
Nothing -> do
let values = extractNumericColumn colName df
labels = map (\i -> "Item " ++ show i) [1..length values]
paired = zip labels values
grouped = groupWithOther n paired
putStrLn $ bars (plotTitle config) grouped (plotSettings config)
plotGroupedBarsWith :: HasCallStack => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()
plotGroupedBarsWith groupCol valCol config df = do
let isNumeric = isNumericColumnCheck valCol df
if isNumeric
then do
let groups = extractStringColumn groupCol df
values = extractNumericColumn valCol df
grouped = M.toList $ M.fromListWith (+) (zip groups values)
finalGroups = groupWithOther 10 grouped
putStrLn $ bars (plotTitle config) finalGroups (plotSettings config)
else do
let groups = extractStringColumn groupCol df
vals = extractStringColumn valCol df
pairs = zip groups vals
counts = M.toList $ M.fromListWith (+)
[(g ++ " - " ++ v, 1) | (g, v) <- pairs]
finalCounts = groupWithOther 15 [(k, fromIntegral v) | (k, v) <- counts]
putStrLn $ bars (plotTitle config) finalCounts (plotSettings config)
plotValueCounts :: HasCallStack => T.Text -> DataFrame -> IO ()
plotValueCounts colName df = plotValueCountsWith colName 10 (defaultPlotConfig Bar') df
plotValueCountsWith :: HasCallStack => T.Text -> Int -> PlotConfig -> DataFrame -> IO ()
plotValueCountsWith colName maxBars config df = do
let counts = getCategoricalCounts colName df
case counts of
Just c -> do
let grouped = groupWithOther maxBars c
config' = config { plotTitle = if null (plotTitle config)
then "Value counts for " ++ T.unpack colName
else plotTitle config }
putStrLn $ bars (T.unpack colName) grouped (plotSettings config')
Nothing -> error $ "Could not get value counts for column " ++ T.unpack colName
plotBarsWithPercentages :: HasCallStack => T.Text -> DataFrame -> IO ()
plotBarsWithPercentages colName df = do
let counts = getCategoricalCounts colName df
case counts of
Just c -> do
let total = sum (map snd c)
percentages = [(label ++ " (" ++ show (round (100 * val / total) :: Int) ++ "%)", val)
| (label, val) <- c]
grouped = groupWithOther 10 percentages
putStrLn $ bars ("Distribution of " ++ T.unpack colName) grouped defPlot
Nothing -> error $ "Could not get value counts for column " ++ T.unpack colName
smartPlotBars :: HasCallStack => T.Text -> DataFrame -> IO ()
smartPlotBars colName df = do
let counts = getCategoricalCounts colName df
case counts of
Just c -> do
let numUnique = length c
config = (defaultPlotConfig Bar') {
plotTitle = T.unpack colName ++ " (" ++ show numUnique ++ " unique values)"
}
if numUnique <= 12
then putStrLn $ bars (plotTitle config) c (plotSettings config)
else if numUnique <= 20
then do
let grouped = groupWithOther 12 c
putStrLn $ bars (plotTitle config ++ " - Top 12 + Other") grouped (plotSettings config)
else do
let grouped = groupWithOther 10 c
otherCount = numUnique - 10
putStrLn $ bars (plotTitle config ++ " - Top 10 + Other (" ++ show otherCount ++ " items)")
grouped (plotSettings config)
Nothing -> plotBars colName df
plotCategoricalSummary :: HasCallStack => DataFrame -> IO ()
plotCategoricalSummary df = do
let cols = columnNames df
forM_ cols $ \col -> do
let counts = getCategoricalCounts col df
case counts of
Just c -> when (length c > 1) $ do
let numUnique = length c
putStrLn $ "\n=== " ++ T.unpack col ++ " (" ++ show numUnique ++ " unique values) ==="
if numUnique > 15 then plotBarsTopN 10 col df else plotBars col df
Nothing -> return ()
getCategoricalCounts :: HasCallStack => T.Text -> DataFrame -> Maybe [(String, Double)]
getCategoricalCounts colName df =
case M.lookup colName (columnIndices df) of
Nothing -> error $ "Column " ++ T.unpack colName ++ " not found"
Just idx ->
let col = columns df V.! idx
in case col of
BoxedColumn vec -> let counts = countValues vec
in Just [(show k, fromIntegral v) | (k, v) <- counts]
UnboxedColumn vec -> let counts = countValuesUnboxed vec
in Just [(show k, fromIntegral v) | (k, v) <- counts]
where
countValues :: (Ord a, Show a) => V.Vector a -> [(a, Int)]
countValues vec = M.toList $ V.foldr' (\x acc -> M.insertWith (+) x 1 acc) M.empty vec
countValuesUnboxed :: (Ord a, Show a, VU.Unbox a) => VU.Vector a -> [(a, Int)]
countValuesUnboxed vec = M.toList $ VU.foldr' (\x acc -> M.insertWith (+) x 1 acc) M.empty vec
isNumericColumnCheck :: T.Text -> DataFrame -> Bool
isNumericColumnCheck colName df =
case M.lookup colName (columnIndices df) of
Nothing -> False
Just idx ->
let col = columns df V.! idx
in case col of
BoxedColumn (vec :: V.Vector a) -> isNumericType @a
UnboxedColumn (vec :: VU.Vector a) -> isNumericType @a
isNumericType :: forall a. Typeable a => Bool
isNumericType =
case testEquality (typeRep @a) (typeRep @Double) of
Just _ -> True
Nothing -> case testEquality (typeRep @a) (typeRep @Int) of
Just _ -> True
Nothing -> case testEquality (typeRep @a) (typeRep @Float) of
Just _ -> True
Nothing -> case testEquality (typeRep @a) (typeRep @Integer) of
Just _ -> True
Nothing -> False
extractStringColumn :: HasCallStack => T.Text -> DataFrame -> [String]
extractStringColumn colName df =
case M.lookup colName (columnIndices df) of
Nothing -> error $ "Column " ++ T.unpack colName ++ " not found"
Just idx ->
let col = columns df V.! idx
in case col of
BoxedColumn vec -> V.toList $ V.map show vec
UnboxedColumn vec -> V.toList $ VG.map show (VG.convert vec)
extractNumericColumn :: HasCallStack => T.Text -> DataFrame -> [Double]
extractNumericColumn colName df =
case M.lookup colName (columnIndices df) of
Nothing -> error $ "Column " ++ T.unpack colName ++ " not found"
Just idx ->
let col = columns df V.! idx
in case col of
BoxedColumn vec -> vectorToDoubles vec
UnboxedColumn vec -> unboxedVectorToDoubles vec
vectorToDoubles :: forall a. (Typeable a, Show a) => V.Vector a -> [Double]
vectorToDoubles vec =
case testEquality (typeRep @a) (typeRep @Double) of
Just Refl -> V.toList vec
Nothing -> case testEquality (typeRep @a) (typeRep @Int) of
Just Refl -> V.toList $ V.map fromIntegral vec
Nothing -> case testEquality (typeRep @a) (typeRep @Integer) of
Just Refl -> V.toList $ V.map fromIntegral vec
Nothing -> case testEquality (typeRep @a) (typeRep @Float) of
Just Refl -> V.toList $ V.map realToFrac vec
Nothing -> error $ "Column is not numeric (type: " ++ show (typeRep @a) ++ ")"
unboxedVectorToDoubles :: forall a. (Typeable a, VU.Unbox a, Show a) => VU.Vector a -> [Double]
unboxedVectorToDoubles vec =
case testEquality (typeRep @a) (typeRep @Double) of
Just Refl -> VU.toList vec
Nothing -> case testEquality (typeRep @a) (typeRep @Int) of
Just Refl -> VU.toList $ VU.map fromIntegral vec
Nothing -> case testEquality (typeRep @a) (typeRep @Float) of
Just Refl -> VU.toList $ VU.map realToFrac vec
Nothing -> error $ "Column is not numeric (type: " ++ show (typeRep @a) ++ ")"
groupWithOther :: Int -> [(String, Double)] -> [(String, Double)]
groupWithOther n items =
let sorted = L.sortOn (negate . snd) items
(topN, rest) = splitAt n sorted
otherSum = sum (map snd rest)
result = if null rest || otherSum == 0
then topN
else topN ++ [("Other (" ++ show (length rest) ++ " items)", otherSum)]
in result
plotPie :: HasCallStack => T.Text -> Maybe T.Text -> DataFrame -> IO ()
plotPie valCol labelCol df = plotPieWith valCol labelCol (defaultPlotConfig Pie') df
plotPieWith :: HasCallStack => T.Text -> Maybe T.Text -> PlotConfig -> DataFrame -> IO ()
plotPieWith valCol labelCol config df = do
let categoricalData = getCategoricalCounts valCol df
case categoricalData of
Just counts -> do
let grouped = groupWithOtherForPie 8 counts
putStrLn $ pie (plotTitle config) grouped (plotSettings config)
Nothing -> do
let values = extractNumericColumn valCol df
labels = case labelCol of
Nothing -> map (\i -> "Item " ++ show i) [1..length values]
Just lCol -> extractStringColumn lCol df
let pieData = zip labels values
grouped = if length pieData > 10
then groupWithOtherForPie 8 pieData
else pieData
putStrLn $ pie (plotTitle config) grouped (plotSettings config)
groupWithOtherForPie :: Int -> [(String, Double)] -> [(String, Double)]
groupWithOtherForPie n items =
let total = sum (map snd items)
sorted = L.sortOn (negate . snd) items
(topN, rest) = splitAt n sorted
otherSum = sum (map snd rest)
otherPct = round (100 * otherSum / total) :: Int
result = if null rest || otherSum == 0
then topN
else topN ++ [("Other (" ++ show (length rest) ++ " items, " ++
show otherPct ++ "%)", otherSum)]
in result
plotPieWithPercentages :: HasCallStack => T.Text -> DataFrame -> IO ()
plotPieWithPercentages colName df = plotPieWithPercentagesConfig colName (defaultPlotConfig Pie') df
plotPieWithPercentagesConfig :: HasCallStack => T.Text -> PlotConfig -> DataFrame -> IO ()
plotPieWithPercentagesConfig colName config df = do
let counts = getCategoricalCounts colName df
case counts of
Just c -> do
let total = sum (map snd c)
withPct = [(label ++ " (" ++ show (round (100 * val / total) :: Int) ++ "%)", val)
| (label, val) <- c]
grouped = groupWithOtherForPie 8 withPct
putStrLn $ pie (plotTitle config) grouped (plotSettings config)
Nothing -> do
let values = extractNumericColumn colName df
total = sum values
labels = map (\i -> "Item " ++ show i) [1..length values]
withPct = [(label ++ " (" ++ show (round (100 * val / total) :: Int) ++ "%)", val)
| (label, val) <- zip labels values]
grouped = groupWithOtherForPie 8 withPct
putStrLn $ pie (plotTitle config) grouped (plotSettings config)
plotPieTopN :: HasCallStack => Int -> T.Text -> DataFrame -> IO ()
plotPieTopN n colName df = plotPieTopNWith n colName (defaultPlotConfig Pie') df
plotPieTopNWith :: HasCallStack => Int -> T.Text -> PlotConfig -> DataFrame -> IO ()
plotPieTopNWith n colName config df = do
let counts = getCategoricalCounts colName df
case counts of
Just c -> do
let grouped = groupWithOtherForPie n c
putStrLn $ pie (plotTitle config) grouped (plotSettings config)
Nothing -> do
let values = extractNumericColumn colName df
labels = map (\i -> "Item " ++ show i) [1..length values]
paired = zip labels values
grouped = groupWithOtherForPie n paired
putStrLn $ pie (plotTitle config) grouped (plotSettings config)
smartPlotPie :: HasCallStack => T.Text -> DataFrame -> IO ()
smartPlotPie colName df = do
let counts = getCategoricalCounts colName df
case counts of
Just c -> do
let numUnique = length c
total = sum (map snd c)
significant = filter (\(_, v) -> v / total >= 0.01) c
config = (defaultPlotConfig Pie') {
plotTitle = T.unpack colName ++ " Distribution"
}
if length significant <= 6
then putStrLn $ pie (plotTitle config) significant (plotSettings config)
else if length significant <= 10
then do
let grouped = groupWithOtherForPie 8 c
putStrLn $ pie (plotTitle config) grouped (plotSettings config)
else do
let grouped = groupWithOtherForPie 6 c
putStrLn $ pie (plotTitle config) grouped (plotSettings config)
Nothing -> plotPie colName Nothing df
plotPieGrouped :: HasCallStack => T.Text -> T.Text -> DataFrame -> IO ()
plotPieGrouped groupCol valCol df = plotPieGroupedWith groupCol valCol (defaultPlotConfig Pie') df
plotPieGroupedWith :: HasCallStack => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()
plotPieGroupedWith groupCol valCol config df = do
let isNumeric = isNumericColumnCheck valCol df
if isNumeric
then do
let groups = extractStringColumn groupCol df
values = extractNumericColumn valCol df
grouped = M.toList $ M.fromListWith (+) (zip groups values)
finalGroups = groupWithOtherForPie 8 grouped
putStrLn $ pie (plotTitle config) finalGroups (plotSettings config)
else do
let groups = extractStringColumn groupCol df
vals = extractStringColumn valCol df
combined = zipWith (\g v -> g ++ " - " ++ v) groups vals
counts = M.toList $ M.fromListWith (+) [(c, 1) | c <- combined]
finalCounts = groupWithOtherForPie 10 [(k, fromIntegral v) | (k, v) <- counts]
putStrLn $ pie (plotTitle config) finalCounts (plotSettings config)
plotPieComparison :: HasCallStack => [T.Text] -> DataFrame -> IO ()
plotPieComparison cols df = do
forM_ cols $ \col -> do
let counts = getCategoricalCounts col df
case counts of
Just c -> when (length c > 1 && length c <= 20) $ do
putStrLn $ "\n=== " ++ T.unpack col ++ " Distribution ==="
smartPlotPie col df
Nothing -> return ()
plotBinaryPie :: HasCallStack => T.Text -> DataFrame -> IO ()
plotBinaryPie colName df = do
let counts = getCategoricalCounts colName df
case counts of
Just c ->
if length c == 2
then do
let total = sum (map snd c)
withPct = [(label ++ " (" ++ show (round (100 * val / total) :: Int) ++ "%)", val)
| (label, val) <- c]
putStrLn $ pie (T.unpack colName ++ " Proportion") withPct defPlot
else error $ "Column " ++ T.unpack colName ++ " is not binary (has " ++
show (length c) ++ " unique values)"
Nothing -> error $ "Column " ++ T.unpack colName ++ " is not categorical"
plotMarketShare :: HasCallStack => T.Text -> DataFrame -> IO ()
plotMarketShare colName df = plotMarketShareWith colName (defaultPlotConfig Pie') df
plotMarketShareWith :: HasCallStack => T.Text -> PlotConfig -> DataFrame -> IO ()
plotMarketShareWith colName config df = do
let counts = getCategoricalCounts colName df
case counts of
Just c -> do
let total = sum (map snd c)
sorted = L.sortOn (negate . snd) c
significantShares = takeWhile (\(_, v) -> v / total >= 0.02) sorted
otherSum = sum [v | (_, v) <- c, v `notElem` map snd significantShares]
formatShare (label, val) =
let pct = round (100 * val / total) :: Int
in (label ++ " (" ++ show pct ++ "%)", val)
shares = map formatShare significantShares
finalShares = if otherSum > 0 && otherSum / total >= 0.01
then shares ++ [("Others (<2% each)", otherSum)]
else shares
let config' = config { plotTitle = if null (plotTitle config)
then T.unpack colName ++ " Market Share"
else plotTitle config }
putStrLn $ pie (plotTitle config') finalShares (plotSettings config')
Nothing -> error $ "Column " ++ T.unpack colName ++ " is not categorical"