{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Plots.Axis.Render
(
RenderAxis (..)
, r2AxisMain
, buildPlots
)where
import Data.Bool
import Data.Foldable as F
import Data.List (sort)
import Data.Typeable
import Geometry.BoundingBox
import Diagrams.Prelude
import Geometry.Envelope
import Diagrams.TwoD.Text
import Linear hiding (rotate, translation)
import Diagrams.Backend.CmdLine
import Diagrams.Coordinates.Polar
import Plots.Axis
import Plots.Axis.ColourBar
import Plots.Axis.Grid
import Plots.Axis.Labels
import Plots.Axis.Line
import Plots.Axis.Scale
import Plots.Axis.Ticks
import Plots.Axis.Title
import Plots.Legend
import Plots.Style
import Plots.Types
import Plots.Util
import Prelude
import qualified Numeric.Interval.NonEmpty as I
import Geometry.TwoD.Transform
import Geometry.TwoD.Ellipse
import Diagrams.Types (Prim (..), mkQD)
instance WithOutcome (Axis Polar)
instance WithOutcome (Axis V2)
instance WithOutcome (Axis V3)
instance RenderOutcome t (Diagram V2) => RenderOutcome t (Axis Polar) where
type MainOpts t (Axis Polar) = MainOpts t (Diagram V2)
resultParser t _ = resultParser t (Proxy :: Proxy (Diagram V2))
renderOutcome t opts axis = renderOutcome t opts (renderPolarAxis axis)
instance RenderOutcome t (Diagram V2) => RenderOutcome t (Axis V2) where
type MainOpts t (Axis V2) = MainOpts t (Diagram V2)
resultParser t _ = resultParser t (Proxy :: Proxy (Diagram V2))
renderOutcome t opts axis = renderOutcome t opts (renderAxis axis)
instance RenderOutcome t (Diagram V3) => RenderOutcome t (Axis V3) where
type MainOpts t (Axis V3) = MainOpts t (Diagram V3)
resultParser t _ = resultParser t (Proxy :: Proxy (Diagram V3))
renderOutcome t opts axis = renderOutcome t opts (renderAxis axis)
strokePathV :: (Typeable v, Metric v, Typeable n, OrderedField n) => Path v n -> QDiagram v n Any
strokePathV path = mkQD (Prim path) (getEnvelope path) mempty mempty
r2AxisMain
:: RenderOutcome t (Diagram V2)
=> t
-> Axis V2
-> IO ()
r2AxisMain = mainWith
buildPlots :: BaseSpace c ~ v => Axis c -> [StyledPlot v]
buildPlots a = map (appEndo $ a ^. plotModifier)
$ zipWith styleDynamic (a ^.. axisStyles) (a ^. axisPlots)
class RenderAxis c where
renderAxis :: Axis c -> Diagram (BaseSpace c)
instance RenderAxis V2 where
renderAxis = renderR2Axis
renderR2Axis :: Axis V2 -> Diagram V2
renderR2Axis a = frame 40
$ leg
<> ttl
<> cBar
<> plots
<> drawAxis ex ey LowerLabels
<> drawAxis ey ex LeftLabels
where
spec = AxisSpec xs t (a^.axes . column logScale) (a ^. axisColourMap)
plots = foldMap (renderStyledPlot spec) styledPlots
drawAxis ll ll2 = axisOnBasis origin xs (a^.axes.el ll) (a^.axes.column logScale) t ll ll2
(xs, tv, t') = calculateScaling (a^.axes.column axisScaling) (boundingBox styledPlots)
t = tv <> t'
bb = fromCorners (P . apply t $ fmap fst xs) (P . apply t $ fmap snd xs)
leg = drawLegend bb (styledPlotLegends styledPlots) (a ^. legend)
cBar = addColourBar bb (a^.colourBar) (a ^. axisColourMap) (a^.colourBarRange)
ttl = drawTitle bb (a^.title)
styledPlots = buildPlots a
instance RenderAxis V3 where
renderAxis = renderR3Axis
renderR3Axis :: Axis V3 -> Diagram V3
renderR3Axis a =
(plots :: Diagram V3)
<> (drawAxis ex ey LowerLabels :: Diagram V3)
<> (drawAxis ey ex UpperLabels :: Diagram V3)
<> (drawAxis ez ey LowerLabels :: Diagram V3)
<> (drawAxis ey ez NoLabels :: Diagram V3)
<> (drawAxis ez ex NoLabels :: Diagram V3)
<> (drawAxis ex ez NoLabels :: Diagram V3)
where
spec = AxisSpec xs t (a^.axes . column logScale) (a ^. axisColourMap)
plots = foldMap (renderStyledPlot spec) styledPlots
drawAxis ll ll2 = axisOnBasis origin xs (a^.axes.el ll) (a^.axes.column logScale) t ll ll2
(xs, tv, t') = calculateScaling (a^.axes.column axisScaling) (boundingBox styledPlots)
t = tv <> t'
styledPlots = buildPlots a
data LabelPosition
= NoLabels
| LowerLabels
| LeftLabels
| RightLabels
| UpperLabels
deriving (Show, Eq, Typeable)
axisOnBasis
:: forall v. (HasLinearMap v, Typeable v)
=> Point v Double
-> v (Double, Double)
-> SingleAxis v
-> v LogScale
-> Transformation v Double
-> E v
-> E v
-> LabelPosition
-> Diagram v
axisOnBasis p bs a ls t e eO lp
| a ^. hidden = phantom axis
| otherwise = axis
where
axis = tickLabels <> axLabels <> ticks <> line <> grid
tStroke = strokePathV . transform t
axLabels
| null txt || lp == NoLabels || a ^. axisLabel . hidden
= mempty
| otherwise = (a ^. axisLabelTextFunction) txtAlign txt
# moveTo p'
# applyStyle (a ^. axisLabelStyle)
where
p' = p & ep e .~ x
& ep eO .~ y0
& coscale
& papply t
& ep eO +~ negate' labelGap
labelGap = a ^. axisLabelGap
txt = a ^. axisLabelText
x = case a ^. axisLabelPosition of
MiddleAxisLabel -> (x0 + x1) / 2
LowerAxisLabel -> x0
UpperAxisLabel -> x1
tickLabels
| lp == NoLabels || a ^. tickLabel . hidden = mempty
| otherwise = foldMap drawLabels (map snd $ take 1 ys)
# applyStyle (a ^. tickLabelStyle)
where
labelFun = a ^. tickLabelFunction
drawLabels y = foldMap f (labelFun (filter inRange majorTickXs) b)
where
f (x, l) = place dia p'
where
dia = view tickLabelTextFunction a txtAlign l
p' = p & ep e .~ x
& ep eO .~ y
& coscale
& papply t
& ep eO +~ negate' (a ^. tickLabelGap)
grid = majorLines <> minorLines
where
majorLines
| a ^. majorGridLines . hidden = mempty
| otherwise = foldMap mkGridLine majorGridXs'
# tStroke
# applyStyle (a ^. majorGridLinesStyle)
majorGridXs = view majorGridLinesFunction a majorTickXs b
majorGridXs' = map coscaleNum (filter inRange majorGridXs)
minorLines
| a ^. minorGridLines . hidden = mempty
| otherwise = foldMap mkGridLine minorGridXs'
# tStroke
# applyStyle (a ^. minorGridLinesStyle)
minorGridXs = view minorGridLinesFunction a minorTickXs b
minorGridXs' = map coscaleNum (filter inRange minorGridXs)
mkGridLine x = pathFromVertices [f y0, f y1]
where f y = over lensP ((el e .~ x) . (el eO .~ y)) p
ticks = foldMap drawTicks ys
drawTicks (pos,y) = maTicks <> miTicks
where
maTicks
| a ^. majorTicks . hidden = mempty
| otherwise = foldMap (positionTick majorTick) majorTickXs'
# strokePathV
# applyStyle (a ^. majorTicksStyle)
miTicks
| a ^. minorTicks . hidden = mempty
| otherwise = foldMap (positionTick minorTick) minorTickXs'
# strokePathV
# applyStyle (a ^. minorTicksStyle)
minorTick = someTick (a ^. minorTicksAlignment) (a ^. minorTicksLength)
majorTick = someTick (a ^. majorTicksAlignment) (a ^. majorTicksLength)
someTick tType d = pathFromVertices $
case tType of
AutoTick ->
case pos of
LowerAxis -> [origin & ep eO -~ d, origin]
MiddleAxis -> [origin & ep eO -~ d, origin & ep eO +~ d]
UpperAxis -> [origin, origin & ep eO +~ d]
TickSpec (fromRational -> aa) (fromRational -> bb) ->
case pos of
UpperAxis -> [origin & ep eO -~ d*bb, origin & ep eO +~ d*aa]
_ -> [origin & ep eO -~ d*aa, origin & ep eO +~ d*bb]
positionTick tick x = place tick p'
where
p' = over lensP ((el e .~ x) . (el eO .~ y)) p
# transform t
line
| a ^. axisLine . hidden = mempty
| otherwise = foldMap mkline (map snd ys)
# transform t
# strokePathV
# lineCap LineCapSquare
# applyStyle (a^.axisLineStyle)
where
mkline y = pathFromVertices
$ map (\x -> over lensP ((el e .~ x) . (el eO .~ y)) p) [x0, x1] :: Path v Double
b@(x0,x1) = bs ^. el e :: (Double, Double)
coscale = ep e %~ coscaleNum
coscaleNum = scaleNum (bs ^. el e) (ls ^. el e)
yb@(y0,y1) = bs ^. el eO . if lp == UpperLabels
then swapped
else id
inRange x = x >= x0 && x <= x1
majorTickXs = sort $ view majorTicksFunction a b
majorTickXs' = map coscaleNum (filter inRange majorTickXs)
minorTickXs = sort $ view minorTicksFunction a majorTickXs b
minorTickXs' = map coscaleNum (filter inRange minorTickXs)
ys = getAxisLinePos yb lineType
lineType = a ^. axisLineType
txtAlign =
case lp of
LowerLabels -> BoxAlignedText 0.5 1
LeftLabels -> BoxAlignedText 1 0.5
RightLabels -> BoxAlignedText 0 0.5
UpperLabels -> BoxAlignedText 1 0
_ -> error "No labels"
negate' = if lp == UpperLabels || lp == RightLabels
then id
else negate
getAxisLinePos :: (Num n, Ord n) => (n, n) -> AxisLineType -> [(AxisPos, n)]
getAxisLinePos (a,b) aType = case aType of
BoxAxisLine -> [(LowerAxis, a), (UpperAxis, b)]
LeftAxisLine -> [(LowerAxis, a)]
MiddleAxisLine -> [(,) MiddleAxis $
if | a > 0 -> a
| b < 0 -> b
| otherwise -> 0]
RightAxisLine -> [(UpperAxis, b)]
NoAxisLine -> []
data AxisPos = LowerAxis | MiddleAxis | UpperAxis
ep :: E v -> Lens' (Point v x) x
ep (E l) = lensP . l
{-# INLINE ep #-}
instance RenderAxis Polar where
renderAxis = renderPolarAxis
boundingRadiusR :: (InSpace V2 n a, Enveloped a) => Int -> a -> (n, n)
boundingRadiusR (max 3 -> n) e =
case getEnvelope e of
EmptyEnvelope -> (0,0)
Envelope f ->
let thetas = map (@@rad) $ enumFromToN 0 tau n
vs = map angleDir thetas
lowerBound = F.foldr (\v r -> max (I.sup $ f v) r) 0 vs
upperBound = lowerBound / cos (pi / fromIntegral n)
in (lowerBound, upperBound)
renderPolarAxis :: Axis Polar -> Diagram V2
renderPolarAxis a = frame 15
$ leg
<> plots
<> theAxis
where
r = snd $ boundingRadiusR 30 styledPlots
spec = AxisSpec xs t (pure LinearAxis) (a ^. axisColourMap)
plots = F.foldMap (renderStyledPlot spec) styledPlots
dataBB = fromCorners (mkP2 (-r) (-r)) (mkP2 r r)
(xs, tv, t') = calculateScaling (view _Wrapped $ a^.axes.column axisScaling) dataBB
t = tv <> t'
theAxis = drawPolarAxis spec (a ^. axes)
bb = fromCorners (P . apply t $ fmap fst xs) (P . apply t $ fmap snd xs)
leg = drawLegend bb (styledPlotLegends styledPlots) (a ^. legend)
styledPlots = map (appEndo $ a ^. plotModifier)
$ zipWith styleDynamic (a ^.. axisStyles) (a ^. axisPlots)
drawPolarAxis :: AxisSpec V2 -> Polar (SingleAxis V2) -> Diagram V2
drawPolarAxis spec (Polar (V2 rA thetaA)) = fcA transparent $ rAx <> thetaAx where
r = spec ^. specBounds . _x . _2
t = spec ^. specTrans
s = avgScale t
rInRange x = x >= 0 && x <= r*1.000001
thetaInRange x = x >= 0 && x < tau
rAx
| rA ^. hidden = mempty
| otherwise = rAxLine <> rAxLabel <> rAxTicks <> rAxTickLabels <> rAxGridLines
rAxLine = line # whenever (rA ^. axisLine . hidden) phantom
where
line = fromVertices [origin, mkP2 r 0]
# applyStyle (rA^.axisLineStyle)
# transform t
rAxLabel
| null rTxt || rA ^. axisLabel . hidden = mempty
| otherwise = view axisLabelTextFunction rA rLabelAlign rTxt
# translate rLabelPos
# applyStyle (rA ^. axisLabelStyle)
# fc black
rLabelPos = V2 (s*x) (- view axisLabelGap rA) where
x = case rA ^. axisLabelPosition of
MiddleAxisLabel -> r/2
LowerAxisLabel -> 0
UpperAxisLabel -> r
rTxt = rA ^. axisLabelText
rLabelAlign = BaselineText
majorTickRs = view majorTicksFunction rA (0,r)
majorTickRs' = map (*s) $ filter rInRange majorTickRs
minorTickRs = view minorTicksFunction rA majorTickRs (0,r)
minorTickRs' = map (*s) $ filter rInRange minorTickRs
rAxTicks = rAxMajorTicks <> rAxMinorTicks
rAxMajorTicks
| rA ^. majorTicks . hidden = mempty
| otherwise = F.foldMap (\x -> rAxMajorTick # translateX x) majorTickRs'
# applyStyle (rA ^. majorTicksStyle)
rAxMinorTicks
| rA ^. minorTicks . hidden = mempty
| otherwise = F.foldMap (\x -> rAxMinorTick # translateX x) minorTickRs'
# applyStyle (rA ^. minorTicksStyle)
rAxMajorTick = someTick (rA ^. majorTicksAlignment) (rA ^. majorTicksLength)
rAxMinorTick = someTick (rA ^. minorTicksAlignment) (rA ^. minorTicksLength)
someTick tType d = case tType of
TickSpec (fromRational -> aa) (fromRational -> bb)
-> fromVertices [mkP2 0 (-d*bb), mkP2 0 (d*aa)]
AutoTick -> fromVertices [mkP2 0 (-d) , mkP2 0 d ]
rAxGridLines
| otherwise = rMajorGridLines <> rMinorGridLines
majorGridRs = view majorGridLinesFunction rA majorTickRs (0,r)
majorGridRs' = map (*s) $ filter rInRange majorGridRs
rMajorGridLines :: Diagram V2
rMajorGridLines
| rA ^. majorGridLines . hidden = mempty
| otherwise = F.foldMap circle (filter (>0) majorGridRs')
# applyStyle (rA ^. majorGridLinesStyle)
minorGridRs = view minorGridLinesFunction rA minorTickRs (0,r)
minorGridRs' = map (*s) $ filter rInRange minorGridRs
rMinorGridLines :: Diagram V2
rMinorGridLines
| rA ^. minorGridLines . hidden = mempty
| otherwise = F.foldMap circle (filter (>0) minorGridRs')
# applyStyle (rA ^. minorGridLinesStyle)
rAxTickLabels :: Diagram V2
rAxTickLabels
| rA ^. tickLabel . hidden = mempty
| otherwise = F.foldMap rDrawTickLabel tickLabelRs
tickLabelRs :: [(Double, String)]
tickLabelRs = view tickLabelFunction rA (filter rInRange majorTickRs) (0,r)
rDrawTickLabel :: (Double,String) -> Diagram V2
rDrawTickLabel (x,label) =
view tickLabelTextFunction rA (BoxAlignedText 0.5 1) label
# translate (V2 (s*x) (- view axisLabelGap rA))
# applyStyle (rA ^. tickLabelStyle)
# fc black
thetaAx
| thetaA ^. hidden = mempty
| otherwise = thetaAxLine <> thetaAxLabel
<> thetaAxTicks <> thetaAxTickLabels <> thetaAxGridLines
theta = 2*pi
thetaAxLine = line # whenever (thetaA ^. axisLine . hidden) phantom
where
line = circle (s*r) # applyStyle (thetaA^.axisLineStyle)
thetaAxLabel
| null thetaTxt || thetaA ^. axisLabel . hidden = mempty
| otherwise = view axisLabelTextFunction thetaA thetaLabelAlign thetaTxt
# translate thetaLabelPos
# applyStyle (thetaA ^. axisLabelStyle)
# fc black
thetaLabelPos = view xy_ (mkPolar (s*r + view axisLabelGap thetaA) x) where
x = case thetaA ^. axisLabelPosition of
MiddleAxisLabel -> quarterTurn
LowerAxisLabel -> zero
UpperAxisLabel -> halfTurn
thetaTxt = thetaA ^. axisLabelText
thetaLabelAlign = BaselineText
majorTickThetas = view majorTicksFunction thetaA (0,theta)
majorTickThetas' = filter thetaInRange majorTickThetas
minorTickThetas = view minorTicksFunction thetaA majorTickThetas (0,theta)
minorTickThetas' = filter thetaInRange minorTickThetas
thetaAxTicks = thetaAxMajorTicks <> thetaAxMinorTicks
thetaAxMajorTicks
| thetaA ^. majorTicks . hidden = mempty
| otherwise = F.foldMap (\phi -> thetaAxMajorTick # translateX (s*r) # rotate (phi@@rad)) majorTickThetas'
# applyStyle (thetaA ^. majorTicksStyle)
thetaAxMinorTicks
| thetaA ^. minorTicks . hidden = mempty
| otherwise = F.foldMap (\phi -> thetaAxMinorTick # translateX (s*r) # rotate (phi@@rad)) minorTickThetas'
# applyStyle (thetaA ^. minorTicksStyle)
thetaAxMajorTick = someThetaTick (thetaA ^. majorTicksAlignment) (thetaA ^. majorTicksLength)
thetaAxMinorTick = someThetaTick (thetaA ^. minorTicksAlignment) (thetaA ^. minorTicksLength)
someThetaTick tType d = case tType of
TickSpec (fromRational -> aa) (fromRational -> bb)
-> fromVertices [mkP2 (-d*bb) 0, mkP2 (d*aa) 0]
AutoTick -> fromVertices [mkP2 (-d) 0 , mkP2 d 0 ]
thetaAxGridLines
| otherwise = thetaMajorGridLines <> thetaMinorGridLines
majorGridThetas = view majorGridLinesFunction thetaA majorTickThetas (0,theta)
majorGridThetas' = filter thetaInRange majorGridThetas
thetaMajorGridLines :: Diagram V2
thetaMajorGridLines
| thetaA ^. majorGridLines . hidden = mempty
| otherwise = F.foldMap (\phi -> fromVertices [origin, mkP2 r 0] # rotate (phi@@rad)) majorGridThetas'
# transform t
# applyStyle (thetaA ^. majorGridLinesStyle)
minorGridThetas = view minorGridLinesFunction thetaA minorTickThetas (0,theta)
minorGridThetas' = filter thetaInRange minorGridThetas
thetaMinorGridLines :: Diagram V2
thetaMinorGridLines
| thetaA ^. minorGridLines . hidden = mempty
| otherwise = F.foldMap (\phi -> fromVertices [origin, mkP2 r 0] # rotate (phi@@rad)) minorGridThetas'
# transform t
# applyStyle (thetaA ^. minorGridLinesStyle)
thetaAxTickLabels :: Diagram V2
thetaAxTickLabels
| thetaA ^. tickLabel . hidden = mempty
| otherwise = F.foldMap thetaDrawTickLabel tickLabelThetas
tickLabelThetas :: [(Double, String)]
tickLabelThetas = view tickLabelFunction thetaA majorTickThetas' (0,theta)
thetaDrawTickLabel :: (Double, String) -> Diagram V2
thetaDrawTickLabel (x,label) =
view tickLabelTextFunction thetaA a label
# translate v
# applyStyle (thetaA ^. tickLabelStyle)
# fc black
where v = mkPolar (s*r + view axisLabelGap thetaA) (x@@rad) ^. xy_
a = BoxAlignedText 0.5 0.5