chart-unit-0.5.1: src/Chart/Core.hs
{-# OPTIONS_GHC -Wall #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE CPP #-}
#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:
--
-- > fileSvg "other/scaleExample.svg" (300,120) $
-- > withHud (hudAspect_ .~ widescreen $ hudRange_ .~ Just (Rect 0 12 0 0.2) $ def)
-- > (lineChart (repeat def)) ((\x -> [Pair x 0, Pair x (x/100)]) <$> [0..10])
--
-- 
--
module Chart.Core
( -- * Chart types
Chart
, UChart(..)
, combine
-- * Scaling
, range
, projectss
, Aspect(..)
, aspect
, asquare
, sixbyfour
, golden
, widescreen
, skinny
, AlignH(..)
, AlignV(..)
, alignHU
, alignHTU
, alignVU
-- * 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
-- * IO
, fileSvg
-- * Color
--
-- | chart-unit exposes the 'colour' and 'palette' libraries for color combinators
, ucolor
, ublue
, ugrey
-- * Compatability
, scaleX
, scaleY
, scale
) where
import Diagrams.Backend.SVG (SVG, renderSVG)
import Diagrams.Prelude
hiding (Color, D, aspect, project, scale, scaleX, scaleY, zero)
import qualified Diagrams.Prelude as Diagrams
import NumHask.Pair
import NumHask.Prelude
import NumHask.Rect
import NumHask.Space
-- | 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) =>
QDiagram b V2 Double Any
-- | a UChart provides a late binding of a chart Aspect so multiple charts can be rendered using the same range.
data UChart a b = UChart
{ uchartRenderer :: () =>
Aspect -> Rect Double -> a -> Chart b
, uchartRenderRange :: Rect Double
, uchartData :: a
}
-- | render a list of charts, taking into account each of their ranges
combine :: Aspect -> [UChart a b] -> Chart b
combine asp qcs = mconcat $ (\(UChart c _ x) -> c asp rall x) <$> qcs
where
rall = fold $ (\(UChart _ r1 _) -> r1) <$> qcs
-- | 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
-- | a wrapped Rect specifying the shape od the chart canvas.
--
-- The Aspect tends to be:
--
-- - independent of the data range
-- - expressed in terms around a width magnitude of one. chart default options are callibrated to this convention.
newtype Aspect = Aspect (Rect Double)
-- | the rendering aspect of a chart expressed as a ratio of x-plane : y-plane.
aspect :: Double -> Aspect
aspect a = Aspect $ Ranges ((a *) <$> one) one
-- | a 1:1 aspect
asquare :: Aspect
asquare = aspect 1
-- | a 1.5:1 aspect
sixbyfour :: Aspect
sixbyfour = aspect 1.5
-- | golden ratio
golden :: Aspect
golden = aspect 1.61803398875
-- | a 3:1 aspect
widescreen :: Aspect
widescreen = aspect 3
-- | a skinny 5:1 aspect
skinny :: Aspect
skinny = aspect 5
-- | horizontal alignment
data AlignH
= AlignLeft
| AlignCenter
| AlignRight
-- | vertical alignment
data AlignV
= AlignTop
| AlignMid
| AlignBottom
-- | 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
-- | 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)
-- | write an svg to file
fileSvg :: FilePath -> (Double, Double) -> Diagram SVG -> IO ()
fileSvg f s = renderSVG f (mkSizeSpec (Just <$> r2 s))
-- | convert an rgba spec to an AlphaColour
ucolor :: (Floating a, Ord a) => a -> a -> a -> a -> AlphaColour a
ucolor r g b o = withOpacity (sRGB r g b) o
-- | the official chart-unit blue
ublue :: AlphaColour Double
ublue = ucolor 0.365 0.647 0.855 0.5
-- | the official chart-unit grey
ugrey :: AlphaColour Double
ugrey = ucolor 0.4 0.4 0.4 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)