{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE NegativeLiterals #-} {-# OPTIONS_GHC -Wall #-} {-# OPTIONS_GHC -fno-warn-orphans #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE ViewPatterns #-} #if ( __GLASGOW_HASKELL__ < 820 ) {-# OPTIONS_GHC -fno-warn-incomplete-patterns #-} #endif -- | In making a chart, there are three main size domains you have to be concerned about: -- -- - the range of the data being charted. This range is often projected onto chart elements such as axes and labels. A data range in two dimensions is a 'Rect' a. -- -- - the scale of various chart primitives and elements. The overall dimensions of the chart canvas - the rectangular shape on which the data is represented - is referred to as an 'Aspect' in the api, and is a wrapped 'Rect' to distinguish aspects from rect ranges. The default chart options tend to be callibrated to Aspects around widths of one. -- -- - the size of the chart rendered as an image. Backends tend to shrink charts to fit the rectangle shape specified in the render function, and a loose sympathy is expected between the aspect and a chart's ultimate physical size. -- -- Jumping ahead a bit, the code snippet below draws vertical lines using a data range of "Rect 0 12 0 0.2" (slightly different to the actual data range), using a widescreen (3:1) aspect, and renders the chart as a 300 by 120 pixel svg: -- -- > scaleExample :: IO () -- > scaleExample = -- > fileSvg "other/scaleExample.svg" (#size .~ Pair 300 120 $ def) $ -- > withHud -- > def -- > widescreen -- > (Rect 0 12 0 0.2) -- > (lineChart (repeat def)) -- > (vlineOneD ((0.01*) <$> [0..10])) -- -- ![scale example](other/scaleExample.svg) -- module Chart.Core ( -- * Chart types Chart -- * Scaling , range , projectss , aspect , asquare , sixbyfour , golden , widescreen , skinny , AlignH(..) , AlignV(..) , alignHU , alignHTU , alignVU , alignVTU -- * Types , Orientation(..) , Place(..) -- * Combinators -- -- | The concept of a point on a chart is the polymorphic 'R2' from the 'linear' library. Diagrams most often uses 'Point', which is a wrapped 'V2'. The 'Pair' type from 'numhask-range' is often used as a point reference. , positioned , p_ , r_ , stack , vert , hori , sepVert , sepHori -- * Color -- -- | chart-unit exposes the 'colour' and 'palette' libraries for color combinators , UColor(..) , acolor , ucolor , ccolor , ublue , ugrey , utrans , ublack , uwhite -- * Compatability , scaleX , scaleY , scale ) where import Diagrams.Prelude hiding (Color, D, aspect, project, scale, scaleX, scaleY, zero, over) import qualified Diagrams.Prelude as Diagrams import qualified Diagrams.TwoD.Text import NumHask.Pair import NumHask.Prelude import NumHask.Rect import NumHask.Space import Data.Colour (over) -- | A Chart is simply a type synonym for a typical Diagrams object. A close relation to this type is 'Diagram' 'B', but this usage tends to force a single backend (B comes from the backend libraries), so making Chart b's maintains backend polymorphism. -- -- Just about everything - text, circles, lines, triangles, charts, axes, titles, legends etc - are 'Chart's, which means that most things are amenable to full use of the combinatorially-inclined diagrams-lib. type Chart b = ( Renderable (Path V2 Double) b , Renderable (Diagrams.TwoD.Text.Text Double) b) => QDiagram b V2 Double Any -- | project a double-containered set of data to a new Rect range projectss :: (Functor f, Functor g) => Rect Double -> Rect Double -> g (f (Pair Double)) -> g (f (Pair Double)) projectss r0 r1 xyss = map (project r0 r1) <$> xyss -- | determine the range of a double-containered set of data range :: (Foldable f, Foldable g) => g (f (Pair Double)) -> Rect Double range xyss = foldMap space xyss -- | the aspect of a chart expressed as a ratio of x-plane : y-plane. aspect :: (BoundedField a, Ord a, Multiplicative a, FromInteger a) => a -> Rect a aspect a = Ranges ((a *) <$> one) one -- | a 1:1 aspect asquare :: Rect Double asquare = aspect 1 -- | a 1.5:1 aspect sixbyfour :: Rect Double sixbyfour = aspect 1.5 -- | golden ratio golden :: Rect Double golden = aspect 1.61803398875 -- | a 3:1 aspect widescreen :: Rect Double widescreen = aspect 3 -- | a skinny 5:1 aspect skinny :: Rect Double skinny = aspect 5 -- | horizontal alignment data AlignH = AlignLeft | AlignCenter | AlignRight deriving (Eq, Show, Generic) -- | vertical alignment data AlignV = AlignTop | AlignMid | AlignBottom deriving (Eq, Show, Generic) -- | conversion of horizontal alignment to (one :: Range Double) limits alignHU :: AlignH -> Double alignHU a = case a of AlignLeft -> 0.5 AlignCenter -> 0 AlignRight -> -0.5 -- | svg text is forced to be lower left (-0.5) by default alignHTU :: AlignH -> Double alignHTU a = case a of AlignLeft -> 0 AlignCenter -> -0.5 AlignRight -> -1 -- | conversion of vertical alignment to (one :: Range Double) limits alignVU :: AlignV -> Double alignVU a = case a of AlignTop -> -0.5 AlignMid -> 0 AlignBottom -> 0.5 -- | svg text is lower by default alignVTU :: AlignV -> Double alignVTU a = case a of AlignTop -> 0.5 AlignMid -> 0 AlignBottom -> -0.5 -- | Orientation for an element. Watch this space for curvature! data Orientation = Hori | Vert deriving (Eq, Show, Generic) -- | Placement of elements around (what is implicity but maybe shouldn't just be) a rectangular canvas data Place = PlaceLeft | PlaceRight | PlaceTop | PlaceBottom deriving (Eq, Show, Generic) -- | position an element at a point positioned :: (R2 r) => r Double -> Chart b -> Chart b positioned p = moveTo (p_ p) -- | convert an R2 to a diagrams Point p_ :: (R2 r) => r Double -> Point V2 Double p_ r = curry p2 (r ^. _x) (r ^. _y) -- | convert an R2 to a V2 r_ :: R2 r => r a -> V2 a r_ r = V2 (r ^. _x) (r ^. _y) -- | foldMap for beside; stacking chart elements in a direction, with a premap stack :: ( R2 r , V a ~ V2 , Foldable t , Juxtaposable a , Semigroup a , N a ~ Double , Monoid a ) => r Double -> (b -> a) -> t b -> a stack dir f xs = foldr (\a x -> beside (r_ dir) (f a) x) mempty xs -- | combine elements vertically, with a premap vert :: (V a ~ V2, Foldable t, Juxtaposable a, Semigroup a, N a ~ Double, Monoid a) => (b -> a) -> t b -> a vert = stack (Pair 0 -1) -- | combine elements horizontally, with a premap hori :: (V a ~ V2, Foldable t, Juxtaposable a, Semigroup a, N a ~ Double, Monoid a) => (b -> a) -> t b -> a hori = stack (Pair 1 0) -- | horizontal separator sepHori :: Double -> Chart b -> Chart b sepHori s x = beside (r2 (0, -1)) x (strutX s) -- | vertical separator sepVert :: Double -> Chart b -> Chart b sepVert s x = beside (r2 (1, 0)) x (strutY s) data UColor a = UColor { ucred :: a , ucgreen :: a , ucblue :: a , ucopacity :: a } deriving (Eq, Ord, Show, Generic) -- | convert a UColor to an AlphaColour acolor :: (Floating a, Num a, Ord a) => UColor a -> AlphaColour a acolor (UColor r g b o) = withOpacity (sRGB r g b) o -- | convert an AlphaColour to a UColor ucolor :: (Floating a, Num a, Ord a) => AlphaColour a -> UColor a ucolor a = let (RGB r g b) = toSRGB (a `over` black) in UColor r g b (alphaChannel a) -- | convert a Colour to a UColor ccolor :: (Floating a, Num a, Ord a) => Colour a -> UColor a ccolor (toSRGB -> RGB r g b) = UColor r g b 1 -- | the official chart-unit blue ublue :: UColor Double ublue = UColor 0.365 0.647 0.855 0.5 -- | the official chart-unit grey ugrey :: UColor Double ugrey = UColor 0.4 0.4 0.4 1 -- | transparent utrans :: UColor Double utrans = UColor 0 0 0 0 -- | black ublack :: UColor Double ublack = UColor 0 0 0 1 -- | white uwhite :: UColor Double uwhite = UColor 1 1 1 1 -- | These are difficult to avoid instance R1 Pair where _x f (Pair a b) = (`Pair` b) <$> f a instance R2 Pair where _y f (Pair a b) = Pair a <$> f b _xy f p = fmap (\(V2 a b) -> Pair a b) . f . (\(Pair a b) -> V2 a b) $ p eps :: N [Point V2 Double] eps = 1e-8 -- | the diagrams scaleX with a zero divide guard to avoid error throws scaleX :: (N t ~ Double, Transformable t, R2 (V t), Diagrams.Additive (V t)) => Double -> t -> t scaleX s = Diagrams.scaleX (if s == zero then eps else s) -- | the diagrams scaleY with a zero divide guard to avoid error throws scaleY :: (N t ~ Double, Transformable t, R2 (V t), Diagrams.Additive (V t)) => Double -> t -> t scaleY s = Diagrams.scaleY (if s == zero then eps else s) -- | the diagrams scale with a zero divide guard to avoid error throws scale :: (N t ~ Double, Transformable t, R2 (V t), Diagrams.Additive (V t)) => Double -> t -> t scale s = Diagrams.scale (if s == zero then eps else s)