{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Plots.Types
(
PlotOptions
, HasPlotOptions (..)
, key
, addLegendEntry
, PlotMods
, plotMods
, Plotable (..)
, Plot
, mkPlot
, rawPlot
, DynamicPlot (..)
, _DynamicPlot
, dynamicPlot
, dynamicPlotMods
, StyledPlot
, styledPlot
, styleDynamic
, renderStyledPlot
, singleStyledPlotLegend
, styledPlotLegends
, HasVisibility (..)
, hide
, display
, Orientation (..)
, HasOrientation (..)
, orient
, horizontal
, vertical
, LegendEntry
, LegendPic (..)
, mkLegendEntry
, legendPicture
, legendText
, legendPrecedence
, AxisSpec (..)
, specTrans
, specBounds
, specScale
, scaleNum
, specPoint
, specColourMap
, Placement (..)
, HasPlacement (..)
, HasGap (..)
, placeAgainst
, topLeft, top, topRight, left, right, bottomLeft, bottom
, bottomRight
, leftAbove, leftTop, leftMid, leftBottom, leftBelow, midAbove, midBelow
, rightAbove, rightTop, rightMid, rightBottom, rightBelow
) where
import Control.Monad.State
import Data.Bool
import Data.List (sortBy)
import Data.Maybe (fromMaybe)
import Data.Ord (comparing)
import Data.Orphans ()
import Data.Typeable
import Diagrams.Prelude as D
import Plots.Axis.Scale
import Plots.Style
import Plots.Util
data Orientation = Horizontal | Vertical
deriving (Show, Eq, Ord, Typeable)
orient :: HasOrientation o => o -> a -> a -> a
orient o h v =
case view orientation o of
Horizontal -> h
Vertical -> v
class HasOrientation a where
orientation :: Lens' a Orientation
instance HasOrientation Orientation where
orientation = id
horizontal :: HasOrientation a => Lens' a Bool
horizontal = orientation . iso (==Horizontal) (bool Vertical Horizontal)
vertical :: HasOrientation a => Lens' a Bool
vertical = horizontal . involuted not
class HasGap a where
gap :: Lens' a (N a)
data Placement = Placement
{ pAt :: V2 Rational
, pAnchor :: V2 Rational
, pGapDir :: Direction V2 Rational
}
deriving (Show, Read, Eq, Ord)
class HasPlacement a where
placement :: Lens' a Placement
placementAt :: Lens' a (V2 Rational)
placementAt = placement . lens pAt (\p a -> p {pAt = a})
placementAnchor :: Lens' a (V2 Rational)
placementAnchor = placement . lens pAnchor (\p a -> p {pAnchor = a})
gapDirection :: Lens' a (Direction V2 Rational)
gapDirection = placement . lens pGapDir (\p a -> p {pGapDir = a})
instance HasPlacement Placement where
placement = id
pInside :: V2 Rational -> Placement
pInside v = Placement
{ pAt = v
, pAnchor = v
, pGapDir = dirBetween' (P v) origin
}
dirBetween' :: (Additive v, Num n) => Point v n -> Point v n -> Direction v n
dirBetween' p q = direction $ q .-. p
topLeft, top, topRight, left, right, bottomLeft, bottom, bottomRight :: Placement
topLeft = pInside (V2 (-1) 1 )
top = pInside (V2 0 1 )
topRight = pInside (V2 1 1 )
left = pInside (V2 (-1) 0 )
right = pInside (V2 (-1) 0 )
bottomLeft = pInside (V2 (-1) (-1))
bottom = pInside (V2 0 (-1))
bottomRight = pInside (V2 1 (-1))
leftAbove, leftTop, leftMid, leftBottom, leftBelow, midAbove, midBelow,
rightAbove, rightTop, rightMid, rightBottom, rightBelow :: Placement
leftAbove = Placement (V2 (-1) 1 ) (V2 (-1) (-1)) (direction (V2 0 1 ))
leftTop = Placement (V2 (-1) 1 ) (V2 1 1 ) (direction (V2 (-1) 0 ))
leftMid = Placement (V2 (-1) 0 ) (V2 1 0 ) (direction (V2 (-1) 0 ))
leftBottom = Placement (V2 (-1) (-1)) (V2 1 (-1)) (direction (V2 (-1) 0 ))
leftBelow = Placement (V2 (-1) (-1)) (V2 (-1) 1 ) (direction (V2 0 (-1)))
midAbove = Placement (V2 0 1 ) (V2 0 (-1)) (direction (V2 0 1 ))
midBelow = Placement (V2 0 (-1)) (V2 0 1 ) (direction (V2 0 (-1)))
rightAbove = Placement (V2 1 1 ) (V2 1 (-1)) (direction (V2 0 1 ))
rightTop = Placement (V2 1 1 ) (V2 (-1) 1 ) (direction (V2 1 0 ))
rightMid = Placement (V2 1 0 ) (V2 (-1) 0 ) (direction (V2 1 0 ))
rightBottom = Placement (V2 1 (-1)) (V2 (-1) (-1)) (direction (V2 1 0 ))
rightBelow = Placement (V2 1 (-1)) (V2 1 1 ) (direction (V2 0 (-1)))
placeAgainst
:: (InSpace V2 n a, SameSpace a b, Enveloped a,
HasOrigin b, Alignable b)
=> a -> Placement -> n -> b -> b
placeAgainst a (Placement (V2 px py) (V2 ax ay) d) n b
= b # anchor
# moveTo (pos .+^ n *^ fromDirection (fmap fromRational d))
where
pos = mkP2 (lerp' px xu xl) (lerp' py yu yl)
anchor = alignBy unitX (fromRational ax) . alignBy unitY (fromRational ay)
(P (V2 xl yl), P (V2 xu yu)) = fromMaybe (origin, origin) (getCorners $ boundingBox a)
lerp' z u v = fromRational alpha * u + (1 - fromRational alpha) * v
where alpha = (z + 1) / 2
data LegendPic b v n
= DefaultLegendPic
| CustomLegendPic (PlotStyle b v n -> QDiagram b v n Any)
instance Default (LegendPic b v n) where
def = DefaultLegendPic
data LegendEntry b v n = LegendEntry
{ lPic :: LegendPic b v n
, lText :: String
, lPrecedence :: n
} deriving Typeable
legendPicture :: Lens' (LegendEntry b v n) (LegendPic b v n)
legendPicture = lens lPic (\l pic -> l {lPic = pic})
legendText :: Lens' (LegendEntry b v n) String
legendText = lens lText (\l txt -> l {lText = txt})
legendPrecedence :: Lens' (LegendEntry b v n) n
legendPrecedence = lens lPrecedence (\l n -> l {lPrecedence = n})
type instance V (LegendEntry b v n) = v
type instance N (LegendEntry b v n) = n
mkLegendEntry :: Num n => String -> LegendEntry b v n
mkLegendEntry x = LegendEntry DefaultLegendPic x 0
data PlotOptions b v n = PlotOptions
{ poName :: Name
, poClipPlot :: Bool
, poLegend :: [LegendEntry b v n]
, poVisible :: Bool
, poTransform :: Transformation v n
} deriving Typeable
type instance V (PlotOptions b v n) = v
type instance N (PlotOptions b v n) = n
class HasPlotOptions f a b | a -> b where
{-# MINIMAL plotOptions #-}
plotOptions :: LensLike' f a (PlotOptions b (V a) (N a))
plotName :: Functor f => LensLike' f a Name
plotName = plotOptions . lens poName (\g a -> g {poName = a})
{-# INLINE plotName #-}
clipPlot :: Functor f => LensLike' f a Bool
clipPlot = plotOptions . lens poClipPlot (\g a -> g {poClipPlot = a})
{-# INLINE clipPlot #-}
legendEntries :: Functor f => LensLike' f a [LegendEntry b (V a) (N a)]
legendEntries = plotOptions . lens poLegend (\g a -> g {poLegend = a})
{-# INLINE legendEntries #-}
plotTransform :: Functor f => LensLike' f a (Transformation (V a) (N a))
plotTransform = plotOptions . lens poTransform (\g a -> g {poTransform = a})
{-# INLINE plotTransform #-}
plotVisible :: Functor f => LensLike' f a Bool
plotVisible = plotOptions . lens poVisible (\po b -> po {poVisible = b})
{-# INLINE plotVisible #-}
instance (Additive v, Num n) => Default (PlotOptions b v n) where
def = PlotOptions
{ poName = mempty
, poClipPlot = True
, poLegend = []
, poVisible = True
, poTransform = mempty
}
instance HasPlotOptions f (PlotOptions b v n) b where
plotOptions = id
{-# INLINE plotOptions #-}
instance (HasLinearMap v, Num n) => Transformable (PlotOptions b v n) where
transform = over plotTransform . transform
instance (Additive v, Num n) => HasOrigin (PlotOptions b v n) where
moveOriginTo = over plotTransform . moveOriginTo
instance Qualifiable (PlotOptions b v n) where
n .>> p = over plotName (n .>>) p
key :: (HasPlotOptions Identity a b, MonadState a m, Num (N a)) => String -> m ()
key = addLegendEntry . mkLegendEntry
addLegendEntry
:: (HasPlotOptions Identity a b, MonadState a m)
=> LegendEntry b (V a) (N a)
-> m ()
addLegendEntry l = legendEntries <>= [l]
data AxisSpec v n = AxisSpec
{ _specBounds :: v (n, n)
, _specTrans :: Transformation v n
, _specScale :: v LogScale
, _specColourMap :: ColourMap
}
makeLenses ''AxisSpec
type instance V (AxisSpec v n) = v
type instance N (AxisSpec v n) = n
-- | Scale a number by log10-ing it and linearly scaling it so it's
-- within the same range.
scaleNum :: Floating n => (n, n) -> LogScale -> n -> n
scaleNum (a,b) s x = case s of
LinearAxis -> x
LogAxis -> subtract a $ (b / logBase 10 d) * (logBase 10 x)
where d = b - a
-- | Apply log scaling and the transform to a point.
specPoint :: (Applicative v, Additive v, Floating n) => AxisSpec v n -> Point v n -> Point v n
specPoint (AxisSpec bs tr ss _) p =
papply tr $ over _Point (scaleNum <$> bs <*> ss <*>) p
------------------------------------------------------------------------
-- Plotable class
------------------------------------------------------------------------
-- | Class defining how plots should be rendered.
class (Typeable p, Enveloped p) => Plotable p b where
-- | Render a plot according to the 'AxisSpec', using the 'PlotStyle'.
renderPlotable
:: InSpace v n p
=> AxisSpec v n
-> PlotStyle b v n
-> p
-> QDiagram b v n Any
-- | The default legend picture when the 'LegendPic' is
-- 'DefaultLegendPic'.
defLegendPic
:: InSpace v n p
=> PlotStyle b v n
-> p
-> QDiagram b v n Any
defLegendPic = mempty
instance (Typeable b, Typeable v, Metric v, Typeable n, OrderedField n)
=> Plotable (QDiagram b v n Any) b where
renderPlotable s _ dia = dia # transform (s^.specTrans)
instance (TypeableFloat n, Renderable (Path V2 n) b) => Plotable (Path V2 n) b where
renderPlotable s sty path
= stroke path
# transform (s^.specTrans)
# applyLineStyle sty
defLegendPic sty _
= (p2 (-10,0) ~~ p2 (10,0))
# applyLineStyle sty
------------------------------------------------------------------------
-- Visibility
------------------------------------------------------------------------
-- | Class of objects that can be hidden.
class HasVisibility a where
-- | Lens onto whether an object should be visible when rendered.
visible :: Lens' a Bool
-- | The opposite of 'visible'.
hidden :: Lens' a Bool
hidden = visible . involuted not
{-# INLINE hidden #-}
instance HasVisibility (PlotOptions b v n) where
visible = plotVisible
instance HasVisibility (PlotMods b v n) where
visible = plotVisible
instance HasVisibility (Plot p b) where
visible = plotVisible
instance HasVisibility (DynamicPlot b v n) where
visible = plotVisible
instance HasVisibility (StyledPlot b v n) where
visible = plotVisible
-- | Set 'visible' to 'False' for the given setter.
--
-- @
-- 'hide' 'minorTicks' :: 'State' ('Axis' b v n) ()
-- 'hide' ('xAxis' . 'gridLines') :: 'State' ('Axis' b v n) ()
-- @
hide :: (MonadState s m, HasVisibility a) => ASetter' s a -> m ()
hide l = l . visible .= False
-- | Set 'visible' to 'True' for the given setter.
--
-- @
-- 'display' 'minorGridLines' :: 'State' ('Axis' b v n) ()
-- 'display' 'colourBar' :: 'State' ('Axis' b v n) ()
-- @
display :: (MonadState s m, HasVisibility a) => ASetter' s a -> m ()
display l = l . visible .= True
------------------------------------------------------------------------
-- Plot modification
------------------------------------------------------------------------
-- | A 'PlotOptions' with modifications to a 'PlotStyle'.
data PlotMods b v n
= PlotMods (PlotOptions b v n) (PlotStyle b v n -> PlotStyle b v n)
type instance V (PlotMods b v n) = v
type instance N (PlotMods b v n) = n
instance Functor f => HasPlotOptions f (PlotMods b v n) b where
plotOptions f (PlotMods opts sty) = f opts <&> \opts' -> PlotMods opts' sty
instance Settable f => HasPlotStyle f (PlotMods b v n) b where
plotStyle = sty . mapped where
sty f (PlotMods opts s) = f s <&> \s' -> PlotMods opts s'
instance (Additive v, Num n) => Default (PlotMods b v n) where
def = PlotMods def id
------------------------------------------------------------------------
-- Plot type
------------------------------------------------------------------------
-- | A parameterised plot, together with a 'PlotMods'. This type has an
-- instance of many classes for modifying specific plots.
data Plot p b =
Plot p
(PlotOptions b (V p) (N p))
(PlotStyle b (V p) (N p) -> PlotStyle b (V p) (N p))
deriving Typeable
type instance V (Plot p b) = V p
type instance N (Plot p b) = N p
instance Functor f => HasPlotOptions f (Plot p b) b where
plotOptions f (Plot p opts sty) = f opts <&> \opts' -> Plot p opts' sty
instance Settable f => HasPlotStyle f (Plot p b) b where
plotStyle = sty . mapped where
sty f (Plot p opts s) = f s <&> \s' -> Plot p opts s'
instance HasOrientation p => HasOrientation (Plot p b) where
orientation = rawPlot . orientation
-- | Make a 'Plot' with 'Default' 'PlotOptions'.
mkPlot :: (Additive (V p), Num (N p)) => p -> Plot p b
mkPlot p = Plot p def id
-- | Lens onto the raw 'Plotable' inside a 'Plot'.
rawPlot :: SameSpace p p' => Lens (Plot p b) (Plot p' b) p p'
rawPlot f (Plot p opts ps) = f p <&> \p' -> Plot p' opts ps
-- | The modifications to the 'PlotOptions' and 'PlotStyle' in a 'Plot'.
plotMods :: Lens' (Plot p b) (PlotMods b (V p) (N p))
plotMods f (Plot p opts ps) =
f (PlotMods opts ps) <&> \(PlotMods opts' ps') -> Plot p opts' ps'
------------------------------------------------------------------------
-- DynamicPlot
------------------------------------------------------------------------
-- | A wrapped up 'Plot', used to store plots in an 'Axis'.
data DynamicPlot b v n where
DynamicPlot :: (InSpace v n p, Plotable p b) => Plot p b -> DynamicPlot b v n
deriving Typeable
type instance V (DynamicPlot b v n) = v
type instance N (DynamicPlot b v n) = n
-- | Prism for a 'DynamicPlot'.
_DynamicPlot :: (Plotable p b, Typeable b) => Prism' (DynamicPlot b (V p) (N p)) (Plot p b)
_DynamicPlot = prism' DynamicPlot (\(DynamicPlot p) -> cast p)
-- | Traversal over the dynamic plot without the 'Plotable' constraint
-- '_DynamicPlot' has.
dynamicPlot :: forall p b. (Typeable p, Typeable b)
=> Traversal' (DynamicPlot b (V p) (N p)) (Plot p b)
dynamicPlot f d@(DynamicPlot p) =
case eq p of
Just Refl -> f p <&> \p' -> DynamicPlot p'
Nothing -> pure d
where eq :: Typeable a => a -> Maybe (a :~: Plot p b)
eq _ = eqT
instance Functor f => HasPlotOptions f (DynamicPlot b v n) b where
plotOptions f (DynamicPlot (Plot p opts sty)) =
f opts <&> \opts' -> DynamicPlot (Plot p opts' sty)
instance Settable f => HasPlotStyle f (DynamicPlot b v n) b where
plotStyle = sty . mapped where
sty :: Setter' (DynamicPlot b v n) (PlotStyle b v n -> PlotStyle b v n)
sty f (DynamicPlot (Plot p opts s)) = f s <&> \s' -> DynamicPlot (Plot p opts s')
-- | The modifications to the 'PlotOptions' and 'PlotStyle' in a 'DynamicPlot'.
dynamicPlotMods :: Lens' (DynamicPlot b v n) (PlotMods b v n)
dynamicPlotMods f (DynamicPlot (Plot p opts ps)) =
f (PlotMods opts ps) <&> \(PlotMods opts' ps') -> DynamicPlot (Plot p opts' ps')
------------------------------------------------------------------------
-- StyledPlot
------------------------------------------------------------------------
-- | A 'DynamicPlot' with a concrete style. This is suitable for being
-- rendered with 'renderStyledPlot' and get extract the legend entries
-- with 'styledPlotLegend'.
--
-- You can make a 'StyledPlot' with 'styleDynamic'
data StyledPlot b v n where
StyledPlot
:: Plotable p b
=> p
-> PlotOptions b (V p) (N p)
-> PlotStyle b (V p) (N p)
-> StyledPlot b (V p) (N p)
type instance V (StyledPlot b v n) = v
type instance N (StyledPlot b v n) = n
instance Functor f => HasPlotOptions f (StyledPlot b v n) b where
plotOptions f (StyledPlot p opts sty) =
f opts <&> \opts' -> StyledPlot p opts' sty
instance (Metric v, OrderedField n) => Enveloped (StyledPlot b v n) where
getEnvelope (StyledPlot p opts _) =
getEnvelope p & transform (poTransform opts)
instance Functor f => HasPlotStyle f (StyledPlot b v n) b where
plotStyle f (StyledPlot p opts sty) =
f sty <&> StyledPlot p opts
-- | Traversal over a raw plot of a styled plot. The type of the plot
-- must match for the traversal to be successful.
styledPlot :: forall p b. Typeable p => Traversal' (StyledPlot b (V p) (N p)) p
styledPlot f s@(StyledPlot p opts sty) =
case eq p of
Just Refl -> f p <&> \p' -> StyledPlot p' opts sty
Nothing -> pure s
where eq :: Typeable a => a -> Maybe (a :~: p)
eq _ = eqT
-- | Give a 'DynamicPlot' a concrete 'PlotStyle'.
styleDynamic :: PlotStyle b v n -> DynamicPlot b v n -> StyledPlot b v n
styleDynamic sty (DynamicPlot (Plot p opts styF)) = StyledPlot p opts (styF sty)
-- | Render a 'StyledPlot' given an and 'AxisSpec'.
renderStyledPlot
:: TypeableFloat n
=> AxisSpec V2 n
-> StyledPlot b V2 n
-> QDiagram b V2 n Any
renderStyledPlot aSpec (StyledPlot p opts sty)
= renderPlotable aSpec sty p
& whenever (opts^.hidden) phantom
& whenever (opts^.clipPlot) (clipTo $ specRect aSpec)
specRect :: TypeableFloat n => AxisSpec V2 n -> Path V2 n
specRect aSpec =
rect (xU - xL) (yU - yL)
# moveTo (mkP2 ((xU+xL)/2) ((yU+yL)/2))
# transform t
where
V2 (xL,xU) (yL,yU) = _specBounds aSpec
t = _specTrans aSpec
-- | Get the legend rendered entries from a single styled plot. The
-- resulting entries are in no particular order. See also
-- 'styledPlotLegends'.
singleStyledPlotLegend
:: StyledPlot b v n
-> [(n, QDiagram b v n Any, String)] -- ^ @(z-order, legend pic, legend text)@
singleStyledPlotLegend (StyledPlot p opts sty) =
map mk (opts ^. legendEntries)
where
mk entry = (entry ^. legendPrecedence, pic, entry ^. legendText)
where
pic = case lPic entry of
DefaultLegendPic -> defLegendPic sty p
CustomLegendPic f -> f sty
-- | Render a list of legend entries, in order.
styledPlotLegends
:: Ord n
=> [StyledPlot b v n]
-> [(QDiagram b v n Any, String)] -- ^ @[(legend pic, legend text)]@
styledPlotLegends
= map (\(_,p,t) -> (p,t))
. sortOn (view _1)
. concatMap singleStyledPlotLegend
-- XXX taken from "Data.List", defined here because it was only added in
-- base-4.8 (ghc-7.10)
sortOn :: Ord b => (a -> b) -> [a] -> [a]
sortOn f =
map snd . sortBy (comparing fst) . map (\x -> let y = f x in y `seq` (y, x))