{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ViewPatterns #-} -- Orphan Mainable Axis instance. {-# OPTIONS_GHC -fno-warn-orphans #-} ----------------------------------------------------------------------------- -- | -- Module : Plots.Axis.Render -- Copyright : (C) 2016 Christopher Chalmers -- License : BSD-style (see the file LICENSE) -- Maintainer : Christopher Chalmers -- Stability : experimental -- Portability : non-portable -- -- Low level module containing functions for rendering different types -- of axis. -- ---------------------------------------------------------------------------- module Plots.Axis.Render ( -- * Rendering axes RenderAxis (..) , r2AxisMain -- * Low level , buildPlots )where import Data.Bool import Data.Foldable as F import Data.List (sort) import Data.Typeable import Diagrams.BoundingBox import Diagrams.Prelude 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 ------------------------------------------------------------------------ -- Mainable instances ------------------------------------------------------------------------ instance (TypeableFloat n, Renderable (Path V2 n) b, Mainable (QDiagram b V2 n Any)) => Mainable (Axis b Polar n) where type MainOpts (Axis b Polar n) = MainOpts (QDiagram b V2 n Any) mainRender opts = mainRender opts . renderAxis instance (TypeableFloat n, Renderable (Path V2 n) b, Mainable (QDiagram b V2 n Any)) => Mainable (Axis b V2 n) where type MainOpts (Axis b V2 n) = MainOpts (QDiagram b V2 n Any) mainRender opts = mainRender opts . renderAxis instance ToResult (Axis b v n) where type Args (Axis b v n) = () type ResultOf (Axis b v n) = Axis b v n toResult d _ = d -- | 'mainWith' specialised to a 2D Axis. r2AxisMain :: (Parseable (MainOpts (QDiagram b V2 Double Any)), Mainable (Axis b V2 Double)) => Axis b V2 Double -> IO () r2AxisMain = mainWith ------------------------------------------------------------------------ -- Low level functions ------------------------------------------------------------------------ -- | Build a list of styled plots from the axis, ready to be rendered. -- This takes into account any 'AxisStyle' changes and applies the -- 'finalPlots' modifications. -- -- The 'StyledPlots' can be rendered with 'renderStyledPlot' and the -- legend entries can be obtained with 'styledPlotLegends'. This is -- what 'renderAxis' can uses internally but might be useful for -- debugging or generating your own legend. buildPlots :: BaseSpace c ~ v => Axis b c n -> [StyledPlot b v n] buildPlots a = map (appEndo $ a ^. plotModifier) $ zipWith styleDynamic (a ^.. axisStyles) (a ^. axisPlots) -- TODO: correct order ------------------------------------------------------------------------ -- Render axis ------------------------------------------------------------------------ -- | Renderable axes. class RenderAxis b v n where -- | Render an axis to a diagram. The size of the diagram is -- determined by the 'axisSize'. renderAxis :: Axis b v n -> QDiagram b (BaseSpace v) n Any -- | The 'RenderAxis' class provides a default way to render an axis for -- each space. instance (TypeableFloat n, Renderable (Path V2 n) b) => RenderAxis b V2 n where -- | Render an axis and its plots, as well as the legend and colour -- bar. renderAxis = renderR2Axis renderR2Axis :: (TypeableFloat n, Renderable (Path V2 n) b) => Axis b V2 n -> QDiagram b V2 n Any 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) -- -- The colour bar cBar = addColourBar bb (a^.colourBar) (a ^. axisColourMap) (a^.colourBarRange) -- title ttl = drawTitle bb (a^.title) -- styledPlots = buildPlots a -- | The position of axis labels for a data LabelPosition = NoLabels | LowerLabels | LeftLabels | RightLabels | UpperLabels deriving (Show, Eq, Typeable) axisOnBasis :: forall b v n. (v ~ V2, TypeableFloat n, HasLinearMap v, Metric v, Renderable (Path V2 n) b, n ~ N (v n), v ~ V (v n), OrderedField n) => Point v n -- start of axis -> v (n, n) -- calculated bounds -> SingleAxis b v n -- axis data -> v LogScale -- log scale -> T2 n -- transformation to apply to positions of things -> E v -- direction of axis -> E v -- orthogonal direction of axis -> LabelPosition -- where (if at all) should labels be placed? -> QDiagram b V2 n Any -- resulting axis axisOnBasis p bs a ls t e eO lp | a ^. hidden = phantom axis | otherwise = axis where axis = tickLabels <> axLabels <> ticks <> line <> grid tStroke = stroke . transform t -- axis labels (x,y etc.) 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 -- & logPoint ls & 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 -- axLabelD = a ^. axisLabels . el e -- tick labels tickLabels | lp == NoLabels || a ^. tickLabel . hidden = mempty | otherwise = foldMap drawLabels (map snd $ take 1 ys) # applyStyle (a ^. tickLabelStyle) where -- tickLabelsD = a ^. axisTickLabels . el e 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 -- & logPoint ls & coscale & papply t & ep eO +~ negate' (a ^. tickLabelGap) -- the grid 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 -- -- gridD = a ^. axisGridLines ^. el e -- :: GridLines N -- the ticks ticks = foldMap drawTicks ys drawTicks (pos,y) = maTicks <> miTicks where maTicks | a ^. majorTicks . hidden = mempty | otherwise = foldMap (positionTick majorTick) majorTickXs' # stroke # applyStyle (a ^. majorTicksStyle) -- miTicks | a ^. minorTicks . hidden = mempty | otherwise = foldMap (positionTick minorTick) minorTickXs' # stroke # 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] -- NoTick -> [] -- middleTick d = -- pathFromVertices positionTick tick x = place tick p' where p' = over lensP ((el e .~ x) . (el eO .~ y)) p # transform t -- axis lines line | a ^. axisLine . hidden = mempty | otherwise = foldMap mkline (map snd ys) -- merge with ticks? # transform t # stroke # lineCap LineCapSquare # applyStyle (a^.axisLineStyle) where -- TODO: Arrow for R3 mkline y = pathFromVertices $ map (\x -> over lensP ((el e .~ x) . (el eO .~ y)) p) [x0, x1] :: Path v n -- measurements b@(x0,x1) = bs ^. el e :: (n, n) -- bounds 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" -- XXX Temporary -- t2 = scaling 4 -- negate' = if lp == UpperLabels || lp == RightLabels then id else negate -- utilities 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 ------------------------------------------------------------------------ -- Utilities ------------------------------------------------------------------------ ep :: E v -> Lens' (Point v x) x ep (E l) = lensP . l {-# INLINE ep #-} ------------------------------------------------------------------------ -- Polar ------------------------------------------------------------------------ instance (TypeableFloat n, Renderable (Path V2 n) b) => RenderAxis b Polar n where renderAxis = renderPolarAxis -- | An lower and upper bound for the bounding radius using @n@ envelope -- calculations. The more calculations used, the smaller the range of -- the bound. boundingRadiusR :: (InSpace V2 n a, Enveloped a) => Int -> a -> (n, n) boundingRadiusR (max 3 -> n) e = case appEnvelope (getEnvelope e) of Nothing -> (0,0) Just f -> let thetas = map (@@rad) $ enumFromToN 0 tau n vs = map angleV thetas -- The lower bound is the maximum distance obtained from the -- envelope trials. We know the radius will be at least this far. lowerBound = F.foldr (\v r -> max (f v) r) 0 vs -- In the worst case, there will be a point at the intersection of -- two neighbouring bounding planes from the envelope calculations. -- We can calculate the distance to this intersecion using simple -- trigonometry. -- (Note, this is why we need at least three envelope calculations, -- otherwise there wouldn't be any intersection between the bounding -- planes) upperBound = lowerBound / cos (pi / fromIntegral n) in (lowerBound, upperBound) renderPolarAxis :: (TypeableFloat n, Renderable (Path V2 n) b) => Axis b Polar n -> QDiagram b V2 n Any renderPolarAxis a = frame 15 $ leg -- <> colourBar -- <> circles <> 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 :: forall b n. (Renderable (Path V2 n) b, TypeableFloat n) => AxisSpec V2 n -> Polar (SingleAxis b V2 n) -> QDiagram b V2 n Any drawPolarAxis spec (Polar (V2 rA thetaA)) = fcA transparent $ rAx <> thetaAx where -- use a radius of the upper x bound for the axis (this is not ideal) 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 ---------------------------------------------------------------------- -- Radial axis ---------------------------------------------------------------------- -- The radial axis consists of an axis line from the centre to the -- edge. The ticks and tickLabels are along this line. The grid lines -- are made up of circles that pass through the line, centered at the -- center of the plot. rAx | rA ^. hidden = mempty | otherwise = rAxLine <> rAxLabel <> rAxTicks <> rAxTickLabels <> rAxGridLines rAxLine = line # whenever (rA ^. axisLine . hidden) phantom where -- XXX for now the radial axis is on the theta=0 line. Need some -- way to change this line = (origin ~~ mkP2 r 0) # applyStyle (rA^.axisLineStyle) # transform t -- Radial axis label ------------------------------------------------- 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 -- Radial ticks ------------------------------------------------------ -- The positions of major and minor ticks along the radial axis 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 -- Major and minor ticks are placed along the line at the calculated -- positions majorTickRs 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) -- The paths used for individual major and minor ticks rAxMajorTick = someTick (rA ^. majorTicksAlignment) (rA ^. majorTicksLength) rAxMinorTick = someTick (rA ^. minorTicksAlignment) (rA ^. minorTicksLength) someTick tType d = case tType of TickSpec (fromRational -> aa) (fromRational -> bb) -> mkP2 0 (-d*bb) ~~ mkP2 0 (d*aa) AutoTick -> mkP2 0 (-d) ~~ mkP2 0 d -- Radial grid lines ------------------------------------------------- rAxGridLines -- - | rA ^. gridLines . hidden = mempty | otherwise = rMajorGridLines <> rMinorGridLines majorGridRs = view majorGridLinesFunction rA majorTickRs (0,r) majorGridRs' = map (*s) $ filter rInRange majorGridRs rMajorGridLines :: QDiagram b V2 n Any 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 :: QDiagram b V2 n Any rMinorGridLines | rA ^. minorGridLines . hidden = mempty | otherwise = F.foldMap circle (filter (>0) minorGridRs') # applyStyle (rA ^. minorGridLinesStyle) -- Radial tick labels ------------------------------------------------ rAxTickLabels :: QDiagram b V2 n Any rAxTickLabels | rA ^. tickLabel . hidden = mempty | otherwise = F.foldMap rDrawTickLabel tickLabelRs -- The positions of the tick labels. tickLabelRs :: [(n, String)] tickLabelRs = view tickLabelFunction rA (filter rInRange majorTickRs) (0,r) -- Draw a single tick label given the position and the string to use rDrawTickLabel :: (n,String) -> QDiagram b V2 n Any rDrawTickLabel (x,label) = view tickLabelTextFunction rA (BoxAlignedText 0.5 1) label # translate (V2 (s*x) (- view axisLabelGap rA)) # applyStyle (rA ^. tickLabelStyle) # fc black ---------------------------------------------------------------------- -- Angular axis ---------------------------------------------------------------------- -- The angular axis is a circular line around the perimeter of the -- polar plot. The Ticks and tick labels are placed around this -- perimeter. The Grid lines go from the perimeter to the center. thetaAx | thetaA ^. hidden = mempty | otherwise = thetaAxLine <> thetaAxLabel <> thetaAxTicks <> thetaAxTickLabels <> thetaAxGridLines theta = 2*pi thetaAxLine = line # whenever (thetaA ^. axisLine . hidden) phantom where -- XXX for now the radial axis is on the theta=0 line. Need some -- way to change this line = circle (s*r) # applyStyle (thetaA^.axisLineStyle) -- Angular axis label ------------------------------------------------ -- Where should the label go? thetaAxLabel | null thetaTxt || thetaA ^. axisLabel . hidden = mempty | otherwise = view axisLabelTextFunction thetaA thetaLabelAlign thetaTxt # translate thetaLabelPos # applyStyle (thetaA ^. axisLabelStyle) # fc black -- thetaLabelPos = V2 (s*x) (- view axisLabelGap thetaA) where thetaLabelPos = view xy_ (mkPolar (s*r + view axisLabelGap thetaA) x) where -- The angle on the axis the label is placed, doesn't make much -- sense right now. x = case thetaA ^. axisLabelPosition of MiddleAxisLabel -> quarterTurn LowerAxisLabel -> zero UpperAxisLabel -> halfTurn thetaTxt = thetaA ^. axisLabelText thetaLabelAlign = BaselineText -- Angular axis ticks ------------------------------------------------ -- The positions of major and minor ticks along the angular axis majorTickThetas = view majorTicksFunction thetaA (0,theta) majorTickThetas' = filter thetaInRange majorTickThetas minorTickThetas = view minorTicksFunction thetaA majorTickThetas (0,theta) minorTickThetas' = filter thetaInRange minorTickThetas -- Major and minor ticks are placed along perimeter, facing the center -- of the axis. Ticks start of horizonal and are rotated to the -- correct position on the axis. 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) -- The paths used for individual major and minor ticks thetaAxMajorTick = someThetaTick (thetaA ^. majorTicksAlignment) (thetaA ^. majorTicksLength) thetaAxMinorTick = someThetaTick (thetaA ^. minorTicksAlignment) (thetaA ^. minorTicksLength) someThetaTick tType d = case tType of TickSpec (fromRational -> aa) (fromRational -> bb) -> mkP2 (-d*bb) 0 ~~ mkP2 (d*aa) 0 AutoTick -> mkP2 (-d) 0 ~~ mkP2 d 0 -- Angular grid lines ------------------------------------------------ -- grid lines go from the centre of the axis to the perimeter thetaAxGridLines -- - | thetaA ^. gridLines . hidden = mempty | otherwise = thetaMajorGridLines <> thetaMinorGridLines majorGridThetas = view majorGridLinesFunction thetaA majorTickThetas (0,theta) majorGridThetas' = filter thetaInRange majorGridThetas thetaMajorGridLines :: QDiagram b V2 n Any thetaMajorGridLines | thetaA ^. majorGridLines . hidden = mempty | otherwise = F.foldMap (\phi -> 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 :: QDiagram b V2 n Any thetaMinorGridLines | thetaA ^. minorGridLines . hidden = mempty | otherwise = F.foldMap (\phi -> origin ~~ mkP2 r 0 # rotate (phi@@rad)) minorGridThetas' # transform t # applyStyle (thetaA ^. minorGridLinesStyle) -- Angular tick labels ----------------------------------------------- thetaAxTickLabels :: QDiagram b V2 n Any thetaAxTickLabels | thetaA ^. tickLabel . hidden = mempty | otherwise = F.foldMap thetaDrawTickLabel tickLabelThetas -- The positions of the tick labels. tickLabelThetas :: [(n, String)] tickLabelThetas = view tickLabelFunction thetaA majorTickThetas' (0,theta) -- Draw a single tick label given the position and the string to use thetaDrawTickLabel :: (n, String) -> QDiagram b V2 n Any 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-cos x/2) (0.5-sin x/2) a = BoxAlignedText 0.5 0.5 -- line -- | a ^. axisLine . hidden = mempty -- | otherwise = F.foldMap mkline (map snd ys) -- merge with ticks? -- # transform t -- # stroke -- -- # applyStyle (a ^. axisLine e . axisArrowOpts . _Just . shaftStyle) -- # lineCap LineCapSquare -- where -- -- TODO: Arrow for R3 -- mkline y = pathFromVertices -- $ map (\x -> over lensP ((el e .~ x) . (el eO .~ y)) p) [x0, x1] :: Path v n