diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,16 @@
+0.7.0
+=====
+
+* GHC 8.10.2 support
+* Added `Transform` and `Affinity` capabilities
+* Added `Line` and associated functions.
+* Support for random-1.2; `uniformS` and `uniformSs`
+* Shifted to zero-centered, width scaling `Range` multiplication.
+* Added `average` & `quantile` to NumHask.Space.Histogram
+* Improved haddocks
+* Extended doctests
+
+0.6.1
+=====
+
+* GHC 8.10.1 support
diff --git a/numhask-space.cabal b/numhask-space.cabal
--- a/numhask-space.cabal
+++ b/numhask-space.cabal
@@ -1,10 +1,19 @@
 cabal-version: 2.4
 name: numhask-space
-version: 0.6.1
+version: 0.7.0.0
 synopsis:
-  numerical spaces
+  Numerical spaces.
 description:
-  Spaces and the numerical elements that inhabit them.
+    @numhask-space@ provides support for spaces where [space](https://en.wikipedia.org/wiki/Space_(mathematics\)) is defined as a set of numbers with a lower and upper bound.
+    .
+    == Usage
+    .
+    >>> {-# LANGUAGE NegativeLiterals #-}
+    >>> {-# LANGUAGE RebindableSyntax #-}
+    >>> import NumHask.Prelude
+    >>> import NumHask.Space
+    .
+
 category:
   mathematics
 homepage:
@@ -23,6 +32,10 @@
   LICENSE
 build-type:
   Simple
+tested-with: GHC ==8.8.4 || ==8.10.2
+extra-source-files:
+  readme.md
+  ChangeLog.md
 source-repository head
   type:
     git
@@ -39,16 +52,12 @@
     -Wincomplete-uni-patterns
     -Wredundant-constraints
   default-extensions:
-    NegativeLiterals
-    NoImplicitPrelude
-    OverloadedStrings
-    UnicodeSyntax
   build-depends:
     adjunctions >=4.0 && <5,
     base >=4.7 && <5,
     containers >= 0.6 && < 0.7,
     distributive >=0.2.2 && <1,
-    numhask >= 0.6 && < 0.7,
+    numhask >= 0.7 && < 0.8,
     semigroupoids >=5 && <6,
     tdigest >= 0.2.1 && < 0.3,
     text >= 1.2.3.1 && <2,
@@ -73,7 +82,7 @@
   build-depends:
     base >=4.7 && <5,
     doctest >= 0.16 && < 0.18,
-    numhask >= 0.6 && < 0.7,
+    numhask >= 0.7 && < 0.8,
   default-language: Haskell2010
   ghc-options:
     -Wall
@@ -82,7 +91,3 @@
     -Wincomplete-uni-patterns
     -Wredundant-constraints
   default-extensions:
-    NegativeLiterals
-    NoImplicitPrelude
-    OverloadedStrings
-    UnicodeSyntax
diff --git a/readme.md b/readme.md
new file mode 100644
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,23 @@
+numhask-space
+===
+
+[![Build Status](https://travis-ci.org/tonyday567/numhask-space.svg)](https://travis-ci.org/tonyday567/numhask-space) [![Hackage](https://img.shields.io/hackage/v/numhask-space.svg)](https://hackage.haskell.org/package/numhask-space)
+
+`numhask-space` provides support for spaces where [space](https://en.wikipedia.org/wiki/Space_(mathematics)) is defined as a set of numbers with a `lower `and `upper` bound.
+
+Usage
+===
+
+``` haskell
+{-# LANGUAGE NegativeLiterals #-}
+{-# LANGUAGE RebindableSyntax #-}
+import NumHask.Prelude
+import NumHask.Space
+```
+
+Develop
+===
+
+```
+stack build --test --haddock --file-watch
+```
diff --git a/src/NumHask/Space.hs b/src/NumHask/Space.hs
--- a/src/NumHask/Space.hs
+++ b/src/NumHask/Space.hs
@@ -1,17 +1,19 @@
+{-# LANGUAGE NegativeLiterals #-}
+{-# LANGUAGE RebindableSyntax #-}
 {-# OPTIONS_GHC -Wall #-}
+{-# OPTIONS_GHC -fno-warn-type-defaults #-}
 
--- | A continuous set of numbers.
---
--- Mathematics does not define a space, leaving library devs to experiment.
---
--- https://en.wikipedia.org/wiki/Space_(mathematics)
+-- | Mathematics does not rigorously define a [space](https://en.wikipedia.org/wiki/Space_(mathematics\)), leaving library devs free to push boundaries on what a space is.
+-- 
 module NumHask.Space
-  ( -- * Space
+  ( -- * Usage
+    -- $setup
+
+    -- * Space
     -- $space
     module NumHask.Space.Types,
 
     -- * Instances
-    -- $instances
     module NumHask.Space.Point,
     module NumHask.Space.Range,
     module NumHask.Space.Rect,
@@ -29,19 +31,40 @@
 import NumHask.Space.Types hiding ()
 import NumHask.Space.XY hiding ()
 
--- $space
--- The final frontier.
-
-{- $instances
-
- Space is an interesting cross-section of many programming domains.
-
- - A Range is a Space of numbers.
-
- - A Rect is a Space of Points.
-
- - A time span is a space containing moments of time.
+-- $setup
+--
+-- >>> :set -XRebindableSyntax
+-- >>> :set -XNegativeLiterals
+-- >>> import NumHask.Prelude
+-- >>> import NumHask.Space
+-- >>> Point 1 1
+-- Point 1 1
+--
+-- >>> one :: Range Double
+-- Range -0.5 0.5
+--
+-- >>> grid OuterPos (Range 0 50 :: Range Double) 5
+-- [0.0,10.0,20.0,30.0,40.0,50.0]
 
- - A histogram is a divided Range with a count of elements within each division.
+-- $space
+--
+-- Space is an interesting cross-section of many programming domains.
+--
+-- - A 'Range' is a 'Space' of numbers.
+--
+-- - A 'Rect' is a 'Space' of 'Point's. It can also be a 'Space' of 'Rect's (but this is not yet coded up here).
+--
+-- - A time span is a 'Space' containing moments of time.
+--
+-- - A 'Histogram' is a divided 'Range' with a count of elements within each division.
 
--}
+-- $extensions
+-- > :t Point 1.0 -1.0
+-- Point 1.0 -1.0
+--   :: (Subtractive a, FromRatio a Integer,
+--       FromRatio (a -> Point a) Integer) =>
+--      a -> Point a
+--
+-- > :set -XNegativeLiterals
+-- > :t Point 1.0 -1.0
+-- Point 1.0 -1.0 :: FromRatio a Integer => Point a
diff --git a/src/NumHask/Space/Histogram.hs b/src/NumHask/Space/Histogram.hs
--- a/src/NumHask/Space/Histogram.hs
+++ b/src/NumHask/Space/Histogram.hs
@@ -1,8 +1,8 @@
 {-# LANGUAGE DataKinds #-}
-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE RebindableSyntax #-}
 {-# OPTIONS_GHC -Wall #-}
 
--- | A histogram, if you squint, is a series of contiguous ranges, annotated with values.
+-- | A histogram, if you squint, is a series of contiguous 'Range's, annotated with values.
 module NumHask.Space.Histogram
   ( Histogram (..),
     DealOvers (..),
@@ -14,25 +14,27 @@
     quantileFold,
     fromQuantiles,
     freq,
+    average,
+    quantiles,
+    quantile,
   )
 where
 
 import qualified Data.List as List
 import qualified Data.Map as Map
-import Data.TDigest
+import qualified Data.TDigest as TD
+import NumHask.Prelude
 import NumHask.Space.Range
 import NumHask.Space.Rect
 import NumHask.Space.Types
-import NumHask.Prelude
 
 -- | This Histogram is a list of contiguous boundaries (a boundary being the lower edge of one bucket and the upper edge of another), and a value (usually a count) for each bucket, represented here as a map
 --
 -- Overs and Unders are contained in key = 0 and key = length cuts
-data Histogram
-  = Histogram
-      { cuts :: [Double], -- bucket boundaries
-        values :: Map.Map Int Double -- bucket counts
-      }
+data Histogram = Histogram
+  { cuts :: [Double], -- bucket boundaries
+    values :: Map.Map Int Double -- bucket counts
+  }
   deriving (Show, Eq)
 
 -- | Whether or not to ignore unders and overs.  If overs and unders are dealt with, IncludeOvers supplies an assumed width for the outer buckets.
@@ -102,9 +104,9 @@
 quantileFold :: [Double] -> [Double] -> [Double]
 quantileFold qs xs = done $ foldl' step begin xs
   where
-    step x a = Data.TDigest.insert a x
-    begin = tdigest ([] :: [Double]) :: TDigest 25
-    done x = fromMaybe (0 / 0) . (`quantile` compress x) <$> qs
+    step x a = TD.insert a x
+    begin = TD.tdigest ([] :: [Double]) :: TD.TDigest 25
+    done x = fromMaybe (0 / 0) . (`TD.quantile` TD.compress x) <$> qs
 
 -- | take a specification of quantiles and make a Histogram
 --
@@ -118,9 +120,39 @@
     diffq (x : xs') = (reverse . snd) $ foldl' step (x, []) xs'
     step (a0, xs') a = (a, (a - a0) : xs')
 
--- | normalize a histogram so that sum values = one
+-- | normalize a histogram
 --
+-- > \h -> sum (values $ freq h) == one
+--
 -- >>> freq $ fill [0,50,100] [1..100]
 -- Histogram {cuts = [0.0,50.0,100.0], values = fromList [(1,0.5),(2,0.5)]}
 freq :: Histogram -> Histogram
 freq (Histogram cs vs) = Histogram cs $ Map.map (* recip (sum vs)) vs
+
+-- | average
+--
+-- >>> average [0..1000]
+-- 500.0
+average :: (Foldable f) => f Double -> Double
+average xs = sum xs / fromIntegral (length xs)
+
+-- | Regularly spaced (approx) quantiles
+--
+-- >>> quantiles 5 [1..1000]
+-- [1.0,200.5,400.5,600.5000000000001,800.5,1000.0]
+--
+quantiles :: (Foldable f) => Int -> f Double -> [Double]
+quantiles n xs =
+  ( \x ->
+      fromMaybe 0 $
+        TD.quantile x (TD.tdigest xs :: TD.TDigest 25)
+  )
+    <$> ((/ fromIntegral n) . fromIntegral <$> [0 .. n])
+
+-- | single (approx) quantile
+--
+-- >>> quantile 0.1 [1..1000]
+-- 100.5
+--
+quantile :: (Foldable f) => Double -> f Double -> Double
+quantile p xs = fromMaybe 0 $ TD.quantile p (TD.tdigest xs :: TD.TDigest 25)
diff --git a/src/NumHask/Space/Point.hs b/src/NumHask/Space/Point.hs
--- a/src/NumHask/Space/Point.hs
+++ b/src/NumHask/Space/Point.hs
@@ -1,6 +1,8 @@
-{-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE DeriveTraversable #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE RebindableSyntax #-}
 {-# LANGUAGE TypeFamilies #-}
 {-# OPTIONS_GHC -Wall #-}
@@ -8,8 +10,20 @@
 -- | A 2-dimensional point.
 module NumHask.Space.Point
   ( Point (..),
-    rotate,
+    rotateP,
     gridP,
+    dotP,
+    (<.>),
+    crossP,
+    flipY,
+    Line (..),
+    lineSolve,
+    lineDistance,
+    closestPoint,
+    lineIntersect,
+    translate,
+    scaleT,
+    skew,
   )
 where
 
@@ -17,17 +31,17 @@
 import Data.Functor.Classes
 import Data.Functor.Rep
 import GHC.Show (show)
+import NumHask.Prelude hiding (Distributive, rotate, show)
+import qualified NumHask.Prelude as P
 import NumHask.Space.Range
 import NumHask.Space.Types
-import NumHask.Prelude hiding (show, Distributive, rotate)
-import qualified NumHask.Prelude as P
 
 -- $setup
 -- >>> :set -XNoImplicitPrelude
 
--- | A 2-dim point of a's
+-- | A 2-dimensional Point of a's
 --
--- A Point is functorial over both arguments, and is a Num instance.
+-- In contrast with a tuple, a Point is functorial over both arguments.
 --
 -- >>> let p = Point 1 1
 -- >>> p + p
@@ -35,14 +49,21 @@
 -- >>> (2*) <$> p
 -- Point 2 2
 --
--- A major reason for this bespoke treatment of a point is that Points do not have maximums and minimums but they form a lattice, and this is useful for folding points to find out the (rectangular) Space they occupy.
+-- A major reason for this bespoke treatment (compared to just using linear, say) is that Points do not have maximums and minimums but they do form a lattice, and this is useful for folding sets of points to find out the (rectangular) Space they occupy.
 --
 -- >>> Point 0 1 /\ Point 1 0
 -- Point 0 0
 -- >>> Point 0 1 \/ Point 1 0
 -- Point 1 1
-data Point a
-  = Point a a
+--
+-- This is used extensively in [chart-svg](https://hackage.haskell.org/package/chart-svg) to ergonomically obtain chart areas.
+--
+-- > space1 [Point 1 0, Point 0 1] :: Rect Double
+-- Rect 0.0 1.0 0.0 1.0
+data Point a = Point
+  { _x :: a,
+    _y :: a
+  }
   deriving (Eq, Generic)
 
 instance (Show a) => Show (Point a) where
@@ -115,6 +136,22 @@
       getL (Point l _) = l
       getR (Point _ r) = r
 
+instance (Additive a) => AdditiveAction (Point a) a where
+  (.+) a (Point x y) = Point (a + x) (a + y)
+  (+.) (Point x y) a = Point (a + x) (a + y)
+
+instance (Subtractive a) => SubtractiveAction (Point a) a where
+  (.-) a (Point x y) = Point (a - x) (a - y)
+  (-.) (Point x y) a = Point (x - a) (y - a)
+
+instance (Multiplicative a) => MultiplicativeAction (Point a) a where
+  (.*) a (Point x y) = Point (a * x) (a * y)
+  (*.) (Point x y) a = Point (a * x) (a * y)
+
+instance (Divisive a) => DivisiveAction (Point a) a where
+  (./) a (Point x y) = Point (a / x) (a / y)
+  (/.) (Point x y) a = Point (x / a) (y / a)
+
 instance Representable Point where
   type Rep Point = Bool
 
@@ -129,18 +166,124 @@
 instance (Ord a) => MeetSemiLattice (Point a) where
   (/\) (Point x y) (Point x' y') = Point (min x x') (min y y')
 
--- | rotate a point by x degrees relative to the origin
---
--- >>> rotate 90 (Point 0 1)
--- Point 1.0 6.123233995736766e-17
-rotate :: (FromInteger a, TrigField a) => a -> Point a -> Point a
-rotate d (Point x y) = Point (x * cos d' + y * sin d') (y * cos d' - x * sin d')
+instance
+  (ExpField a) =>
+  Norm (Point a) a
   where
-    d' = d * pi / 180
+  norm (Point x y) = sqrt (x * x + y * y)
+  basis p = p /. norm p
 
+-- | angle formed by a vector from trhe origin to a Point and the x-axis (Point 1 0). Note that an angle between two points p1 & p2 is thus angle p2 - angle p1
+--
+-- > \u@(Point ux uy) v@(Point vx vy) -> angle v - angle u == sign (ux*vy-uy*vx) * acos (dotP u v / (norm u * norm v))
+instance (TrigField a) => Direction (Point a) a where
+  angle (Point x y) = atan2 y x
+  ray x = Point (cos x) (sin x)
+
+instance (UniformRange a) => UniformRange (Point a) where
+  uniformRM (Point x y, Point x' y') g =
+    Point <$> uniformRM (x, x') g <*> uniformRM (y, y') g
+
+instance (Multiplicative a, Additive a) => Affinity (Point a) a where
+  transform (Transform a b c d e f) (Point x y) =
+    Point (a * x + b * y + c) (d * x + e * y + f)
+
+-- | move an 'Affinity' by a 'Point'
+translate :: (TrigField a) => Point a -> Transform a
+translate (Point x y) = Transform one zero x zero one y
+
+-- | scale an 'Affinity' by a 'Point'
+scaleT :: (TrigField a) => Point a -> Transform a
+scaleT (Point x y) = Transform x zero zero y zero zero
+
+-- | Skew transform
+--
+-- x-axis skew
+--
+-- > skew (Point x 0)
+skew :: (TrigField a) => Point a -> Transform a
+skew (Point x y) = Transform one (tan x) zero (tan y) one zero
+
+-- | rotate a point by x relative to the origin
+--
+-- >>> rotateP (pi/2) (Point 1 0)
+-- Point 6.123233995736766e-17 1.0
+rotateP :: (TrigField a) => a -> Point a -> Point a
+rotateP d p = rotate d |. p
+
 -- | Create Points for a formulae y = f(x) across an x range
 --
--- >>> gridP (**2) (Range 0 4) 4
+-- >>> gridP (^2) (Range 0 4) 4
 -- [Point 0.0 0.0,Point 1.0 1.0,Point 2.0 4.0,Point 3.0 9.0,Point 4.0 16.0]
 gridP :: (FieldSpace (Range a)) => (a -> a) -> Range a -> Grid (Range a) -> [Point a]
 gridP f r g = (\x -> Point x (f x)) <$> grid OuterPos r g
+
+-- | dot product
+dotP :: (Multiplicative a, Additive a) => Point a -> Point a -> a
+dotP (Point x y) (Point x' y') = x * x' + y * y'
+
+infix 4 <.>
+
+-- | dot product operator
+(<.>) :: (Multiplicative a, Additive a) => Point a -> Point a -> a
+(<.>) = dotP
+
+-- | cross product
+crossP :: (Multiplicative a, Subtractive a) => Point a -> Point a -> a
+crossP (Point x y) (Point x' y') = x * y' - y * x'
+
+-- | reflect on x-axis
+flipY :: (Subtractive a) => Point a -> Point a
+flipY (Point x y) = Point x (- y)
+
+-- | A line is a composed of 2 'Point's
+data Line a = Line
+  { lineStart :: Point a,
+    lineEnd :: Point a
+  }
+  deriving (Show, Eq, Functor, Foldable, Traversable)
+
+instance (Multiplicative a, Additive a) => Affinity (Line a) a where
+  transform t (Line s e) = Line (transform t s) (transform t e)
+
+-- | Return the parameters (a, b, c) for the line equation @a*x + b*y + c = 0@.
+lineSolve :: ExpField a => Line a -> (a, a, a)
+lineSolve (Line p1 p2) = (- my, mx, c)
+  where
+    m@(Point mx my) = basis (p2 - p1)
+    c = crossP p1 m
+
+-- | Return the signed distance from a point to the line.  If the
+-- distance is negative, the point lies to the right of the line
+lineDistance :: (ExpField a) => Line a -> Point a -> a
+lineDistance (Line (Point x1 y1) (Point x2 y2)) =
+  let dy = y1 - y2
+      dx = x2 - x1
+      d = sqrt (dx * dx + dy * dy)
+   in dy `seq` dx `seq` d
+        `seq` \(Point x y) -> (x - x1) * dy / d + (y - y1) * dx / d
+
+-- | Return the point on the line closest to the given point.
+closestPoint :: (Field a) => Line a -> Point a -> Point a
+closestPoint (Line p1 p2) p3 = Point px py
+  where
+    d@(Point dx dy) = p2 - p1
+    u = dy * _y p3 + dx * _x p3
+    v = _x p1 * _y p2 - _x p2 * _y p1
+    m = d <.> d
+    px = (dx * u + dy * v) / m
+    py = (dy * u - dx * v) / m
+
+-- | Calculate the intersection of two lines.  If the determinant is
+-- less than tolerance (parallel or coincident lines), return Nothing.
+lineIntersect :: (Ord a, Epsilon a, Signed a, Field a) => Line a -> Line a -> Maybe (Point a)
+lineIntersect (Line p1 p2) (Line p3 p4)
+  | abs det <= epsilon = Nothing
+  | otherwise = Just $ (a .* d2 - b .* d1) /. det
+  where
+    d1 = p1 - p2
+    d2 = p3 - p4
+    det = crossP d1 d2
+    a = crossP p1 p2
+    b = crossP p3 p4
+
diff --git a/src/NumHask/Space/Range.hs b/src/NumHask/Space/Range.hs
--- a/src/NumHask/Space/Range.hs
+++ b/src/NumHask/Space/Range.hs
@@ -1,7 +1,9 @@
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE RebindableSyntax #-}
 {-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
 {-# OPTIONS_GHC -Wall #-}
 
 -- | A Space containing numerical elements
@@ -18,14 +20,13 @@
 import Data.Semigroup.Foldable (Foldable1 (..))
 import Data.Semigroup.Traversable (Traversable1 (..))
 import GHC.Show (show)
-import NumHask.Space.Types as S
 import NumHask.Prelude hiding (show)
+import NumHask.Space.Types as S
 
 -- $setup
 --
 -- >>> :set -XFlexibleContexts
 -- >>> :set -XGADTs
---
 
 -- | A continuous range over type a
 --
@@ -33,12 +34,12 @@
 -- >>> a
 -- Range -1 1
 --
--- Num instance based on interval arithmetic (with Ranges normalising to lower ... upper)
---
 -- >>> a + a
 -- Range -2 2
+--
 -- >>> a * a
--- Range -1.0 1.0
+-- Range -2.0 2.0
+--
 -- >>> (+1) <$> (Range 1 2)
 -- Range 2 3
 --
@@ -120,10 +121,10 @@
 
   (>.<) = Range
 
-instance FieldSpace (Range Double) where
-  type Grid (Range Double) = Int
+instance (Field a, Ord a, FromIntegral a Int) => FieldSpace (Range a) where
+  type Grid (Range a) = Int
 
-  grid o s n = (+ bool 0 (step / 2) (o == MidPos)) <$> posns
+  grid o s n = (+ bool zero (step / two) (o == MidPos)) <$> posns
     where
       posns = (lower s +) . (step *) . fromIntegral <$> [i0 .. i1]
       step = (/) (width s) (fromIntegral n)
@@ -150,9 +151,18 @@
   negate (Range l u) = negate u ... negate l
 
 instance (Field a, Eq a, Ord a) => Multiplicative (Range a) where
-  (Range l u) * (Range l' u') =
-    space1 [l * l', l * u', u * l', u * u']
-  one = Range (negate one/(one + one)) (one/(one+one))
+  a * b = bool (Range (m - r / (one + one)) (m + r / (one + one))) zero (a == zero || b == zero)
+    where
+      m = mid a + mid b
+      r = width a * width b
+
+  one = Range (negate one / (one + one)) (one / (one + one))
+
+instance (Ord a, Field a) => Divisive (Range a) where
+  recip a = bool (Range (- m - one / (two * r)) (- m + one / (two * r))) zero (r == zero)
+    where
+      m = mid a
+      r = width a
 
 instance (Field a, Subtractive a, Eq a, Ord a) => Signed (Range a) where
   sign (Range l u) = bool (negate one) one (u >= l)
diff --git a/src/NumHask/Space/Rect.hs b/src/NumHask/Space/Rect.hs
--- a/src/NumHask/Space/Rect.hs
+++ b/src/NumHask/Space/Rect.hs
@@ -1,16 +1,16 @@
-{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE DeriveTraversable #-}
 {-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NegativeLiterals #-}
 {-# LANGUAGE PatternSynonyms #-}
 {-# LANGUAGE RebindableSyntax #-}
 {-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
 {-# OPTIONS_GHC -Wall #-}
-{-# OPTIONS_GHC -Wincomplete-patterns #-}
 
--- | a two-dimensional plane, implemented as a composite of a 'Point' of 'Range's.
+-- | A (finite) two-dimensional plane, implemented as a composite of a 'Point' of 'Range's.
 module NumHask.Space.Rect
   ( Rect (..),
     pattern Rect,
@@ -19,37 +19,50 @@
     corners4,
     projectRect,
     foldRect,
+    foldRectUnsafe,
     addPoint,
-    rotateRect,
+    rotationBound,
     gridR,
     gridF,
     aspect,
     ratio,
+    projectOnR,
+    projectOnP,
   )
 where
 
 import Data.Distributive as D
+import Data.Foldable (Foldable (foldr1))
 import Data.Functor.Compose
 import Data.Functor.Rep
-import Data.List.NonEmpty
-import GHC.Exts
 import GHC.Show (show)
+import NumHask.Prelude hiding (Distributive, rotate, show)
 import NumHask.Space.Point
 import NumHask.Space.Range
 import NumHask.Space.Types
-import NumHask.Prelude hiding (rotate, Distributive, show)
 
 -- $setup
 --
 -- >>> :set -XFlexibleContexts
--- >>> :set -XGADTs
---
 
--- | a rectangular space often representing a 2-dimensional or XY plane.
+-- | a rectangular space often representing a finite 2-dimensional or XY plane.
 --
+-- >>> one :: Rect Double
+-- Rect -0.5 0.5 -0.5 0.5
+--
+-- >>> zero :: Rect Double
+-- Rect 0.0 0.0 0.0 0.0
+--
+-- >>> one + one :: Rect Double
+-- Rect -1.0 1.0 -1.0 1.0
+--
 -- >>> let a = Rect (-1.0) 1.0 (-2.0) 4.0
 -- >>> a
 -- Rect -1.0 1.0 -2.0 4.0
+--
+-- >>> a * one
+-- Rect -1.0 1.0 -2.0 4.0
+--
 -- >>> let (Ranges x y) = a
 -- >>> x
 -- Range -1.0 1.0
@@ -142,10 +155,10 @@
 
   (|<|) s0 s1 = lower s1 `joinLeq` upper s0
 
-instance FieldSpace (Rect Double) where
-  type Grid (Rect Double) = Point Int
+instance (FromIntegral a Int, Field a, Ord a) => FieldSpace (Rect a) where
+  type Grid (Rect a) = Point Int
 
-  grid o s n = (+ bool zero (step / (one+one)) (o == MidPos)) <$> posns
+  grid o s n = (+ bool zero (step / (one + one)) (o == MidPos)) <$> posns
     where
       posns =
         (lower s +) . (step *) . fmap fromIntegral
@@ -194,10 +207,11 @@
 -- >>> projectRect (Rect 0 1 (-1) 0) (Rect 0 4 0 8) (Rect 0.25 0.75 (-0.75) (-0.25))
 -- Rect 1.0 3.0 2.0 6.0
 projectRect ::
-  Rect Double ->
-  Rect Double ->
-  Rect Double ->
-  Rect Double
+  (Field a, Ord a) =>
+  Rect a ->
+  Rect a ->
+  Rect a ->
+  Rect a
 projectRect r0 r1 (Rect a b c d) = Rect a' b' c' d'
   where
     (Point a' c') = project r0 r1 (Point a c)
@@ -206,10 +220,8 @@
 -- | Numeric algebra based on interval arithmetioc for addition and unitRect and projection for multiplication
 -- >>> one + one :: Rect Double
 -- Rect -1.0 1.0 -1.0 1.0
---
 instance (Additive a) => Additive (Rect a) where
-  (+)
-    (Rect a b c d) (Rect a' b' c' d') =
+  (+) (Rect a b c d) (Rect a' b' c' d') =
     Rect (a + a') (b + b') (c + c') (d + d')
   zero = Rect zero zero zero zero
 
@@ -217,17 +229,14 @@
   negate = fmap negate
 
 instance (Ord a, Field a) => Multiplicative (Rect a) where
-  (*)
-    (Ranges x0 y0) (Ranges x1 y1) =
-    Ranges (x0 `rtimes` x1) (y0 `rtimes` y1)
-    where
-      rtimes a b = bool (Range (m - r / (one+one)) (m + r / (one+one))) zero (a == zero || b == zero)
-        where
-          m = mid a + mid b
-          r = width a * width b
+  (*) (Ranges x0 y0) (Ranges x1 y1) =
+    Ranges (x0 * x1) (y0 * y1)
 
   one = Ranges one one
 
+instance (Ord a, Field a) => Divisive (Rect a) where
+  recip (Ranges x y) = Ranges (recip x) (recip y)
+
 instance (Ord a, Field a) => Signed (Rect a) where
   sign (Rect x z y w) = bool (negate one) one (z >= x && (w >= y))
   abs (Ranges x y) = Ranges (abs x) (abs y)
@@ -240,6 +249,13 @@
 foldRect [] = Nothing
 foldRect (x : xs) = Just $ sconcat (x :| xs)
 
+-- | convex hull union of Rect's applied to a non-empty structure
+--
+-- >>> foldRectUnsafe [Rect 0 1 0 1, one]
+-- Rect -0.5 1.0 -0.5 1.0
+foldRectUnsafe :: (Foldable f, Ord a) => f (Rect a) -> Rect a
+foldRectUnsafe = foldr1 (<>)
+
 -- | add a Point to a Rect
 --
 -- >>> addPoint (Point 0 1) one
@@ -249,18 +265,17 @@
 
 -- | rotate the corners of a Rect by x degrees relative to the origin, and fold to a new Rect
 --
--- >>> rotateRect 45 one
--- Rect -0.7071067811865475 0.7071067811865475 -5.551115123125783e-17 5.551115123125783e-17
-rotateRect :: Double -> Rect Double -> Rect Double
-rotateRect d r =
-  space1 $ rotate d <$> corners r
+-- >>> rotationBound (pi/4) one
+-- Rect -0.7071067811865475 0.7071067811865475 -0.7071067811865475 0.7071067811865475
+rotationBound :: (TrigField a, Ord a) => a -> Rect a -> Rect a
+rotationBound d = space1 . fmap (rotate d |.) . corners4
 
 -- | Create Rects for a formulae y = f(x) across an x range where the y range is Range 0 y
 --
--- >>> gridR (**2) (Range 0 4) 4
+-- >>> gridR (^2) (Range 0 4) 4
 -- [Rect 0.0 1.0 0.0 0.25,Rect 1.0 2.0 0.0 2.25,Rect 2.0 3.0 0.0 6.25,Rect 3.0 4.0 0.0 12.25]
-gridR :: (Double -> Double) -> Range Double -> Int -> [Rect Double]
-gridR f r g = (\x -> Rect (x - tick / 2) (x + tick / 2) 0 (f x)) <$> grid MidPos r g
+gridR :: (Field a, FromIntegral a Int, Ord a) => (a -> a) -> Range a -> Int -> [Rect a]
+gridR f r g = (\x -> Rect (x - tick / two) (x + tick / two) zero (f x)) <$> grid MidPos r g
   where
     tick = width r / fromIntegral g
 
@@ -276,11 +291,38 @@
 -- >>> aspect 2
 -- Rect -1.0 1.0 -0.5 0.5
 aspect :: Double -> Rect Double
-aspect a = Rect (a * (-0.5)) (a * 0.5) (-0.5) 0.5
+aspect a = Rect (a * -0.5) (a * 0.5) -0.5 0.5
 
 -- | convert a Rect to a ratio
 --
--- >>> ratio (Rect (-1) 1 (-0.5) 0.5)
+-- >>> :set -XNegativeLiterals
+-- >>> ratio (Rect -1 1 -0.5 0.5)
 -- 2.0
 ratio :: (Field a) => Rect a -> a
 ratio (Rect x z y w) = (z - x) / (w - y)
+
+-- | project a Rect from one Rect to another, preserving relative position, with guards for singleton Rects.
+--
+-- >>> projectOnR one (Rect 0 1 0 1) (Rect 0 0.5 0 0.5)
+-- Rect -0.5 0.0 -0.5 0.0
+projectOnR :: Rect Double -> Rect Double -> Rect Double -> Rect Double
+projectOnR new old@(Rect x z y w) ao@(Rect ox oz oy ow)
+  | x == z && y == w = ao
+  | x == z = Rect ox oz ny nw
+  | y == w = Rect nx nz oy ow
+  | otherwise = a
+  where
+    a@(Rect nx nz ny nw) = projectRect old new ao
+
+-- | project a Point from one Rect to another, preserving relative position, with guards for singleton Rects.
+--
+-- >>> projectOnP one (Rect 0 1 0 1) zero
+-- Point -0.5 -0.5
+projectOnP :: Rect Double -> Rect Double -> Point Double -> Point Double
+projectOnP new old@(Rect x z y w) po@(Point px py)
+  | x == z && y == w = po
+  | x == z = Point px py'
+  | y == w = Point px' py
+  | otherwise = Point px' py'
+  where
+    (Point px' py') = project old new po
diff --git a/src/NumHask/Space/Time.hs b/src/NumHask/Space/Time.hs
--- a/src/NumHask/Space/Time.hs
+++ b/src/NumHask/Space/Time.hs
@@ -1,8 +1,9 @@
-{-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE NegativeLiterals #-}
 {-# LANGUAGE RebindableSyntax #-}
 {-# OPTIONS_GHC -Wall #-}
-{-# OPTIONS_GHC -Wno-unused-top-binds #-}
 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
 {-# OPTIONS_GHC -fno-warn-type-defaults #-}
 
@@ -23,13 +24,22 @@
   )
 where
 
+import Data.Fixed (Fixed (MkFixed))
 import Data.List (nub)
 import Data.String (String)
 import Data.Time
-import NumHask.Space.Types
 import NumHask.Prelude
-import Data.Fixed (Fixed(MkFixed))
+import NumHask.Space.Types
 
+-- $setup
+--
+-- >>> :set -XRebindableSyntax
+-- >>> :set -XNegativeLiterals
+-- >>> import NumHask.Prelude
+-- >>> import NumHask.Space
+-- >>> Point 1 1
+-- Point 1 1
+
 -- | parse text as per iso8601
 --
 -- >>> :set -XOverloadedStrings
@@ -58,9 +68,11 @@
 grainSecs (Minutes n) = fromIntegral n * 60
 grainSecs (Seconds n) = n
 
+-- | convenience conversion to Double
 fromNominalDiffTime :: NominalDiffTime -> Double
-fromNominalDiffTime t = let (MkFixed i) = (nominalDiffTimeToSeconds t) in (fromInteger i) * 1e-12
+fromNominalDiffTime t = let (MkFixed i) = nominalDiffTimeToSeconds t in fromInteger i * 1e-12
 
+-- | convenience conversion from Double
 toNominalDiffTime :: Double -> NominalDiffTime
 toNominalDiffTime x =
   let d0 = ModifiedJulianDay 0
@@ -70,25 +82,32 @@
       t1 = UTCTime (addDays days d0) (picosecondsToDiffTime $ floor (secs / 1.0e-12))
    in diffUTCTime t1 t0
 
+-- | Convert from 'DiffTime' to seconds (as a Double)
+--
+-- >>> fromDiffTime $ toDiffTime 1
+-- 1.0
 fromDiffTime :: DiffTime -> Double
 fromDiffTime =
-  (\x -> x / ((10 :: Double) ^ (12 :: Integer))) . fromIntegral . fromEnum
+  (/ 1e12) . fromIntegral . fromEnum
 
+-- | Convert from seconds (as a Double) to 'DiffTime'
+-- >>> toDiffTime 1
+-- 1s
 toDiffTime :: Double -> DiffTime
-toDiffTime d = toEnum . fromEnum $ d * ((10 :: Double) ^ (12 :: Integer))
+toDiffTime d = toEnum . fromEnum $ d * 1e12
 
 -- | add a TimeGrain to a UTCTime
 --
--- >>> addGrain (Years 1) 5 (UTCTime (fromGregorian 2015 2 28) 0)
+-- >>> addGrain (Years 1) 5 (UTCTime (fromGregorian 2015 2 28) (toDiffTime 0))
 -- 2020-02-29 00:00:00 UTC
 --
--- >>> addGrain (Months 1) 1 (UTCTime (fromGregorian 2015 2 28) 0)
+-- >>> addGrain (Months 1) 1 (UTCTime (fromGregorian 2015 2 28) (toDiffTime 0))
 -- 2015-03-31 00:00:00 UTC
 --
--- >>> addGrain (Hours 6) 5 (UTCTime (fromGregorian 2015 2 28) 0)
+-- >>> addGrain (Hours 6) 5 (UTCTime (fromGregorian 2015 2 28) (toDiffTime 0))
 -- 2015-03-01 06:00:00 UTC
 --
--- >>> addGrain (Seconds 0.001) (60*1000+1) (UTCTime (fromGregorian 2015 2 28) 0)
+-- >>> addGrain (Seconds 0.001) (60*1000+1) (UTCTime (fromGregorian 2015 2 28) (toDiffTime 0))
 -- 2015-02-28 00:01:00.001 UTC
 addGrain :: TimeGrain -> Int -> UTCTime -> UTCTime
 addGrain (Years n) x (UTCTime d t) =
@@ -129,19 +148,19 @@
 
 -- | compute the floor UTCTime based on the timegrain
 --
--- >>> floorGrain (Years 5) (UTCTime (fromGregorian 1999 1 1) 0)
+-- >>> floorGrain (Years 5) (UTCTime (fromGregorian 1999 1 1) (toDiffTime 0))
 -- 1995-12-31 00:00:00 UTC
 --
--- >>> floorGrain (Months 3) (UTCTime (fromGregorian 2016 12 30) 0)
+-- >>> floorGrain (Months 3) (UTCTime (fromGregorian 2016 12 30) (toDiffTime 0))
 -- 2016-09-30 00:00:00 UTC
 --
--- >>> floorGrain (Days 5) (UTCTime (fromGregorian 2016 12 30) 1)
+-- >>> floorGrain (Days 5) (UTCTime (fromGregorian 2016 12 30) (toDiffTime 1))
 -- 2016-12-30 00:00:00 UTC
 --
 -- >>> floorGrain (Minutes 15) (UTCTime (fromGregorian 2016 12 30) (toDiffTime $ 15*60+1))
 -- 2016-12-30 00:15:00 UTC
 --
--- >>> floorGrain (Seconds 0.1) (UTCTime (fromGregorian 2016 12 30) 0.12)
+-- >>> floorGrain (Seconds 0.1) (UTCTime (fromGregorian 2016 12 30) ((toDiffTime 0.12)))
 -- 2016-12-30 00:00:00.1 UTC
 floorGrain :: TimeGrain -> UTCTime -> UTCTime
 floorGrain (Years n) (UTCTime d _) = UTCTime (addDays (-1) $ fromGregorian y' 1 1) (secondsToDiffTime 0)
@@ -168,19 +187,19 @@
 
 -- | compute the ceiling UTCTime based on the timegrain
 --
--- >>> ceilingGrain (Years 5) (UTCTime (fromGregorian 1999 1 1) 0)
+-- >>> ceilingGrain (Years 5) (UTCTime (fromGregorian 1999 1 1) (toDiffTime 0))
 -- 2000-12-31 00:00:00 UTC
 --
--- >>> ceilingGrain (Months 3) (UTCTime (fromGregorian 2016 12 30) 0)
+-- >>> ceilingGrain (Months 3) (UTCTime (fromGregorian 2016 12 30) (toDiffTime 0))
 -- 2016-12-31 00:00:00 UTC
 --
--- >>> ceilingGrain (Days 5) (UTCTime (fromGregorian 2016 12 30) 1)
+-- >>> ceilingGrain (Days 5) (UTCTime (fromGregorian 2016 12 30) (toDiffTime 1))
 -- 2016-12-31 00:00:00 UTC
 --
 -- >>> ceilingGrain (Minutes 15) (UTCTime (fromGregorian 2016 12 30) (toDiffTime $ 15*60+1))
 -- 2016-12-30 00:30:00 UTC
 --
--- >>> ceilingGrain (Seconds 0.1) (UTCTime (fromGregorian 2016 12 30) 0.12)
+-- >>> ceilingGrain (Seconds 0.1) (UTCTime (fromGregorian 2016 12 30) (toDiffTime 0.12))
 -- 2016-12-30 00:00:00.2 UTC
 ceilingGrain :: TimeGrain -> UTCTime -> UTCTime
 ceilingGrain (Years n) (UTCTime d _) = UTCTime (addDays (-1) $ fromGregorian y' 1 1) (secondsToDiffTime 0)
@@ -192,7 +211,7 @@
     (y, m, _) = toGregorian (addDays 1 d)
     m' = (m + n - 1) `div` n * n
     (y', m'') = fromIntegral <$> if m' == 12 then (y + 1, 1) else (y, m' + 1)
-ceilingGrain (Days _) (UTCTime d t) = if t == (secondsToDiffTime 0) then UTCTime d (secondsToDiffTime 0) else UTCTime (addDays 1 d) (secondsToDiffTime 0)
+ceilingGrain (Days _) (UTCTime d t) = if t == secondsToDiffTime 0 then UTCTime d (secondsToDiffTime 0) else UTCTime (addDays 1 d) (secondsToDiffTime 0)
 ceilingGrain (Hours h) u@(UTCTime _ t) = addUTCTime x u
   where
     s = fromDiffTime t
@@ -213,7 +232,7 @@
 --
 -- The assumption with getSensibleTimeGrid is that there is a list of discountinuous UTCTimes rather than a continuous range.  Output is a list of index points for the original [UTCTime] and label tuples, and a list of unused list elements.
 --
--- >>> placedTimeLabelDiscontinuous PosIncludeBoundaries (Just "%d %b") 2 [UTCTime (fromGregorian 2017 12 6) 0, UTCTime (fromGregorian 2017 12 29) 0, UTCTime (fromGregorian 2018 1 31) 0, UTCTime (fromGregorian 2018 3 3) 0]
+-- >>> placedTimeLabelDiscontinuous PosIncludeBoundaries (Just "%d %b") 2 [UTCTime (fromGregorian 2017 12 6) (toDiffTime 0), UTCTime (fromGregorian 2017 12 29) (toDiffTime 0), UTCTime (fromGregorian 2018 1 31) (toDiffTime 0), UTCTime (fromGregorian 2018 3 3) (toDiffTime 0)]
 -- ([(0,"06 Dec"),(1,"31 Dec"),(2,"28 Feb"),(3,"03 Mar")],[])
 placedTimeLabelDiscontinuous :: PosDiscontinuous -> Maybe Text -> Int -> [UTCTime] -> ([(Int, Text)], [UTCTime])
 placedTimeLabelDiscontinuous posd format n ts = (zip (fst <$> inds') labels, rem')
@@ -232,7 +251,6 @@
       | p > a = (p : ps, xs, n + 1)
       | otherwise = step (ps, (n - 1, p) : xs, n) a
     (rem', inds) = done $ foldl' step begin ts
-
     inds' = laterTimes inds
     fmt = case format of
       Just f -> unpack f
@@ -254,13 +272,13 @@
 laterTimes :: [(Int, a)] -> [(Int, a)]
 laterTimes [] = []
 laterTimes [x] = [x]
-laterTimes (x : xs) = (\(x0, x1) -> reverse $ x0 : x1) $ foldl' step (x,[]) xs
+laterTimes (x : xs) = (\(x0, x1) -> reverse $ x0 : x1) $ foldl' step (x, []) xs
   where
     step ((n, a), rs) (na, aa) = if na == n then ((na, aa), rs) else ((na, aa), (n, a) : rs)
 
 -- | A sensible time grid between two dates, projected onto (0,1) with no attempt to get finnicky.
 --
--- >>> placedTimeLabelContinuous PosIncludeBoundaries (Just "%d %b") 2 (UTCTime (fromGregorian 2017 12 6) 0, UTCTime (fromGregorian 2017 12 29) 0)
+-- >>> placedTimeLabelContinuous PosIncludeBoundaries (Just "%d %b") 2 (UTCTime (fromGregorian 2017 12 6) (toDiffTime 0), UTCTime (fromGregorian 2017 12 29) (toDiffTime 0))
 -- [(0.0,"06 Dec"),(0.4347826086956521,"16 Dec"),(0.8695652173913042,"26 Dec"),(0.9999999999999999,"29 Dec")]
 placedTimeLabelContinuous :: PosDiscontinuous -> Maybe Text -> Int -> (UTCTime, UTCTime) -> [(Double, Text)]
 placedTimeLabelContinuous posd format n (l, u) = zip tpsd labels
@@ -280,16 +298,16 @@
 
 -- | compute a sensible TimeGrain and list of UTCTimes
 --
--- >>> sensibleTimeGrid InnerPos 2 (UTCTime (fromGregorian 2016 12 31) 0, UTCTime (fromGregorian 2017 12 31) 0)
+-- >>> sensibleTimeGrid InnerPos 2 (UTCTime (fromGregorian 2016 12 31) (toDiffTime 0), UTCTime (fromGregorian 2017 12 31) (toDiffTime 0))
 -- (Months 6,[2016-12-31 00:00:00 UTC,2017-06-30 00:00:00 UTC,2017-12-31 00:00:00 UTC])
 --
--- >>> sensibleTimeGrid InnerPos 2 (UTCTime (fromGregorian 2017 1 1) 0, UTCTime (fromGregorian 2017 12 30) 0)
+-- >>> sensibleTimeGrid InnerPos 2 (UTCTime (fromGregorian 2017 1 1) (toDiffTime 0), UTCTime (fromGregorian 2017 12 30) (toDiffTime 0))
 -- (Months 6,[2017-06-30 00:00:00 UTC])
 --
--- >>>  sensibleTimeGrid UpperPos 2 (UTCTime (fromGregorian 2017 1 1) 0, UTCTime (fromGregorian 2017 12 30) 0)
+-- >>>  sensibleTimeGrid UpperPos 2 (UTCTime (fromGregorian 2017 1 1) (toDiffTime 0), UTCTime (fromGregorian 2017 12 30) (toDiffTime 0))
 -- (Months 6,[2017-06-30 00:00:00 UTC,2017-12-31 00:00:00 UTC])
 --
--- >>>sensibleTimeGrid LowerPos 2 (UTCTime (fromGregorian 2017 1 1) 0, UTCTime (fromGregorian 2017 12 30) 0)
+-- >>>sensibleTimeGrid LowerPos 2 (UTCTime (fromGregorian 2017 1 1) (toDiffTime 0), UTCTime (fromGregorian 2017 12 30) (toDiffTime 0))
 -- (Months 6,[2016-12-31 00:00:00 UTC,2017-06-30 00:00:00 UTC])
 sensibleTimeGrid :: Pos -> Int -> (UTCTime, UTCTime) -> (TimeGrain, [UTCTime])
 sensibleTimeGrid p n (l, u) = (grain, ts)
diff --git a/src/NumHask/Space/Types.hs b/src/NumHask/Space/Types.hs
--- a/src/NumHask/Space/Types.hs
+++ b/src/NumHask/Space/Types.hs
@@ -1,6 +1,9 @@
-{-# LANGUAGE RebindableSyntax #-}
 {-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE DeriveTraversable #-}
 {-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE RebindableSyntax #-}
 {-# LANGUAGE TypeFamilies #-}
 {-# OPTIONS_HADDOCK hide #-}
 
@@ -14,6 +17,8 @@
     project,
     Pos (..),
     space1,
+    randomS,
+    randomSs,
     memberOf,
     contains,
     disjoint,
@@ -25,13 +30,18 @@
     widenEps,
     scale,
     move,
+    Transform (..),
+    inverseTransform,
+    Affinity (..),
+    (|.),
+    rotate,
   )
 where
 
-import NumHask.Prelude
+import NumHask.Prelude hiding (rotate)
 import qualified Prelude as P
 
--- | Space is a continuous range of numbers that contains elements and has an upper and lower value.
+-- | A 'Space' is a continuous set of numbers. Continuous here means that the set has an upper and lower bound, and an element that is between these two bounds is a member of the 'Space'.
 --
 -- > a `union` b == b `union` a
 -- > a `intersection` b == b `intersection` a
@@ -74,8 +84,8 @@
   --
   -- > lower a \/ upper a == lower a
   -- > lower a /\ upper a == upper a
-  norm :: s -> s
-  norm s = lower s ... upper s
+  normalise :: s -> s
+  normalise s = lower s ... upper s
 
   -- | create a normalised space from two elements
   infix 3 ...
@@ -151,6 +161,21 @@
 instance (Space a) => Semigroup (Intersection a) where
   (<>) (Intersection a) (Intersection b) = Intersection (a `union` b)
 
+-- | supply a random element within a 'Space'
+randomS :: (Space s, RandomGen g, Random (Element s)) => s -> g -> (Element s, g)
+randomS s = randomR (lower s, upper s)
+
+-- | supply an (infinite) list of random elements within a 'Space'
+--
+-- >>> let g = mkStdGen 42
+-- >>> take 3 $ randomSs (one :: Range Double) g
+-- [0.43085240252163404,-6.472345419562497e-2,0.3854692674681801]
+--
+-- >>> take 3 $ randomSs (Rect 0 10 0 10 :: Rect Word8) g
+-- [Point 8 0,Point 6 4,Point 5 3]
+randomSs :: (Space s, RandomGen g, Random (Element s)) => s -> g -> [Element s]
+randomSs s = randomRs (lower s, upper s)
+
 -- | a space that can be divided neatly
 --
 -- > space1 (grid OuterPos s g) == s
@@ -182,7 +207,7 @@
 mid :: (Space s, Field (Element s)) => s -> Element s
 mid s = (lower s + upper s) / (one + one)
 
--- | project a data point from one space to another, preserving relative position
+-- | project an element from one space to another, preserving relative position.
 --
 -- > project o n (lower o) = lower n
 -- > project o n (upper o) = upper n
@@ -227,7 +252,7 @@
 widenEps ::
   ( Space s,
     FromRational (Element s),
-    Field (Element s)
+    Ring (Element s)
   ) =>
   Element s ->
   s ->
@@ -241,3 +266,75 @@
 -- | Move a Space. (scalar addition)
 move :: (Additive (Element s), Space s) => Element s -> s -> s
 move e s = (e + lower s) ... (e + upper s)
+
+-- | linear transform + translate of a point-like number
+--
+-- > (x, y) -> (ax + by + c, dx + ey + d)
+--
+-- or
+--
+-- \[
+-- \begin{pmatrix}
+-- a & b & c\\
+-- d & e & f\\
+-- 0 & 0 & 1
+-- \end{pmatrix}
+-- \begin{pmatrix}
+-- x\\
+-- y\\
+-- 1
+-- \end{pmatrix}
+-- \]
+data Transform a = Transform
+  { ta :: !a,
+    tb :: !a,
+    tc :: !a,
+    td :: !a,
+    te :: !a,
+    tf :: !a
+  }
+  deriving (Eq, Show, Functor, Foldable, Traversable)
+
+-- | Calculate the inverse of a transformation.
+inverseTransform :: (Eq a, Field a) => Transform a -> Maybe (Transform a)
+inverseTransform (Transform a b c d e f) =
+  let det = a * e - b * d
+   in bool
+        ( Just
+            ( Transform
+                (a / det)
+                (d / det)
+                (- (a * c + d * f) / det)
+                (b / det)
+                (e / det)
+                (- (b * c + e * f) / det)
+            )
+        )
+        Nothing
+        (det == zero)
+
+-- | An 'Affinity' is something that can be subjected to an affine transformation in 2-dimensional space, where affine means a linear matrix operation or a translation (+).
+--
+-- https://en.wikipedia.org/wiki/Affine_transformation
+class Affinity a b | a -> b where
+  transform :: Transform b -> a -> a
+
+infix 3 |.
+
+-- | Apply a 'Transform' to an 'Affinity'
+(|.) :: (Affinity a b) => Transform b -> a -> a
+(|.) = transform
+
+instance (Multiplicative a, Additive a) => Affinity (Transform a) a where
+  transform (Transform a' b' c' d' e' f') (Transform a b c d e f) =
+    Transform
+      (a * a' + b' * d)
+      (a' * b + b' * e)
+      (a' * c + b' * f + c')
+      (d' * a + e' * d)
+      (d' * b + e' * e)
+      (d' * c + e' * f + f')
+
+-- | Rotate an 'Affinity'
+rotate :: (TrigField a) => a -> Transform a
+rotate a = Transform (cos a) (- sin a) zero (sin a) (cos a) zero
diff --git a/src/NumHask/Space/XY.hs b/src/NumHask/Space/XY.hs
--- a/src/NumHask/Space/XY.hs
+++ b/src/NumHask/Space/XY.hs
@@ -1,24 +1,21 @@
 {-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE RebindableSyntax #-}
 {-# LANGUAGE PatternSynonyms #-}
 
-{- | Unification of a point on the XY-plane and a rectangle on the XY-plane.
-
-
--}
-
+-- | Unification of 'Point' and 'Rect'.
 module NumHask.Space.XY
-  ( XY(..),
+  ( XY (..),
     pattern P,
     pattern R,
     toRect,
     toPoint,
     projectOn,
     projectTo,
-    fixRect,
-  ) where
+  )
+where
 
-import NumHask.Prelude
+import GHC.Show (show)
+import NumHask.Prelude hiding (show)
 import NumHask.Space.Point
 import NumHask.Space.Rect
 import NumHask.Space.Types
@@ -27,8 +24,12 @@
 data XY a
   = PointXY (Point a)
   | RectXY (Rect a)
-  deriving (Eq, Show, Functor)
+  deriving (Eq, Functor)
 
+instance (Show a) => Show (XY a) where
+  show (PointXY (Point x y)) = "P " <> show x <> " " <> show y
+  show (RectXY (Rect x z y w)) = "R " <> show x <> " " <> show z <> " " <> show y <> " " <> show w
+
 -- | make an XY from a point
 pattern P :: a -> a -> XY a
 pattern P x y = PointXY (Point x y)
@@ -63,12 +64,12 @@
 
 -- * Natural transformations
 
--- | Convert a spot to a Rect
+-- | Convert an XY to a Rect
 toRect :: XY a -> Rect a
 toRect (PointXY (Point x y)) = Rect x x y y
 toRect (RectXY a) = a
 
--- | Convert a spot to a Point
+-- | Convert an XY to a Point
 toPoint :: (Ord a, Field a) => XY a -> Point a
 toPoint (PointXY (Point x y)) = Point x y
 toPoint (RectXY (Ranges x y)) = Point (mid x) (mid y)
@@ -79,37 +80,15 @@
 -- | project an XY from one Rect to another, preserving relative position.
 --
 -- >>> projectOn one (Rect 0 1 0 1) zero
--- PointXY Point -0.5 -0.5
+-- P -0.5 -0.5
 projectOn :: Rect Double -> Rect Double -> XY Double -> XY Double
-projectOn new old@(Rect x z y w) po@(PointXY (Point px py))
-  | x == z && y == w = po
-  | x == z = (P px py')
-  | y == w = (P px' py)
-  | otherwise = (P px' py')
-  where
-    (Point px' py') = project old new (toPoint po)
-projectOn new old@(Rect x z y w) ao@(RectXY (Rect ox oz oy ow))
-  | x == z && y == w = ao
-  | x == z = (R ox oz ny nw)
-  | y == w = (R nx nz oy ow)
-  | otherwise = RectXY a
-  where
-    a@(Rect nx nz ny nw) = projectRect old new (toRect ao)
+projectOn new old (PointXY p) = PointXY $ projectOnP new old p
+projectOn new old (RectXY r) = RectXY $ projectOnR new old r
 
--- | project a [Spot a] from it's folded space to the given area
+-- | project an [XY a] from it's enclosing space to the given space
 --
--- >>> projectTo one (SpotPoint <$> zipWith Point [0..2] [0..2])
--- [SpotPoint Point -0.5 -0.5,SpotPoint Point 0.0 0.0,SpotPoint Point 0.5 0.5]
+-- >>> projectTo one (zipWith P [0..2] [0..2])
+-- [P -0.5 -0.5,P 0.0 0.0,P 0.5 0.5]
 projectTo :: Rect Double -> [XY Double] -> [XY Double]
 projectTo _ [] = []
 projectTo vb (x : xs) = projectOn vb (toRect $ sconcat (x :| xs)) <$> (x : xs)
-
--- | guard substituting singleton dimensions
-fixRect :: Maybe (Rect Double) -> Rect Double
-fixRect r = maybe one singletonUnit r
-  where
-    singletonUnit (Rect x z y w)
-      | x == z && y == w = Rect (x - 0.5) (x + 0.5) (y - 0.5) (y + 0.5)
-      | x == z = Rect (x - 0.5) (x + 0.5) y w
-      | y == w = Rect x z (y - 0.5) (y + 0.5)
-      | otherwise = Rect x z y w
diff --git a/test/test.hs b/test/test.hs
--- a/test/test.hs
+++ b/test/test.hs
@@ -1,4 +1,7 @@
+{-# LANGUAGE RebindableSyntax #-}
+{-# LANGUAGE NegativeLiterals #-}
 {-# OPTIONS_GHC -Wall #-}
+{-# OPTIONS_GHC -Wno-unused-imports #-}
 
 module Main where
 
@@ -8,9 +11,11 @@
 main :: IO ()
 main =
   doctest
-    [ "src/NumHask/Space/Histogram.hs",
+    [ "src/NumHask/Space.hs",
+      "src/NumHask/Space/Histogram.hs",
       "src/NumHask/Space/Point.hs",
       "src/NumHask/Space/Range.hs",
       "src/NumHask/Space/Rect.hs",
-      "src/NumHask/Space/Time.hs"
+      "src/NumHask/Space/Time.hs",
+      "src/NumHask/Space/XY.hs"
     ]
