streamgraph-0.5.1.0: Streamgraph.hs
{-# LANGUAGE OverloadedStrings #-}
module Streamgraph where
import Visie
import Visie.Data (TextFloat(TextFloat, getText), toText)
import Visie.ToTimeSeries
import Visie.Index
import Visie.Data
import Paths_streamgraph (getDataFileName)
import Data.Time.Clock
import Data.Time.Calendar (fromGregorian)
import Data.Time.Format (formatTime, defaultTimeLocale)
import Data.Scientific (fromFloatDigits)
import Data.Hashable
import Data.List (sortOn)
import qualified Data.HashMap.Strict as H
import qualified Data.Text as T
type Point = Timestamped TextFloat
dateFormat :: UTCTime -> T.Text
dateFormat = T.pack . formatTime defaultTimeLocale "%D"
-- make an universal time value using year, month, day
makeUni :: Integer -> Int -> Int -> UTCTime
makeUni y m d = UTCTime (fromGregorian y m d) (secondsToDiffTime 0)
options = defaultOptions { d3Version = Version2, index = ChartDiv }
toPoint :: (T.Text, Float, UTCTime) -> Point
toPoint (te, fl, ti) = Timestamped (TextFloat te fl) ti
-- if some dates will be missing for some labels, a runtime error will
-- be thrown. In order to prevent that, this function inefficiently
-- generates empty data points for all labels and dates
addAllPoints :: [Point] -> [Point]
addAllPoints existing = existing ++ allPoints
where allPoints = [Timestamped (TextFloat te 0) ti | te <- texts, ti <- times]
texts = map (getText . getStamped) existing
times = map getTime existing
-- | given a function `f` and a list `l`, produce a list of lists of
-- elements of `l` that produce the same result when `f` is applied to
-- them
groupWith :: (Eq b, Hashable b) => (a -> b) -> [a] -> [[a]]
groupWith f = H.elems . foldr myInsert H.empty
where myInsert a = H.insertWith (++) (f a) [a]
-- | This function groups points with the same description, which are
-- then mapped and concatenated in a flat list of points. So points
-- with the same description are considered like a single time
-- series. Order might affect the proper functioning of the drawing
-- logic, so test after changes
toTimeSeries :: ([Point] -> [Point]) -> [Point] -> [Point]
toTimeSeries onEachStream = concat
. map onEachStream
. toStreams
where toStreams :: [Point] -> [[Point]]
toStreams = groupWith (getText . getStamped)
-- | sort `elements` concatenating their monoid when their time falls
-- within the same interval
convertPoints :: NominalDiffTime -> [Point] -> [Point]
convertPoints interval elements = sampler sorted
where sorted = sortOn getTime elements
times = iterator interval (getTime (head sorted))
sampler = consume times
iterator :: NominalDiffTime -> UTCTime -> [UTCTime]
iterator interval start = iterate (addUTCTime interval) start
getText (Timestamped (TextFloat t _) _) = t
label = getText $ head elements
-- at every call, the recursive function returns a processed list, and
-- it gets a non processed list and a reference date about the last
-- emitted element. if the next element would have a date greater than
-- the reference + the interval, a filling element is
-- created. otherwise, the function will look ahead and merge all
-- elements within the same interval, pick a representative date for
-- the merged elements and use it as the new reference
consume :: [UTCTime] -> [Point] -> [Point]
consume (t:ts) [] = []
consume (t:ts) elements
| length preceding == 0 = filled : rest
| otherwise = foldl (merge t) filled preceding : rest
where (preceding, succeeding) = span ((<= t) . getTime) elements
filled = toPoint (label, 0, t)
rest = consume ts succeeding
merge :: Monoid a => UTCTime -> Timestamped a -> Timestamped a -> Timestamped a
merge t (Timestamped a _) (Timestamped b _) = Timestamped (mappend a b) t
streamgraph :: Int -> Maybe Int -> [(T.Text, Float, UTCTime)] -> IO ()
streamgraph days maybeLastPoints =
custom options getDataFileName transform
where transform = toText
. toTimeSeries onEachStream
. addAllPoints
. map toPoint
-- | turn each stream into a time series and take the last n
-- points
onEachStream :: [Point] -> [Point]
onEachStream = maybeLastN maybeLastPoints . convertPoints seconds
seconds = fromIntegral (days * 60 * 60 * 24)
maybeLastN :: Maybe Int -> [a] -> [a]
maybeLastN Nothing l = l
maybeLastN (Just n) l = drop (length l - n) l
simple
:: [(T.Text, Float, UTCTime)]
-- ^ Input data. Labels in the Text field will be grouped so that
-- values with the same description form a stream
-> IO ()
simple = streamgraph 1 Nothing