packages feed

granite-0.5.0.0: test/SpecPropSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module SpecPropSpec (spec) where

import Data.Text qualified as Text

import Granite.Color (Color (..))
import Granite.Spec
import Test.Hspec

-- A representative chart that exercises every constructor we expect to
-- be JSON-faithful: layers with mappings + stats + positions, scales of
-- various transforms, polar coord, facet wrap, theme, responsive size.
representativeChart :: Chart
representativeChart =
    emptyChart
        { chartData =
            fromColumns
                [ ("x", ColNum [1, 2, 3, 4, 5])
                , ("y", ColNum [1.1, 4.0, 9.2, 16.05, 25.5])
                , ("group", ColCat ["A", "A", "B", "B", "B"])
                ,
                    ( "when"
                    , ColTime
                        [1700000000000, 1700000003600, 1700000007200, 1700000010800, 1700000014400]
                    )
                , ("active", ColBool [True, False, True, False, True])
                ]
        , chartLayers =
            [ (defLayer GeomPoint)
                { layerMapping =
                    emptyMapping
                        { aesX = Just (ColumnRef "x")
                        , aesY = Just (ColumnRef "y")
                        , aesColor = Just (ColumnRef "group")
                        }
                , layerAesDef = emptyAesDefaults{defSize = Just 3}
                }
            , (defLayer GeomLine)
                { layerMapping =
                    emptyMapping{aesX = Just (ColumnRef "x"), aesY = Just (ColumnRef "y")}
                , layerStat = StatSmooth (SmoothLoess 0.5)
                }
            , (defLayer GeomBar)
                { layerMapping = emptyMapping{aesX = Just (ColumnRef "group")}
                , layerStat = StatCount
                , layerPosition = PosDodge 0.1
                }
            ]
        , chartScales =
            defScales
                { scaleX = SLog Base10 defScaleOpts{scaleBreaks = BreaksCount 5}
                , scaleY = SSqrt defScaleOpts{scaleLabels = FormatPrecision 2}
                , scaleColor =
                    Just (SColorDiscrete [NamedColor BrightBlue, Hex "#ff0000", RGB 0 128 0])
                }
        , chartCoord = CoordPolar ThetaX 0 PolarCCW
        , chartFacet = FacetWrap (ColumnRef "group") (Just 2) Nothing ScalesFreeY
        , chartTitle = Just "Round-trip fixture"
        , chartSize = SizeResponsive 1.5
        }

spec :: Spec
spec = describe "Granite.Spec" $ do
    describe "show / read round trip (sentinel for JSON faithfulness)" $ do
        it "round-trips emptyChart" $ do
            (read (show emptyChart) :: Chart) `shouldBe` emptyChart

        it "round-trips a representative chart" $ do
            (read (show representativeChart) :: Chart) `shouldBe` representativeChart

        it "double-encoding is idempotent" $ do
            let s = show representativeChart
            show (read s :: Chart) `shouldBe` s

    describe "DataFrame" $ do
        it "addColumn preserves order" $ do
            let f =
                    addColumn "z" (ColNum [3]) $
                        addColumn "y" (ColNum [2]) $
                            addColumn "x" (ColNum [1]) emptyFrame
            columnNames f `shouldBe` ["x", "y", "z"]

        it "addColumn replaces existing in place" $ do
            let f0 =
                    addColumn "y" (ColNum [10]) $
                        addColumn "x" (ColNum [1]) emptyFrame
                f1 = addColumn "x" (ColNum [99]) f0
            columnNames f1 `shouldBe` ["x", "y"]
            lookupColumn "x" f1 `shouldBe` Just (ColNum [99])

        it "frameLength is the longest column" $ do
            let f =
                    addColumn "short" (ColNum [1]) $
                        addColumn "long" (ColNum [1, 2, 3]) emptyFrame
            frameLength f `shouldBe` 3

    describe "Granite.Format" $ do
        it "FormatPrecision 2 uses fixed notation" $
            runFormatter (FormatPrecision 2) 3.14159 `shouldBe` "3.14"
        it "FormatPercent 1 multiplies by 100" $
            runFormatter (FormatPercent 1) 0.345 `shouldBe` "34.5%"
        it "FormatComma groups thousands" $
            runFormatter FormatComma 1234567 `shouldBe` "1,234,567"
        it "FormatSI uses k for thousands" $
            Text.isInfixOf "k" (runFormatter FormatSI 1500) `shouldBe` True
        it "FormatSI uses M for millions" $
            Text.isInfixOf "M" (runFormatter FormatSI 2_500_000) `shouldBe` True