packages feed

chart-svg-0.2.0: src/Chart/Reanimate.hs

{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}

-- | Integration of reanimate and chart-svg
module Chart.Reanimate
  ( chartSvgTree,
    chartSvgTreeDef,
    chartSvgTrees,
    chartSvgTreesDef,
    writeChartSvgTree,
    tree,
    toPixelRGBA8,
  )
where

import Chart as C hiding (transform, Line)
import Codec.Picture.Types
import Control.Lens hiding (transform)
import qualified Data.Attoparsec.Text as A
import Graphics.SvgTree.PathParser
import Graphics.SvgTree.Types as SvgTree hiding (Point, Text)
import Linear.V2
import NumHask.Prelude hiding (fold)
import qualified Graphics.SvgTree.CssTypes as Css
import qualified Graphics.SvgTree as SvgTree
import Reanimate
import NumHask.Space.Types (width)
import Graphics.SvgTree.Printer (ppDocument)

-- | Render a 'ChartSvg' to a 'Tree'
--
-- Alters the reanimate default viewbox of 16:9, and simplifies the SVG.
chartSvgTree :: ChartSvg -> Tree
chartSvgTree cs = simplify . withViewBox vb . mkGroup $ ts
  where
    (ts, r, _) = chartSvgTrees cs
    (Rect x z y w) = r
    vb = (x, -y, z-x, w-y)

-- | Render a 'ChartSvg' to a 'Tree' with the default viewbox
chartSvgTreeDef :: ChartSvg -> Tree
chartSvgTreeDef = simplify . mkGroup . chartSvgTreesDef

