{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Plots.Types.HeatMap
(
HeatMap
, heatMap
, heatMap'
, heatMapIndexed
, heatMapIndexed'
, HasHeatMap (..)
, pathHeatRender
, pixelHeatRender
, pixelHeatRender'
, HeatMatrix
, heatImage
, hmPoints
, hmSize
, mkHeatMap
, mkHeatMatrix
, mkHeatMatrix'
) where
import Control.Lens hiding (transform, ( # ))
import qualified Data.Colour as C
import Control.Monad.ST
import Control.Monad.State
import qualified Data.Foldable as F
import Data.Typeable
import qualified Data.Vector.Generic.Mutable as M
import qualified Data.Vector.Storable as S
import qualified Data.Vector.Unboxed as V
import Data.Word (Word8)
import Statistics.Function (minMax)
import Codec.Picture
import Diagrams.Coordinates.Isomorphic
import Diagrams.Prelude
import Plots.Axis
import Plots.Style
import Plots.Types
data HeatMatrix = HeatMatrix
{ hmSize :: {-# UNPACK #-} !(V2 Int)
, _hmVector :: {-# UNPACK #-} !(V.Vector Double)
, hmBoundLower :: {-# UNPACK #-} !Double
, hmBoundUpper :: {-# UNPACK #-} !Double
}
mkHeatMatrix :: V2 Int -> (V2 Int -> Double) -> HeatMatrix
mkHeatMatrix s@(V2 x y) f = runST $ do
mv <- M.new (x*y)
let go !q !a !b !i !j
| j == y = do v <- V.unsafeFreeze mv
return (HeatMatrix s v a b)
| i == x = go q a b 0 (j + 1)
| otherwise = do let !d = f (V2 i j)
M.unsafeWrite mv q d
go (q + 1) (min a d) (max b d) (i + 1) j
go 0 (1/0) (-1/0) 0 0
{-# INLINE mkHeatMatrix #-}
mkHeatMatrix' :: (F.Foldable f, F.Foldable g) => f (g Double) -> HeatMatrix
mkHeatMatrix' xss = HeatMatrix (V2 x y) vd a b
where
(a,b) = minMax vd
vd = V.create $ do
mv <- M.new (x*y)
let go !_ [] = return mv
go j (r:rs) = V.unsafeCopy (M.unsafeSlice (j*x) x mv) r >> go (j-1) rs
go (y - 1) vs
(!x,!y,!vs) = F.foldl' f (maxBound,0,[]) xss
f (!i,!j,!ss) xs = let !v = V.fromList (F.toList xs)
in (min i (V.length v), j+1, v : ss)
hmPoints :: IndexedTraversal' (V2 Int) HeatMatrix Double
hmPoints f (HeatMatrix e@(V2 x y) v a b) =
go 0 0 0 <&> \vs ->
let v'= V.fromListN (x*y) vs
in HeatMatrix e v' a b
where
go !s !i !j
| i >= x = go s 0 (j+1)
| j >= y = pure []
| otherwise = (:) <$> indexed f (V2 i j) (V.unsafeIndex v s)
<*> go (s+1) (i+1) j
{-# INLINE [0] hmPoints #-}
{-# RULES
"hmPoints/foldr"
hmPoints = ifoldring hmFold :: Getting (Endo r) HeatMatrix Double;
"hmPoints/ifoldr"
hmPoints = ifoldring hmFold :: IndexedGetting (V2 Int) (Endo r) HeatMatrix Double
#-}
hmFold :: (V2 Int -> Double -> b -> b) -> b -> HeatMatrix -> b
hmFold f b0 (HeatMatrix (V2 x y) v _ _) = go 0 0 0 b0 where
go !s !i !j b
| i >= x = go s 0 (j+1) b
| j >= y = b
| otherwise = f (V2 i j) (V.unsafeIndex v s) (go (s+1) (i+1) j b)
{-# INLINE hmFold #-}
pixelHeatRender
:: (Renderable (DImage n Embedded) b, TypeableFloat n)
=> HeatMatrix
-> ColourMap
-> QDiagram b V2 n Any
pixelHeatRender hm cm =
alignBL . image $ DImage (ImageRaster (ImageRGB8 img)) x y mempty
where
img = heatImage hm cm
V2 x y = hmSize hm
pixelHeatRender'
:: (Renderable (DImage n Embedded) b, TypeableFloat n)
=> Int
-> HeatMatrix
-> ColourMap
-> QDiagram b V2 n Any
pixelHeatRender' n hm cm =
scale (1/fromIntegral n) . alignBL . image $ DImage (ImageRaster (ImageRGB8 img)) (x*n) (y*n) mempty
where
img = scaleImage n $ heatImage hm cm
V2 x y = hmSize hm
scaleImage :: Int -> Image PixelRGB8 -> Image PixelRGB8
scaleImage n img | n == 1 = img
| n == 0 = Image 0 0 S.empty
| n < 0 = error "scaleImage: negative scale"
scaleImage n (Image x y v) = Image (n*x) (n*y) vn where
!refV = V.fromList $ map (*3) [ i + n*x*j | i <- [0..n-1], j <- [0..n-1] ]
!n3 = 3*n
vn = S.create $ do
mv <- M.new (n * n * S.length v)
let go !q !i !s | q >= 3*x*y = return mv
| i == x = go q 0 (s + 3*x*n*(n-1))
go q i s = do
let !r = S.unsafeIndex v q
!g = S.unsafeIndex v (q+1)
!b = S.unsafeIndex v (q+2)
V.forM_ refV $ \ds -> do
M.unsafeWrite mv (s + ds ) r
M.unsafeWrite mv (s + ds + 1) g
M.unsafeWrite mv (s + ds + 2) b
go (q+3) (i+1) (s+n3)
go 0 0 0
heatImage :: HeatMatrix -> ColourMap -> Image PixelRGB8
heatImage (HeatMatrix (V2 x y) dv a b) cm = Image x y v' where
!cv = mkColourVector cm
v' = S.create $ do
mv <- M.new (3 * x * y)
let !m = 256 / (b - a)
go s i q
| s == x-1 = do
let !d = V.unsafeIndex dv s
let !o = 3 * (min 255 . max 0 . round $ (d - a) * m)
M.unsafeWrite mv q (V.unsafeIndex cv o )
M.unsafeWrite mv (q+1) (V.unsafeIndex cv (o+1))
M.unsafeWrite mv (q+2) (V.unsafeIndex cv (o+2))
return mv
| i == x = go (s - 2*x) 0 q
| otherwise = do
let !d = V.unsafeIndex dv s
let !o = 3 * (min 255 . max 0 . round $ (d - a) * m)
M.unsafeWrite mv q (V.unsafeIndex cv o )
M.unsafeWrite mv (q+1) (V.unsafeIndex cv (o+1))
M.unsafeWrite mv (q+2) (V.unsafeIndex cv (o+2))
go (s+1) (i+1) (q+3)
go (x * (y-1)) 0 0
mkColourVector :: ColourMap -> V.Vector Word8
mkColourVector cm = V.create $ do
mv <- M.new (3*256)
let go i | i == 3*256 = return mv
| otherwise = do
let PixelRGB8 r g b = cm ^. ixColour (fromIntegral i / (3*256))
. to colourToPixel
M.unsafeWrite mv i r
M.unsafeWrite mv (i+1) g
M.unsafeWrite mv (i+2) b
go (i+3)
go 0
colourToPixel :: AlphaColour Double -> PixelRGB8
colourToPixel c = PixelRGB8 r g b
where RGB r g b = toSRGB24 (c `C.over` black)
pathHeatRender
:: (Renderable (Path V2 n) b, TypeableFloat n)
=> HeatMatrix
-> ColourMap
-> QDiagram b V2 n Any
pathHeatRender hm@(HeatMatrix _ _ a b) cm = ifoldMapOf hmPoints mk hm # lwO 0
where
normalise d = toRational $ (d - a) / (b - a)
mk v@(V2 i j) d =
rect w h
# alignTR
# translate (fromIntegral <$> v ^+^ 1)
# fcA (cm ^. ixColour (normalise d))
where
w | i == 0 = 1
| otherwise = 1.5
h | j == 0 = 1
| otherwise = 1.5
data HeatMap b n = HeatMap
{ hMatrix :: HeatMatrix
, hStart :: P2 n
, hSize :: V2 n
, hGridSty :: Style V2 n
, hGridVisible :: Bool
, hLimits :: Maybe (Double,Double)
, hDraw :: HeatMatrix -> ColourMap -> QDiagram b V2 n Any
} deriving Typeable
type instance V (HeatMap b n) = V2
type instance N (HeatMap b n) = n
class HasHeatMap f a b | a -> b where
heatMapOptions :: LensLike' f a (HeatMap b (N a))
heatMapGridVisible :: Functor f => LensLike' f a Bool
heatMapGridVisible = heatMapOptions . lens hGridVisible (\s b -> (s {hGridVisible = b}))
heatMapGridStyle :: Functor f => LensLike' f a (Style V2 (N a))
heatMapGridStyle = heatMapOptions . lens hGridSty (\s b -> (s {hGridSty = b}))
heatMapSize :: Functor f => LensLike' f a (V2 (N a))
heatMapSize = heatMapOptions . lens hSize (\s b -> (s {hSize = b}))
heatMapExtent :: (Functor f, Fractional (N a)) => LensLike' f a (V2 (N a))
heatMapExtent = heatMapOptions . l where
l f hm = f (hSize hm * s) <&> \x -> hm { hSize = x / s }
where s = fmap fromIntegral (hmSize $ hMatrix hm)
heatMapStart :: Functor f => LensLike' f a (P2 (N a))
heatMapStart = heatMapOptions . lens hStart (\s b -> (s {hStart = b}))
heatMapCentre :: (Functor f, Fractional (N a)) => LensLike' f a (P2 (N a))
heatMapCentre = heatMapOptions . l where
l f hm = f (hStart hm .+^ v) <&> \p -> hm { hStart = p .-^ v }
where v = fmap fromIntegral (hmSize $ hMatrix hm) * hSize hm / 2
heatMapLimits :: Functor f => LensLike' f a (Maybe (Double, Double))
heatMapLimits = heatMapOptions . lens hLimits (\s b -> (s {hLimits = b}))
heatMapRender :: Functor f => LensLike' f a (HeatMatrix -> ColourMap -> QDiagram b V2 (N a) Any)
heatMapRender = heatMapOptions . lens hDraw (\s b -> (s {hDraw = b}))
instance HasHeatMap f (HeatMap b n) b where
heatMapOptions = id
instance (Functor f, HasHeatMap f a b) => HasHeatMap f (Plot a b) b where
heatMapOptions = rawPlot . heatMapOptions
instance OrderedField n => Enveloped (HeatMap b n) where
getEnvelope hm = getEnvelope (fromCorners p (p .+^ v))
where p = view heatMapStart hm
v = view heatMapExtent hm
instance (Typeable b, TypeableFloat n, Renderable (Path V2 n) b)
=> Plotable (HeatMap b n) b where
renderPlotable s _sty HeatMap {..} =
transform (s^.specTrans) $
grid <> hDraw matrix' (s^.specColourMap)
# transform (scaleV hSize)
# moveTo hStart
where
grid = mempty
matrix' = case hLimits of
Just (a,b) -> hMatrix { hmBoundLower = a, hmBoundUpper = b }
Nothing -> hMatrix
defLegendPic sty HeatMap {..} = square 5 # applyAreaStyle sty
scaleV :: (Additive v, Fractional n) => v n -> Transformation v n
scaleV v = fromLinear f f
where f = (liftU2 (*) v) <-> (\u -> liftU2 (/) u v)
mkHeatMap :: (Renderable (Path V2 n) b, TypeableFloat n)
=> HeatMatrix -> HeatMap b n
mkHeatMap mat = HeatMap
{ hMatrix = mat
, hStart = origin
, hSize = V2 1 1
, hGridSty = mempty
, hGridVisible = False
, hLimits = Nothing
, hDraw = pathHeatRender
}
heatMap
:: (F.Foldable f,
F.Foldable g,
TypeableFloat n,
Typeable b,
MonadState (Axis b V2 n) m,
Renderable (Path V2 n) b)
=> f (g Double)
-> State (Plot (HeatMap b n) b) ()
-> m ()
heatMap xss s = do
let hm@(HeatMatrix _ _ a b) = mkHeatMatrix' xss
addPlotable (mkHeatMap hm) s
colourBarRange .= over both realToFrac (a,b)
heatMap'
:: (F.Foldable f,
F.Foldable g,
TypeableFloat n,
Typeable b,
MonadState (Axis b V2 n) m,
Renderable (Path V2 n) b)
=> f (g Double)
-> m ()
heatMap' xss = heatMap xss (return ())
heatMapIndexed
:: (VectorLike V2 Int i,
TypeableFloat n,
Typeable b,
MonadState (Axis b V2 n) m,
Renderable (Path V2 n) b)
=> i
-> (i -> Double)
-> State (Plot (HeatMap b n) b) ()
-> m ()
heatMapIndexed i f s = do
let hm@(HeatMatrix _ _ a b) = mkHeatMatrix (view unvectorLike i) (f . view vectorLike)
addPlotable (mkHeatMap hm) s
colourBarRange .= over both realToFrac (a,b)
heatMapIndexed'
:: (VectorLike V2 Int i,
TypeableFloat n,
Typeable b,
MonadState (Axis b V2 n) m,
Renderable (Path V2 n) b)
=> i
-> (i -> Double)
-> m ()
heatMapIndexed' i f = heatMapIndexed i f (return ())