chart-svg-0.4.1.0: src/Chart/Style.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Stylistic or syntactical options for chart elements.
module Chart.Style
( -- * RectStyle
RectStyle (..),
defaultRectStyle,
blob,
clear,
border,
-- * TextStyle
TextStyle (..),
defaultTextStyle,
styleBoxText,
ScaleX (..),
-- * GlyphStyle
GlyphStyle (..),
defaultGlyphStyle,
styleBoxGlyph,
gpalette1,
ScaleBorder (..),
GlyphShape (..),
glyphText,
-- * LineStyle
LineStyle (..),
defaultLineStyle,
LineCap (..),
fromLineCap,
toLineCap,
LineJoin (..),
fromLineJoin,
toLineJoin,
Anchor (..),
fromAnchor,
toAnchor,
-- * PathStyle
PathStyle (..),
defaultPathStyle,
)
where
import Chart.Data
import Data.ByteString (ByteString)
import Data.Colour
import Data.List qualified as List
import Data.Maybe
import Data.Path
import Data.Path.Parser
import Data.String
import Data.Text (Text)
import Data.Text qualified as Text
import GHC.Generics
import Optics.Core
import Prelude
-- $setup
--
-- >>> :set -XOverloadedLabels
-- >>> :set -XOverloadedStrings
-- >>> import Chart
-- >>> import Optics.Core
-- | Rectangle styling
--
-- >>> defaultRectStyle
-- RectStyle {borderSize = 1.0e-2, borderColor = Colour 0.02 0.29 0.48 1.00, color = Colour 0.02 0.73 0.80 0.10}
--
-- 
data RectStyle = RectStyle
{ borderSize :: Double,
borderColor :: Colour,
color :: Colour
}
deriving (Show, Eq, Generic)
-- | the style
defaultRectStyle :: RectStyle
defaultRectStyle = RectStyle 0.01 (palette1a 1 1) (palette1a 0 0.1)
-- | solid rectangle, no border
--
-- >>> blob black
-- RectStyle {borderSize = 0.0, borderColor = Colour 0.00 0.00 0.00 0.00, color = Colour 0.00 0.00 0.00 1.00}
blob :: Colour -> RectStyle
blob = RectStyle 0 transparent
-- | transparent rect
--
-- >>> clear
-- RectStyle {borderSize = 0.0, borderColor = Colour 0.00 0.00 0.00 0.00, color = Colour 0.00 0.00 0.00 0.00}
clear :: RectStyle
clear = RectStyle 0 transparent transparent
-- | transparent rectangle, with border
--
-- >>> border 0.01 transparent
-- RectStyle {borderSize = 1.0e-2, borderColor = Colour 0.00 0.00 0.00 0.00, color = Colour 0.00 0.00 0.00 0.00}
border :: Double -> Colour -> RectStyle
border s c = RectStyle s c transparent
-- | Text styling
--
-- >>> defaultTextStyle
-- TextStyle {size = 0.12, color = Colour 0.05 0.05 0.05 1.00, anchor = AnchorMiddle, hsize = 0.45, vsize = 1.1, vshift = -0.25, rotation = Nothing, scalex = ScaleX, frame = Nothing}
data TextStyle = TextStyle
{ size :: Double,
color :: Colour,
anchor :: Anchor,
hsize :: Double,
vsize :: Double,
vshift :: Double,
rotation :: Maybe Double,
scalex :: ScaleX,
frame :: Maybe RectStyle
}
deriving (Show, Eq, Generic)
-- | Whether to scale text given X-axis scaling
data ScaleX = ScaleX | NoScaleX deriving (Eq, Show, Generic)
-- | position anchor
data Anchor = AnchorMiddle | AnchorStart | AnchorEnd deriving (Eq, Show, Generic)
-- | text
fromAnchor :: (IsString s) => Anchor -> s
fromAnchor AnchorMiddle = "Middle"
fromAnchor AnchorStart = "Start"
fromAnchor AnchorEnd = "End"
-- | from text
toAnchor :: (Eq s, IsString s) => s -> Anchor
toAnchor "Middle" = AnchorMiddle
toAnchor "Start" = AnchorStart
toAnchor "End" = AnchorEnd
toAnchor _ = AnchorMiddle
-- | the offical text style
defaultTextStyle :: TextStyle
defaultTextStyle =
TextStyle 0.12 dark AnchorMiddle 0.45 1.1 (-0.25) Nothing ScaleX Nothing
-- | the extra area from text styling
styleBoxText ::
TextStyle ->
Text ->
Point Double ->
Rect Double
styleBoxText o t p = mpad $ move p $ maybe flat (`rotationBound` flat) (o ^. #rotation)
where
flat = Rect ((-x' / 2.0) + x' * a') (x' / 2 + x' * a') (-y' / 2 + n1') (y' / 2 + n1')
s = o ^. #size
h = o ^. #hsize
v = o ^. #vsize
n1 = o ^. #vshift
x' = s * h * fromIntegral (Text.length t)
y' = s * v
n1' = -s * n1
a' = case o ^. #anchor of
AnchorStart -> 0.5
AnchorEnd -> -0.5
AnchorMiddle -> 0.0
mpad = case view #frame o of
Nothing -> id
Just f -> padRect (0.5 * view #borderSize f * view #size o)
-- | Glyph styling
--
-- >>> defaultGlyphStyle
-- GlyphStyle {size = 3.0e-2, color = Colour 0.02 0.73 0.80 0.20, borderColor = Colour 0.02 0.29 0.48 1.00, borderSize = 3.0e-3, shape = SquareGlyph, rotation = Nothing, translate = Nothing}
--
-- 
data GlyphStyle = GlyphStyle
{ -- | glyph radius
size :: Double,
-- | fill color
color :: Colour,
-- | stroke color
borderColor :: Colour,
-- | stroke width (adds a bit to the bounding box)
borderSize :: Double,
shape :: GlyphShape,
rotation :: Maybe Double,
translate :: Maybe (Point Double)
}
deriving (Show, Eq, Generic)
-- | the offical glyph style
defaultGlyphStyle :: GlyphStyle
defaultGlyphStyle =
GlyphStyle
0.03
(palette1a 0 0.2)
(set lightness' 0.4 $ palette1a 1 1)
0.003
SquareGlyph
Nothing
Nothing
-- | Should glyph borders be scaled?
data ScaleBorder = ScaleBorder | NoScaleBorder deriving (Show, Eq, Generic)
-- | glyph shapes
data GlyphShape
= CircleGlyph
| SquareGlyph
| EllipseGlyph Double
| RectSharpGlyph Double
| RectRoundedGlyph Double Double Double
| -- | line width is determined by borderSize
TriangleGlyph (Point Double) (Point Double) (Point Double)
| VLineGlyph
| HLineGlyph
| PathGlyph ByteString ScaleBorder
deriving (Show, Eq, Generic)
-- | textifier
glyphText :: GlyphShape -> ByteString
glyphText sh =
case sh of
CircleGlyph -> "Circle"
SquareGlyph -> "Square"
TriangleGlyph {} -> "Triangle"
EllipseGlyph _ -> "Ellipse"
RectSharpGlyph _ -> "RectSharp"
RectRoundedGlyph {} -> "RectRounded"
VLineGlyph -> "VLine"
HLineGlyph -> "HLine"
PathGlyph _ _ -> "Path"
-- | the extra area from glyph styling
styleBoxGlyph :: GlyphStyle -> Rect Double
styleBoxGlyph s = move p' $
rot' $
sw $ case sh of
CircleGlyph -> (sz *) <$> one
SquareGlyph -> (sz *) <$> one
EllipseGlyph a -> scale (Point sz (a * sz)) one
RectSharpGlyph a -> scale (Point sz (a * sz)) one
RectRoundedGlyph a _ _ -> scale (Point sz (a * sz)) one
VLineGlyph -> scale (Point (s ^. #borderSize) sz) one
HLineGlyph -> scale (Point sz (s ^. #borderSize)) one
TriangleGlyph a b c -> (sz *) <$> unsafeSpace1 ([a, b, c] :: [Point Double])
PathGlyph path' _ -> maybe zero (fmap (sz *)) (pathBoxes . svgToPathData $ path')
where
sh = s ^. #shape
sz = s ^. #size
sw = padRect (0.5 * s ^. #borderSize)
p' = fromMaybe (Point 0.0 0.0) (s ^. #translate)
rot' = maybe id rotationBound (view #rotation s)
-- | Infinite list of glyph shapes
--
-- >>> gpalette1 0
-- CircleGlyph
gpalette1 :: Int -> GlyphShape
gpalette1 x = cycle gpalette1_ List.!! x
-- | finite list of glyphs
gpalette1_ :: [GlyphShape]
gpalette1_ =
[ CircleGlyph,
SquareGlyph,
RectSharpGlyph 0.75,
RectRoundedGlyph 0.75 0.01 0.01,
EllipseGlyph 0.75,
VLineGlyph,
HLineGlyph,
TriangleGlyph (Point 0.0 0.0) (Point 1 1) (Point 1 0),
PathGlyph "M0.05,-0.03660254037844387 A0.1 0.1 0.0 0 1 0.0,0.05 0.1 0.1 0.0 0 1 -0.05,-0.03660254037844387 0.1 0.1 0.0 0 1 0.05,-0.03660254037844387 Z" ScaleBorder
]
-- | line cap style
data LineCap = LineCapButt | LineCapRound | LineCapSquare deriving (Eq, Show, Generic)
-- | textifier
fromLineCap :: (IsString s) => LineCap -> s
fromLineCap LineCapButt = "butt"
fromLineCap LineCapRound = "round"
fromLineCap LineCapSquare = "square"
-- | readifier
toLineCap :: (Eq s, IsString s) => s -> LineCap
toLineCap "butt" = LineCapButt
toLineCap "round" = LineCapRound
toLineCap "square" = LineCapSquare
toLineCap _ = LineCapButt
-- | line cap style
data LineJoin = LineJoinMiter | LineJoinBevel | LineJoinRound deriving (Eq, Show, Generic)
-- | textifier
fromLineJoin :: (IsString s) => LineJoin -> s
fromLineJoin LineJoinMiter = "miter"
fromLineJoin LineJoinBevel = "bevel"
fromLineJoin LineJoinRound = "round"
-- | readifier
toLineJoin :: (Eq s, IsString s) => s -> LineJoin
toLineJoin "miter" = LineJoinMiter
toLineJoin "bevel" = LineJoinBevel
toLineJoin "round" = LineJoinRound
toLineJoin _ = LineJoinMiter
-- | line style
--
-- >>> defaultLineStyle
-- LineStyle {size = 1.2e-2, color = Colour 0.05 0.05 0.05 1.00, linecap = Nothing, linejoin = Nothing, dasharray = Nothing, dashoffset = Nothing}
--
-- 
--
-- See also <https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute>
data LineStyle = LineStyle
{ size :: Double,
color :: Colour,
linecap :: Maybe LineCap,
linejoin :: Maybe LineJoin,
dasharray :: Maybe [Double],
dashoffset :: Maybe Double
}
deriving (Show, Eq, Generic)
-- | the official default line style
defaultLineStyle :: LineStyle
defaultLineStyle = LineStyle 0.012 dark Nothing Nothing Nothing Nothing
-- | Path styling
--
-- >>> defaultPathStyle
-- PathStyle {borderSize = 1.0e-2, borderColor = Colour 0.02 0.29 0.48 1.00, color = Colour 0.66 0.07 0.55 1.00}
data PathStyle = PathStyle
{ borderSize :: Double,
borderColor :: Colour,
color :: Colour
}
deriving (Show, Eq, Generic)
-- | the style
defaultPathStyle :: PathStyle
defaultPathStyle =
PathStyle 0.01 (palette1 1) (palette1 2)