-- | Render a 'ChartSvg' to 'Tree's, the fitted chart viewbox, and the suggested SVG dimensions
--
chartSvgTrees :: ChartSvg -> ([Tree], Rect Double, Point Double)
chartSvgTrees cs = (ts, rect', size')
  where
    so = view #svgOptions cs
    ho = view #hudOptions cs
    hl = view #hudList cs
    cl = view #chartList cs
    penult = case view #chartAspect so of
      FixedAspect _ -> styleBoxesS cl
      CanvasAspect _ -> dataBoxesS cl
      ChartAspect -> styleBoxesS cl
      UnadjustedAspect -> dataBoxesS cl
    clAspect = cl &
      runHud penult [chartAspectHud (so ^. #chartAspect)] &
      maybe id (\x -> frameChart x (fromMaybe 0 (view #innerPad so)))
        (view #chartFrame so)
    (hlExtra, clExtra) = makeHud (padBox $ dataBoxes clAspect) ho
    clAll = clAspect <> clExtra
    hlAll = hl <> hlExtra
    ts = fmap (SvgTree.cssApply (cssRules (view #cssOptions so)) . tree)
      (runHud (initialCanvas (so ^. #chartAspect) clAll) hlAll clAll)
    rect' = styleBoxesS clAll & maybe id padRect (view #outerPad so)
    Point w h = NumHask.Space.Types.width rect'
    size' = Point (view #svgHeight so/h*w) (view #svgHeight so)

-- | Render a 'ChartSvg' to 'Tree's at the standard reanimate viewbox
--
chartSvgTreesDef :: ChartSvg -> [Tree]
chartSvgTreesDef cs = ts
  where
    so = view #svgOptions cs
    ho = view #hudOptions cs
    hl = view #hudList cs
    cl = view #chartList cs
    clAspect = projectXYs (Rect (-8.0) 8.0 (-4.5) 4.5) cl
    (hlExtra, clExtra) = makeHud (padBox $ dataBoxes clAspect) ho
    clAll = clAspect <> clExtra
    hlAll = hl <> hlExtra
    ts = fmap (SvgTree.cssApply (cssRules (view #cssOptions so)) . tree)
      (runHud (initialCanvas (so ^. #chartAspect) clAll) hlAll clAll)

-- | render Charts to a Document using the supplied size and viewbox.
renderToDocument :: CssOptions -> Point Double -> Rect Double -> [Tree] -> Document
renderToDocument csso (Point w' h') vb ts =
  Document
    ((\(Rect x z y w) -> Just (x, - w, z - x, w - y)) vb)
    (Just (Num w'))
    (Just (Num h'))
    (SvgTree.cssApply (cssRules csso) <$> ts)
    (unpack "")
    ""
    (PreserveAspectRatio False AlignNone Nothing)

-- | write a 'ChartSvg' to a file via conversion to a reanimate 'Tree' structure.
writeChartSvgTree :: FilePath -> ChartSvg -> IO ()
writeChartSvgTree fp cs = writeFile fp . pack . ppDocument $ doc
  where
    doc = renderToDocument (view (#svgOptions . #cssOptions) cs) size' rect' ts'
    (ts', rect', size') = chartSvgTrees cs

cssRules :: CssOptions -> [Css.CssRule]
cssRules UseCssCrisp = [cssCrisp']
cssRules UseGeometricPrecision = [cssGeometricPrecision]
cssRules NoCssOptions = []

-- | crisp edges css
cssGeometricPrecision :: Css.CssRule
cssGeometricPrecision = Css.CssRule [] [Css.CssDeclaration "shape-rendering" [[Css.CssString "geometricPrecision"]]]

cssCrisp' :: Css.CssRule
cssCrisp' = Css.CssRule [] [Css.CssDeclaration "shape-rendering" [[Css.CssString "crispEdges"]]]

-- | Rectange svg
treeRect :: Rect Double -> Tree
treeRect a =
  RectangleTree $ rectSvg a defaultSvg

-- | Text svg
treeText :: TextStyle -> Text -> Point Double -> Tree
treeText s t p =
  TextTree Nothing (textAt (pointSvg p) t)
    & maybe id (\x -> drawAttributes %~ rotatePDA x p) (s ^. #rotation)

-- | GlyphShape to svg Tree
treeShape :: GlyphShape -> Double -> Point Double -> Tree
treeShape CircleGlyph s p =
  CircleTree $ Circle mempty (pointSvg p) (Num (s / 2))
treeShape SquareGlyph s p = treeRect (move p ((s *) <$> one))
treeShape (RectSharpGlyph x') s p =
  treeRect (move p (C.scale (Point s (x' * s)) one))
treeShape (RectRoundedGlyph x'' rx ry) s p =
  RectangleTree
    . rectSvg (addPoint p $ C.scale (Point s (x'' * s)) one)
    . (rectCornerRadius .~ (Just $ Num rx, Just $ Num ry))
    $ defaultSvg
treeShape (TriangleGlyph (Point xa ya) (Point xb yb) (Point xc yc)) s p =
  PolygonTree
    . (polygonPoints .~ rps)
    $ (drawAttributes %~ translateDA p) defaultSvg
  where
    rps =
      [ V2 (s * xa) (- s * ya),
        V2 (s * xb) (- s * yb),
        V2 (s * xc) (- s * yc)
      ]
treeShape (EllipseGlyph x') s p =
  EllipseTree $
    Ellipse
      mempty
      (pointSvg p)
      (Num $ s / 2)
      (Num $ (x' * s) / 2)
treeShape (VLineGlyph x') s (Point x y) =
  LineTree $
    Line
      (mempty & strokeWidth .~ Last (Just (Num x')))
      (pointSvg (Point x (y - s / 2)))
      (pointSvg (Point x (y + s / 2)))
treeShape (HLineGlyph x') s (Point x y) =
  LineTree $
    Line
      (mempty & strokeWidth .~ Last (Just (Num x')))
      (pointSvg (Point (x - s / 2) y))
      (pointSvg (Point (x + s / 2) y))
treeShape (PathGlyph path) _ p =
  PathTree $ Path ((drawAttributes %~ translateDA p) defaultSvg) path'
  where
    path' = either mempty (: []) $ A.parseOnly command path

-- | GlyphStyle to svg Tree
treeGlyph :: GlyphStyle -> Point Double -> Tree
treeGlyph s p =
  treeShape (s ^. #shape) (s ^. #size) p
    & maybe id (\x -> drawAttributes %~ rotatePDA x p) (s ^. #rotation)

-- | line svg
treeLine :: [Point Double] -> Tree
treeLine xs =
  PolyLineTree
    . (polyLinePoints .~ ((\(Point x y) -> V2 x (- y)) <$> xs))
    $ defaultSvg

-- | GlyphStyle to svg Tree
treePath :: [PathInfo Double] -> [Point Double] -> Tree
treePath s p = PathTree $ Path mempty (zipWith (curry toPathCommand) s p)

-- | convert a 'Chart' to a 'Tree'
--
tree :: Chart Double -> Tree
tree (Chart (TextA s ts) xs) =
  groupTrees (daText s) (zipWith (treeText s) ts (toPoint <$> xs))
tree (Chart (GlyphA s) xs) =
  groupTrees (daGlyph s) (treeGlyph s . toPoint <$> xs)
tree (Chart (LineA s) xs) =
  groupTrees (daLine s) [treeLine (toPoint <$> xs)]
tree (Chart (RectA s) xs) =
  groupTrees (daRect s) (treeRect <$> (toRect <$> xs))
tree (Chart (PathA s pis) xs) =
  groupTrees (daPath s) [treePath pis (toPoint <$> xs)]
tree (Chart BlankA _) =
  groupTrees mempty []

-- | add drawing attributes as a group svg wrapping a [Tree]
groupTrees :: DrawAttributes -> [Tree] -> Tree
groupTrees da' tree' =
  GroupTree (drawAttributes %~ (<> da') $ groupChildren .~ tree' $ defaultSvg)

-- * DrawAttribute computations
daRect :: RectStyle -> DrawAttributes
daRect o =
  mempty &
  (strokeWidth .~ Last (Just $ Num (o ^. #borderSize))) &
  (strokeColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #borderColor))) &
  (strokeOpacity ?~ realToFrac (opac $ o ^. #borderColor)) &
  (fillColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #color))) &
  (fillOpacity ?~ realToFrac (opac $ o ^. #color))

daText :: () => TextStyle -> DrawAttributes
daText o =
  mempty &
  (fontSize .~ Last (Just $ Num (o ^. #size))) &
  (strokeWidth .~ Last (Just $ Num 0)) &
  (strokeColor .~ Last (Just FillNone)) &
  (fillColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #color))) &
  (fillOpacity ?~ realToFrac (opac $ o ^. #color)) &
  (textAnchor .~ Last (Just (toTextAnchor $ o ^. #anchor))) &
  maybe
      id
      (\(Point x y) -> transform ?~ [Translate x (-y)])
      (o ^. #translate)
  where
    toTextAnchor :: Anchor -> TextAnchor
    toTextAnchor AnchorMiddle = TextAnchorMiddle
    toTextAnchor AnchorStart = TextAnchorStart
    toTextAnchor AnchorEnd = TextAnchorEnd

daGlyph :: GlyphStyle -> DrawAttributes
daGlyph o =
  mempty &
  (strokeWidth .~ Last (Just $ Num (o ^. #borderSize))) &
  (strokeColor .~
   Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #borderColor))) &
  (strokeOpacity ?~ realToFrac (opac $ o ^. #borderColor)) &
  (fillColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #color))) &
  (fillOpacity ?~ realToFrac (opac $ o ^. #color)) &
  maybe id (\(Point x y) -> transform ?~ [Translate x (-y)]) (o ^. #translate)

daLine :: LineStyle -> DrawAttributes
daLine o =
  mempty &
  (strokeWidth .~ Last (Just $ Num (o ^. #width))) &
  (strokeColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #color))) &
  (strokeOpacity ?~ realToFrac (opac $ o ^. #color)) &
  (fillColor .~ Last (Just FillNone)) &
  maybe id (\x -> strokeLineCap .~ Last (Just $ fromLineCap' x))
  (o ^. #linecap) &
  maybe id (\x -> strokeLineJoin .~ Last (Just $ fromLineJoin' x))
  (o ^. #linejoin) &
  maybe id (\x -> strokeOffset .~ Last (Just $ Num x))
  (o ^. #dashoffset) &
  maybe id (\xs -> strokeDashArray .~ Last (Just (Num <$> xs)))
  (o ^. #dasharray)

fromLineCap' :: LineCap -> Cap
fromLineCap' LineCapButt = CapButt
fromLineCap' LineCapRound = CapRound
fromLineCap' LineCapSquare = CapSquare

fromLineJoin' :: C.LineJoin -> SvgTree.LineJoin
fromLineJoin' LineJoinMiter = JoinMiter
fromLineJoin' LineJoinBevel = JoinBevel
fromLineJoin' LineJoinRound = JoinRound

daPath :: PathStyle -> DrawAttributes
daPath o =
  mempty &
  (strokeWidth .~ Last (Just $ Num (o ^. #borderSize))) &
  (strokeColor .~ Last
   (Just $ ColorRef (toPixelRGBA8 $ o ^. #borderColor))) &
  (strokeOpacity ?~ realToFrac (opac $ o ^. #borderColor)) &
  (fillColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #color))) &
  (fillOpacity ?~ realToFrac (opac $ o ^. #color))

-- * svg primitives

-- | Convert to reanimate color primitive.
toPixelRGBA8 :: Colour -> PixelRGBA8
toPixelRGBA8 (Colour r g b o) =
  PixelRGBA8
    (fromIntegral (floor $ r * 256 :: Int))
    (fromIntegral (floor $ g * 256 :: Int))
    (fromIntegral (floor $ b * 256 :: Int))
    (fromIntegral (floor $ o * 256 :: Int))

-- | convert a point to the svg co-ordinate system
-- The svg coordinate system has the y-axis going from top to bottom.
pointSvg :: Point Double -> (Number, Number)
pointSvg (Point x y) = (Num x, Num (- y))

-- | A DrawAttributes to rotate around a point by x degrees.
rotatePDA :: (HasDrawAttributes s) => Double -> Point Double -> s -> s
rotatePDA a (Point x y) s = s & transform %~ (Just . maybe r (<> r))
  where
    r = [Rotate (-a*180/pi) (Just (x, -y))]

-- | A DrawAttributes to translate by a Point.
translateDA :: (HasDrawAttributes s) => Point Double -> s -> s
translateDA (Point x' y') =
  transform
    %~ (\x -> Just $ maybe [Translate x' (- y')] (<> [Translate x' (- y')]) x)

-- | convert a Rect to the svg co-ordinate system
rectSvg :: Rect Double -> Rectangle -> Rectangle
rectSvg (Rect x z y w) =
  (rectUpperLeftCorner .~ (Num x, Num (- w)))
    . (rectWidth .~ Just (Num (z - x)))
    . (rectHeight .~ Just (Num (w - y)))