{-# LANGUAGE TupleSections #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NoImplicitPrelude #-} {-# OPTIONS_GHC -fno-warn-name-shadowing #-} module Chart.Various ( -- * chart-svg or chart-various transfers defaultRender, xify, xify', yify, yify', addLineX, addLineY, stdLineChart, stdLines, lineLegend, tsAxes, titlesHud, gpalette, gpaletteStyle, blendMidLineStyles, -- * generic charts quantileChart, digitChart, scatterChart, histChart, quantileHistChart, digitPixelChart, tableChart, -- * scanner charts scanChart, scanHud, foldScanChart, scannerChart, scannersChart, tsRatesHud, -- * chartAny -- chartAny, write', tryChart, ) where import Chart import NumHask.Prelude hiding (fold) import Control.Lens import qualified Data.List as List import Data.Time (UTCTime(..)) import Data.List ((!!)) import Data.Mealy import NumHask.Space import qualified Data.HashMap.Strict as HashMap defaultRender :: SvgOptions -> (HudOptions, [Chart Double]) -> Text defaultRender svgo (h, c) = renderHudOptionsChart svgo h [] c taker :: Int -> [a] -> [a] taker n = reverse . take n . reverse type Rate = Double -- | convert from [a] to [Point a], by adding the index as the x axis xify :: [Double] -> [Point Double] xify ys = zipWith Point [0 ..] ys -- | convert from [a] to [SpotPoint a], by adding the index as the x axis xify' :: [Double] -> [XY Double] xify' ys = zipWith P [0 ..] ys -- | convert from [a] to [Point a], by adding the index as the y axis yify :: [Double] -> [Point Double] yify xs = zipWith Point xs [0 ..] -- | convert from [a] to [SpotPoint a], by adding the index as the y axis yify' :: [Double] -> [XY Double] yify' xs = zipWith P xs [0 ..] -- | add a horizontal line at y addLineX :: Double -> LineStyle -> [Chart Double] -> [Chart Double] addLineX y ls cs = cs <> [l] where l = Chart (LineA ls) (PointXY <$> [Point lx y, Point ux y]) (Rect lx ux _ _) = fromMaybe one $ foldRect $ mconcat $ fmap toRect . xys <$> cs -- | add a verticle line at x addLineY :: Double -> LineStyle -> [Chart Double] -> [Chart Double] addLineY x ls cs = cs <> [zeroLine] where zeroLine = Chart (LineA ls) (PointXY <$> [Point x ly, Point x uy]) (Rect _ _ ly uy) = fromMaybe one $ foldRect $ mconcat $ fmap toRect . xys <$> cs -- | interpret a [[Double]] as a series of lines with x coordinates of [0..] stdLineChart :: Double -> [Colour] -> [[Double]] -> [Chart Double] stdLineChart w p xss = zipWith (\c xs -> Chart (LineA (defaultLineStyle & #color .~ c & #width .~ w)) (xify' xs)) p xss -- | Can of the main palette stdLines :: Double -> [LineStyle] stdLines w = (\c -> defaultLineStyle & #color .~ c & #width .~ w) <$> palette1 lineLegend :: Double -> [Text] -> [Colour] -> (LegendOptions, [(Annotation, Text)]) lineLegend w rs cs = ( defaultLegendOptions & #ltext . #size .~ 0.3 & #lplace .~ PlaceBottom & #legendFrame .~ Just (RectStyle 0.02 (palette1 !! 5) white), zipWith (\a r -> (LineA a, r)) ((\c -> defaultLineStyle & #color .~ c & #width .~ w) <$> cs) rs ) -- Create a hud that has time as the x-axis, based on supplied days, and a rounded yaxis. tsAxes :: [UTCTime] -> [AxisOptions] tsAxes ds = [ defaultAxisOptions & #atick . #tstyle .~ TickRound (FormatPrec (Just 3)) 6 TickExtend & #place .~ PlaceLeft, defaultAxisOptions & #atick . #tstyle .~ TickPlaced ( first fromIntegral <$> makeTickDates PosIncludeBoundaries Nothing 8 ds ) ] -- | common pattern of chart title, x-axis title and y-axis title titlesHud :: Text -> Text -> Text -> HudOptions titlesHud t x y = defaultHudOptions & #hudTitles .~ [ defaultTitle t, defaultTitle x & #place .~ PlaceBottom & #style . #size .~ 0.08, defaultTitle y & #place .~ PlaceLeft & #style . #size .~ 0.08 ] gpaletteStyle :: Double -> [GlyphStyle] gpaletteStyle s = zipWith (\c g -> defaultGlyphStyle & #size .~ s & #color .~ c & #shape .~ fst g & #borderSize .~ snd g) palette1 gpalette gpalette :: [(GlyphShape, Double)] gpalette = [ (CircleGlyph, 0.01 :: Double), (SquareGlyph, 0.01), (RectSharpGlyph 0.75, 0.01), (RectRoundedGlyph 0.75 0.01 0.01, 0.01), (EllipseGlyph 0.75, 0), (VLineGlyph 0.005, 0.01), (HLineGlyph 0.005, 0.01), (TriangleGlyph (Point 0.0 0.0) (Point 1 1) (Point 1 0), 0.01), (PathGlyph "M0.05,-0.03660254037844387 A0.1 0.1 0.0 0 1 0.0,0.05 0.1 0.1 0.0 0 1 -0.05,-0.03660254037844387 0.1 0.1 0.0 0 1 0.05,-0.03660254037844387 Z", 0.01) ] -- * charts quantileChart :: Text -> [Text] -> [LineStyle] -> [AxisOptions] -> [[Double]] -> (HudOptions, [Chart Double]) quantileChart title names ls as xs = (hudOptions, chart') where hudOptions = defaultHudOptions & #hudTitles .~ [defaultTitle title] & ( #hudLegend .~ Just ( defaultLegendOptions & #ltext . #size .~ 0.1 & #vgap .~ 0.05 & #innerPad .~ 0.2 & #lplace .~ PlaceRight, legendFromChart names chart' ) ) & #hudAxes .~ as chart' = zipWith (\l c -> Chart (LineA l) c) ls (zipWith P [0 ..] <$> xs) blendMidLineStyles :: Int -> Double -> (Colour, Colour) -> [LineStyle] blendMidLineStyles l w (c1, c2) = lo where m = (fromIntegral l - 1) / 2 :: Double cs = (\x -> 1 - abs (fromIntegral x - m) / m) <$> [0 .. (l - 1)] bs = (\x -> blend x c1 c2) <$> cs lo = (\c -> defaultLineStyle & #width .~ w & #color .~ c) <$> bs -- | a chart showing a time series of digitized values (eg this return was a 30th percentile) digitChart :: Text -> [UTCTime] -> [Double] -> (HudOptions, [Chart Double]) digitChart title utcs xs = (hudOptions, [c]) where hudOptions = defaultHudOptions & #hudTitles .~ [defaultTitle title] & #hudAxes .~ tsAxes utcs c = Chart (GlyphA (defaultGlyphStyle & #color .~ Colour 0 0 1 1 & #shape .~ CircleGlyph & #size .~ 0.01)) (xify' xs) -- | scatter chart scatterChart :: [[Point Double]] -> [Chart Double] scatterChart xss = zipWith (\gs xs -> Chart (GlyphA gs) (PointXY <$> xs)) (gpaletteStyle 0.02) xss -- | histogram chart histChart :: Text -> Maybe [Text] -> Range Double -> Int -> [Double] -> (HudOptions, [Chart Double]) histChart title names r g xs = barChart defaultBarOptions barData & first (#hudTitles .~ [defaultTitle title]) where barData = BarData [hr] names Nothing hcuts = grid OuterPos r g h = fill hcuts xs hr = (\(Rect x x' _ _) -> (x+x')/2) <$> makeRects (IncludeOvers (NumHask.Space.width r / fromIntegral g)) h -- | a chart drawing a histogram based on quantile information quantileHistChart :: Text -> Maybe [Text] -> -- | quantiles [Double] -> -- | quantile values [Double] -> (HudOptions, [Chart Double]) quantileHistChart title names qs vs = (hudOptions, [chart']) where hudOptions = defaultHudOptions & #hudTitles .~ [defaultTitle title] & #hudAxes .~ [ maybe (defaultAxisOptions & #atick . #tstyle .~ TickRound (FormatPrec (Just 3)) 8 TickExtend) ( \x -> defaultAxisOptions & #atick . #tstyle .~ TickPlaced (zip vs x) ) names ] chart' = Chart (RectA defaultRectStyle) (RectXY <$> hr) hr = zipWith (\(y, w) (x, z) -> Rect x z 0 ((w - y) / (z - x))) (zip qs (drop 1 qs)) (zip vs (drop 1 vs)) -- | pixel chart of digitized vs digitized counts digitPixelChart :: PixelStyle -> PixelLegendOptions -> (Text, Text, Text) -> [Text] -> [(Int, Int)] -> [Chart Double] digitPixelChart pixelStyle plo ts names ps = runHud (aspect 1) (hs0 <> hs1) (cs0 <> cs1) where l = length names pts = Point l l gr :: Rect Double gr = fromIntegral <$> Rect 0 l 0 l mapCount = foldl' (\m x -> HashMap.insertWith (+) x 1.0 m) HashMap.empty ps f :: Point Double -> Double f (Point x y) = fromMaybe 0 $ HashMap.lookup (fromIntegral (floor x :: Integer), fromIntegral (floor y :: Integer)) mapCount (hs0, cs0) = makeHud gr (qvqHud ts names) (cs1, hs1) = pixelfl f (PixelOptions pixelStyle pts gr) plo -- style helpers qvqHud :: (Text, Text, Text) -> [Text] -> HudOptions qvqHud ts labels = defaultHudOptions & #hudTitles .~ makeTitles ts & #hudAxes .~ [ defaultAxisOptions & #atick . #tstyle .~ TickPlaced (zip ((0.5 +) <$> [0 ..]) labels) & #place .~ PlaceLeft, defaultAxisOptions & #atick . #tstyle .~ TickPlaced (zip ((0.5 +) <$> [0 ..]) labels) & #place .~ PlaceBottom ] makeTitles :: (Text, Text, Text) -> [Title] makeTitles (t, xt, yt) = reverse [ defaultTitle t, defaultTitle xt & #place .~ PlaceBottom & #style . #size .~ 0.06, defaultTitle yt & #place .~ PlaceLeft & #style . #size .~ 0.06 ] tableChart :: [[Text]] -> [Chart Double] tableChart tss = zipWith (\ts x -> Chart (TextA defaultTextStyle ts) (P x <$> take (length ts) [0..])) tss [0..] -- * scanners -- | simple scan of a time series through a Mealy using a list of rates, with time dimension of [0..] scanChart :: (Rate -> Mealy a Double) -> [Rate] -> Int -> [a] -> [Chart Double] scanChart m rates d xs = zipWith (\s xs' -> Chart (LineA s) xs') (stdLines 0.003) (zipWith P (fromIntegral <$> [d..]) <$> ((\r -> drop d $ scan (m r) xs) <$> rates)) -- | common line chart hud with rates as a legend scanHud :: Double -> Text -> [Double] -> HudOptions scanHud w t rates = defaultHudOptions & #hudTitles .~ [ defaultTitle t] & #hudLegend .~ Just (lineLegend w (("rate = " <>) . show <$> rates) palette1) -- | fold over a scanned time series by rates foldScanChart :: (Rate -> Mealy a b) -> (Rate -> Mealy b Double) -> [Rate] -> [a] -> [Chart Double] foldScanChart scan' fold' rates xs = (: []) $ Chart (LineA defaultLineStyle) (zipWith P rates ((\r -> fold (fold' r) $ scan (scan' r) xs) <$> rates)) zeroLineStyle :: LineStyle zeroLineStyle = defaultLineStyle & #color .~ (palette1!!7) & #width .~ 0.002 -- | take a decaying scanner, a list of decay rates, and create linecharts from an [a] scannerChart :: Int -> [Double] -> (Double -> [a] -> [Double]) -> [a] -> [Chart Double] scannerChart n rs rscan xs = addLineX 0 zeroLineStyle $ stdLineChart 0.005 palette1 (tsrs n rs rscan xs) where tsrs n rs rscan xs = taker n . (`rscan` xs) <$> rs -- | take a multi-decaying scanner, a decay rate, and create linecharts from an [a] scannersChart :: Int -> Double -> (Double -> [a] -> [[Double]]) -> [a] -> [Chart Double] scannersChart n r rscan xs = addLineX 0 zeroLineStyle $ stdLineChart 0.005 palette1 (tsrs n r rscan xs) where tsrs n r rscan xs = taker n <$> (List.transpose $ rscan r xs) -- | a Hud for time series with a rates legend tsRatesHud :: Text -> [Double] -> [UTCTime] -> HudOptions tsRatesHud title rs ds = defaultHudOptions & #hudTitles .~ [defaultTitle title & #style . #size .~ 0.08] & #hudLegend .~ Just (lineLegend 0.001 ((("rate: " <>) . show) <$> rs) palette1) & #hudAxes .~ tsAxes ds chartAny :: Text -> Either Text Text chartAny t = maybe (Left "bad read") Right . head . rights $ [ -- single list tryChart t (\xs -> bool (let (h,c) = barChart defaultBarOptions (BarData [xs] Nothing Nothing) in renderHudOptionsChart defaultSvgOptions h [] c) (anyLineChart [xs]) (length xs > 10) ), -- double list tryChart t chartList2, -- tuple tryChart t (\x -> anyScatterChart [x]), tryChart t anyScatterChart, -- text tryChart t anyTextChart, tryChart t chartText1, -- just text FIXME: doesn't parse for escaped characters tryChart ("\"" <> t <> "\"") (\x -> renderHudOptionsChart defaultSvgOptions mempty [] [Chart (TextA defaultTextStyle ["\"" <> x <> "\""]) [zero]]) ] chartList2 :: [[Double]] -> Text chartList2 xss | (length xss < 4) && (length (xss!!0) < 10) = anyBarChart xss -- square | all (length xss ==) (length <$> xss) = anyPixelChart xss | otherwise = anyLineChart xss chartText1 :: [Text] -> Text chartText1 xs | (length xs < 20) = anyTextChart [xs] -- word salad | otherwise = anySingleNamedBarChart (second fromIntegral <$> HashMap.toList mapCount) where mapCount = foldl' (\m x -> HashMap.insertWith (+) x (1::Int) m) HashMap.empty xs anyBarChart :: [[Double]] -> Text anyBarChart xss = renderHudOptionsChart defaultSvgOptions h [] c where (h,c) = barChart defaultBarOptions (BarData xss (Just (("row " <>) . show <$> take nx [(0::Int)..])) (Just (("col " <>) . show <$> take ny [(0::Int)..])) ) where nx = length xss ny = maximum (length <$> xss) anySingleNamedBarChart :: [(Text, Double)] -> Text anySingleNamedBarChart xs = renderHudOptionsChart defaultSvgOptions h [] c where (h,c) = barChart defaultBarOptions (BarData [snd <$> xs] (Just (fst <$> xs)) Nothing ) anyLineChart :: [[Double]] -> Text anyLineChart xss = renderHudOptionsChart defaultSvgOptions defaultHudOptions [] (stdLineChart 0.02 palette1 xss) anyScatterChart :: [[(Double, Double)]] -> Text anyScatterChart xss = renderHudOptionsChart defaultSvgOptions defaultHudOptions [] (scatterChart (fmap (fmap (uncurry Point)) xss)) anyTextChart :: [[Text]] -> Text anyTextChart xss = renderHudOptionsChart defaultSvgOptions defaultHudOptions [] (tableChart xss) anyPixelChart :: [[Double]] -> Text anyPixelChart xss = renderHudOptionsChart defaultSvgOptions (anyPixelHud nx ny) h c where (c,h) = pixelfl (\(Point x y) -> ((xss !! (floor x)) !! (floor y))) (PixelOptions defaultPixelStyle (Point nx ny) (Rect 0 (fromIntegral nx :: Double) 0 (fromIntegral ny))) (defaultPixelLegendOptions "square") nx = length xss ny = length (xss!!0) anyPixelHud :: Int -> Int -> HudOptions anyPixelHud nx ny = defaultHudOptions & #hudAxes .~ [ defaultAxisOptions & #atick . #tstyle .~ TickPlaced (zip ((0.5 +) <$> [0 ..]) labelsy) & #place .~ PlaceLeft, defaultAxisOptions & #atick . #tstyle .~ TickPlaced (zip ((0.5 +) <$> [0 ..]) labelsx) & #place .~ PlaceBottom ] where labelsx = show <$> [0..(nx - 1)] labelsy = show <$> [0..(ny - 1)] tryChart :: (Read a) => Text -> (a -> Text) -> Either Text Text tryChart t c = either (Left . pack) (Right . c) $ readEither (unpack t) write' :: Text -> IO () write' t = writeFile "scratch.svg" $ either id id $ chartAny t