packages feed

granite 0.6.0.0 → 0.7.0.0

raw patch · 53 files changed

+1303/−880 lines, 53 filesdep −directorydep −filepathdep ~granitePVP ok

version bump matches the API change (PVP)

Dependencies removed: directory, filepath

Dependency ranges changed: granite

API changes (from Hackage documentation)

- Granite: Black :: Color
- Granite: Blue :: Color
- Granite: BrightBlack :: Color
- Granite: BrightBlue :: Color
- Granite: BrightCyan :: Color
- Granite: BrightGreen :: Color
- Granite: BrightMagenta :: Color
- Granite: BrightRed :: Color
- Granite: BrightWhite :: Color
- Granite: BrightYellow :: Color
- Granite: Cyan :: Color
- Granite: Default :: Color
- Granite: Green :: Color
- Granite: Magenta :: Color
- Granite: Red :: Color
- Granite: White :: Color
- Granite: Yellow :: Color
- Granite.Color: Black :: Color
- Granite.Color: Blue :: Color
- Granite.Color: BrightBlack :: Color
- Granite.Color: BrightBlue :: Color
- Granite.Color: BrightCyan :: Color
- Granite.Color: BrightGreen :: Color
- Granite.Color: BrightMagenta :: Color
- Granite.Color: BrightRed :: Color
- Granite.Color: BrightWhite :: Color
- Granite.Color: BrightYellow :: Color
- Granite.Color: Cyan :: Color
- Granite.Color: Default :: Color
- Granite.Color: Green :: Color
- Granite.Color: Magenta :: Color
- Granite.Color: Red :: Color
- Granite.Color: White :: Color
- Granite.Color: Yellow :: Color
- Granite.String: Black :: Color
- Granite.String: Blue :: Color
- Granite.String: BrightBlack :: Color
- Granite.String: BrightBlue :: Color
- Granite.String: BrightCyan :: Color
- Granite.String: BrightGreen :: Color
- Granite.String: BrightMagenta :: Color
- Granite.String: BrightRed :: Color
- Granite.String: BrightWhite :: Color
- Granite.String: BrightYellow :: Color
- Granite.String: Cyan :: Color
- Granite.String: Default :: Color
- Granite.String: Green :: Color
- Granite.String: Magenta :: Color
- Granite.String: Red :: Color
- Granite.String: White :: Color
- Granite.String: Yellow :: Color
- Granite.Svg: Black :: Color
- Granite.Svg: Blue :: Color
- Granite.Svg: BrightBlack :: Color
- Granite.Svg: BrightBlue :: Color
- Granite.Svg: BrightCyan :: Color
- Granite.Svg: BrightGreen :: Color
- Granite.Svg: BrightMagenta :: Color
- Granite.Svg: BrightRed :: Color
- Granite.Svg: BrightWhite :: Color
- Granite.Svg: BrightYellow :: Color
- Granite.Svg: Cyan :: Color
- Granite.Svg: Default :: Color
- Granite.Svg: Green :: Color
- Granite.Svg: Magenta :: Color
- Granite.Svg: Red :: Color
- Granite.Svg: White :: Color
- Granite.Svg: Yellow :: Color
+ Granite: Color :: !Word8 -> !Word8 -> !Word8 -> Color
+ Granite: pattern Black :: Color
+ Granite: pattern Blue :: Color
+ Granite: pattern BrightBlack :: Color
+ Granite: pattern BrightBlue :: Color
+ Granite: pattern BrightCyan :: Color
+ Granite: pattern BrightGreen :: Color
+ Granite: pattern BrightMagenta :: Color
+ Granite: pattern BrightRed :: Color
+ Granite: pattern BrightWhite :: Color
+ Granite: pattern BrightYellow :: Color
+ Granite: pattern Cyan :: Color
+ Granite: pattern Default :: Color
+ Granite: pattern Green :: Color
+ Granite: pattern Magenta :: Color
+ Granite: pattern Red :: Color
+ Granite: pattern White :: Color
+ Granite: pattern Yellow :: Color
+ Granite.Color: Color :: !Word8 -> !Word8 -> !Word8 -> Color
+ Granite.Color: parseHex :: Text -> Maybe Color
+ Granite.Color: pattern Black :: Color
+ Granite.Color: pattern Blue :: Color
+ Granite.Color: pattern BrightBlack :: Color
+ Granite.Color: pattern BrightBlue :: Color
+ Granite.Color: pattern BrightCyan :: Color
+ Granite.Color: pattern BrightGreen :: Color
+ Granite.Color: pattern BrightMagenta :: Color
+ Granite.Color: pattern BrightRed :: Color
+ Granite.Color: pattern BrightWhite :: Color
+ Granite.Color: pattern BrightYellow :: Color
+ Granite.Color: pattern Cyan :: Color
+ Granite.Color: pattern Default :: Color
+ Granite.Color: pattern Green :: Color
+ Granite.Color: pattern Magenta :: Color
+ Granite.Color: pattern Red :: Color
+ Granite.Color: pattern White :: Color
+ Granite.Color: pattern Yellow :: Color
+ Granite.String: Color :: !Word8 -> !Word8 -> !Word8 -> Color
+ Granite.String: pattern Black :: Color
+ Granite.String: pattern Blue :: Color
+ Granite.String: pattern BrightBlack :: Color
+ Granite.String: pattern BrightBlue :: Color
+ Granite.String: pattern BrightCyan :: Color
+ Granite.String: pattern BrightGreen :: Color
+ Granite.String: pattern BrightMagenta :: Color
+ Granite.String: pattern BrightRed :: Color
+ Granite.String: pattern BrightWhite :: Color
+ Granite.String: pattern BrightYellow :: Color
+ Granite.String: pattern Cyan :: Color
+ Granite.String: pattern Default :: Color
+ Granite.String: pattern Green :: Color
+ Granite.String: pattern Magenta :: Color
+ Granite.String: pattern Red :: Color
+ Granite.String: pattern White :: Color
+ Granite.String: pattern Yellow :: Color
+ Granite.Svg: Color :: !Word8 -> !Word8 -> !Word8 -> Color
+ Granite.Svg: pattern Black :: Color
+ Granite.Svg: pattern Blue :: Color
+ Granite.Svg: pattern BrightBlack :: Color
+ Granite.Svg: pattern BrightBlue :: Color
+ Granite.Svg: pattern BrightCyan :: Color
+ Granite.Svg: pattern BrightGreen :: Color
+ Granite.Svg: pattern BrightMagenta :: Color
+ Granite.Svg: pattern BrightRed :: Color
+ Granite.Svg: pattern BrightWhite :: Color
+ Granite.Svg: pattern BrightYellow :: Color
+ Granite.Svg: pattern Cyan :: Color
+ Granite.Svg: pattern Default :: Color
+ Granite.Svg: pattern Green :: Color
+ Granite.Svg: pattern Magenta :: Color
+ Granite.Svg: pattern Red :: Color
+ Granite.Svg: pattern White :: Color
+ Granite.Svg: pattern Yellow :: Color

Files

CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for granite +## 0.7.0.0 -- 2026-05-24++* `Color` is now a 24-bit RGB value, and categorical colour mappings are honoured for points.+* Scatter plots now respect point colours.++ ## 0.5.0.0 -- 2026-05-13  A major refactor introducing a declarative, Grammar-of-Graphics-style
− app/GenerateTutorial.hs
@@ -1,796 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--{- |-Regenerate @docs/tutorial.md@ from @docs/base/tutorial.md@ by-rendering each @\<!-- chart: SLUG -->@ marker to an SVG under-@docs/images/@ and inserting the image link after the code block.--}-module Main where--import Control.Monad (forM_)-import Data.List qualified as List-import Data.Text (Text)-import Data.Text qualified as Text-import Data.Text.IO qualified as Text.IO-import System.Directory (createDirectoryIfMissing)-import System.FilePath ((</>))--import Granite.Color (Color (..))-import Granite.Data.Frame-import Granite.Render.Pipeline (renderChartSvg)-import Granite.Spec--baseFile, outputFile, imagesDir :: FilePath-baseFile = "docs/base/tutorial.md"-outputFile = "docs/tutorial.md"-imagesDir = "docs/images"--main :: IO ()-main = do-    createDirectoryIfMissing True imagesDir-    src <- Text.IO.readFile baseFile-    let rendered = renderTutorial src-    Text.IO.writeFile outputFile rendered-    forM_ fixtures $ \(slug, chart) -> do-        let svgPath = imagesDir </> Text.unpack slug <> ".svg"-        Text.IO.writeFile svgPath (renderChartSvg chart)-    putStrLn $ "Wrote " <> outputFile-    putStrLn $ "Wrote " <> show (length fixtures) <> " SVGs under " <> imagesDir--renderTutorial :: Text -> Text-renderTutorial src = Text.unlines (go (Text.lines src))-  where-    go :: [Text] -> [Text]-    go [] = []-    go (line : rest) = case chartSlug line of-        Just slug ->-            let (block, afterBlock) = takeCodeBlock rest-                output = renderChartOutput slug-             in line : block <> output <> go afterBlock-        Nothing -> line : go rest--chartSlug :: Text -> Maybe Text-chartSlug ln =-    let stripped = Text.strip ln-     in case Text.stripPrefix "<!-- chart:" stripped of-            Just rest -> case Text.stripSuffix "-->" (Text.strip rest) of-                Just slug -> Just (Text.strip slug)-                Nothing -> Nothing-            Nothing -> Nothing--takeCodeBlock :: [Text] -> ([Text], [Text])-takeCodeBlock = goOpen []-  where-    goOpen acc [] = (reverse acc, [])-    goOpen acc (ln : rest)-        | Text.isPrefixOf "```" (Text.stripStart ln) =-            goClose (ln : acc) rest-        | otherwise = goOpen (ln : acc) rest--    goClose acc [] = (reverse acc, [])-    goClose acc (ln : rest)-        | Text.isPrefixOf "```" (Text.stripStart ln) =-            (reverse (ln : acc), rest)-        | otherwise = goClose (ln : acc) rest--renderChartOutput :: Text -> [Text]-renderChartOutput slug = case lookup slug fixtures of-    Nothing ->-        [ ""-        , "> _(no fixture for chart slug `" <> slug <> "` — skipped)_"-        ]-    Just _chart ->-        let imgRel = "images/" <> slug <> ".svg"-         in [ ""-            , "![" <> slug <> "](" <> imgRel <> ")"-            ]--fixtures :: [(Text, Chart)]-fixtures =-    [ ("scatter", scatterChart)-    , ("line", lineChart)-    , ("bar", barChart)-    , ("bar-horizontal", barHorizontal)-    , ("bar-stacked", barStacked)-    , ("bar-grouped", barGrouped)-    , ("pie", pieChart)-    , ("area", areaChart)-    , ("histogram", histChart)-    , ("boxplot", boxChart)-    , ("errorbar", errorbarChart)-    , ("density", densityChart)-    , ("distplot", distplotChart)-    , ("heatmap", heatmapChart)-    , ("heatmap-annotated", heatmapAnnotated)-    , ("funnel", funnelChart)-    , ("waterfall", waterfallChart)-    , ("polar", polarChart)-    , ("facet", facetChart)-    , ("log-y", logYChart)-    , ("layer-bar-line", barLineChart)-    , ("layer-scatter-radius", scatterRadiusChart)-    , ("layer-scatter-fit", scatterFitChart)-    , ("layer-line-ribbon", lineRibbonChart)-    ]--scatterChart :: Chart-scatterChart =-    let xs = [0, 1, 2, 3, 4] :: [Double]-        ys = [0, 1, 4, 9, 16] :: [Double]-        df = fromColumns [("x", ColNum xs), ("y", ColNum ys)]-        layer =-            (defLayer GeomPoint)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "y")-                        }-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartTitle = Just "y = x^2"-            , chartSize = SizeChars 40 14-            }--lineChart :: Chart-lineChart =-    let xs = [0, 0.5 .. 6.0] :: [Double]-        ys = map sin xs-        df = fromColumns [("x", ColNum xs), ("y", ColNum ys)]-        layer =-            (defLayer GeomLine)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "y")-                        }-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartTitle = Just "sin(x)"-            , chartSize = SizeChars 50 14-            }--barChart :: Chart-barChart =-    let xs = ["A", "B", "C", "D"]-        ys = [12, 7, 18, 9] :: [Double]-        df = fromColumns [("x", ColCat xs), ("y", ColNum ys)]-        layer =-            (defLayer GeomBar)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "y")-                        }-                , layerStat = StatIdentity-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartTitle = Just "Bars"-            , chartSize = SizeChars 40 14-            }--barHorizontal :: Chart-barHorizontal =-    let xs = ["Alpha", "Bravo", "Charlie", "Delta", "Echo"]-        ys = [82, 67, 45, 33, 21] :: [Double]-        df = fromColumns [("item", ColCat xs), ("value", ColNum ys)]-        layer =-            (defLayer GeomBar)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "item")-                        , aesY = Just (ColumnRef "value")-                        }-                , layerStat = StatIdentity-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartCoord = CoordFlip-            , chartTitle = Just "Top 5 (horizontal)"-            , chartSize = SizeChars 60 16-            }--barStackedFrame :: DataFrame-barStackedFrame =-    let quarters = concat [replicate 3 q | q <- ["Q1", "Q2", "Q3", "Q4"]]-        products = take 12 (cycle ["Widgets", "Gadgets", "Gizmos"])-        sales = [12, 8, 4, 15, 10, 6, 18, 12, 8, 22, 14, 10] :: [Double]-     in fromColumns-            [ ("quarter", ColCat quarters)-            , ("product", ColCat products)-            , ("sales", ColNum sales)-            ]--barStacked :: Chart-barStacked =-    let layer =-            (defLayer GeomBar)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "quarter")-                        , aesY = Just (ColumnRef "sales")-                        , aesGroup = Just (ColumnRef "product")-                        , aesFill = Just (ColumnRef "product")-                        }-                , layerStat = StatIdentity-                , layerPosition = PosStack-                }-     in emptyChart-            { chartData = barStackedFrame-            , chartLayers = [layer]-            , chartTitle = Just "Sales by quarter (stacked)"-            , chartSize = SizeChars 60 16-            }--barGrouped :: Chart-barGrouped =-    barStacked-        { chartLayers =-            [ (List.head (chartLayers barStacked))-                { layerPosition = PosDodge 0.25-                }-            ]-        , chartTitle = Just "Sales by quarter (grouped)"-        }--pieChart :: Chart-pieChart =-    let df =-            fromColumns-                [ ("slice", ColCat ["A", "B", "C", "D"])-                , ("value", ColNum [40, 25, 20, 15])-                ]-        layer =-            (defLayer GeomArc)-                { layerMapping = emptyMapping{aesY = Just (ColumnRef "value")}-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartTitle = Just "Pie"-            , chartSize = SizeChars 30 16-            }--areaChart :: Chart-areaChart =-    let xs = [0 .. 12] :: [Double]-        ymax = [sin (x / 2) + 2 | x <- xs]-        zeros = replicate (length xs) 0 :: [Double]-        df =-            fromColumns-                [ ("x", ColNum xs)-                , ("ymin", ColNum zeros)-                , ("ymax", ColNum ymax)-                ]-        layer =-            (defLayer GeomRibbon)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesYmin = Just (ColumnRef "ymin")-                        , aesYmax = Just (ColumnRef "ymax")-                        }-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartTitle = Just "Filled area"-            , chartSize = SizeChars 56 14-            }--histChart :: Chart-histChart =-    let raw = take 50 (cycle [1.0, 1.2, 1.8, 2.0, 2.3, 2.7, 3.1, 3.4, 3.8, 4.2])-        df = fromColumns [("x", ColNum raw)]-        layer =-            (defLayer GeomHistogram)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "count")-                        }-                , layerStat = StatBin (BinByCount 8)-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartTitle = Just "Histogram"-            , chartSize = SizeChars 44 14-            }--boxChart :: Chart-boxChart =-    let pts =-            [("A", v) | v <- [1, 2, 3, 4, 5, 6, 7, 8, 9 :: Double]]-                <> [("B", v) | v <- [3, 4, 5, 6, 7, 8, 9, 10, 11]]-                <> [("C", v) | v <- [5, 6, 7, 8, 9, 10, 11, 12, 13]]-        df =-            fromColumns-                [ ("g", ColCat [g | (g, _) <- pts])-                , ("y", ColNum [v | (_, v) <- pts])-                ]-        layer =-            (defLayer GeomBoxplot)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "g")-                        , aesY = Just (ColumnRef "y")-                        }-                , layerStat = StatBoxplot-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartTitle = Just "Boxplot"-            , chartSize = SizeChars 40 16-            }--errorbarChart :: Chart-errorbarChart =-    let xs = [1 .. 5] :: [Double]-        ys = [2.1, 3.5, 5.2, 4.7, 6.1] :: [Double]-        los = [1.5, 2.8, 4.6, 4.0, 5.3] :: [Double]-        his = [2.7, 4.2, 5.8, 5.4, 6.9] :: [Double]-        df =-            fromColumns-                [ ("x", ColNum xs)-                , ("y", ColNum ys)-                , ("lo", ColNum los)-                , ("hi", ColNum his)-                ]-        m =-            emptyMapping-                { aesX = Just (ColumnRef "x")-                , aesY = Just (ColumnRef "y")-                , aesYmin = Just (ColumnRef "lo")-                , aesYmax = Just (ColumnRef "hi")-                }-        bars = (defLayer GeomErrorbar){layerMapping = m, layerStat = StatIdentity}-        pts = (defLayer GeomPoint){layerMapping = m, layerStat = StatIdentity}-     in emptyChart-            { chartData = df-            , chartLayers = [bars, pts]-            , chartTitle = Just "Measurements +/- CI"-            , chartSize = SizeChars 56 14-            }--densitySample :: [Double]-densitySample =-    [ 1.0-    , 1.2-    , 1.3-    , 1.5-    , 1.7-    , 2.0-    , 2.1-    , 2.3-    , 2.5-    , 2.8-    , 3.0-    , 3.1-    , 3.3-    , 3.7-    , 3.9-    , 4.0-    ]--densityChart :: Chart-densityChart =-    let df = fromColumns [("x", ColNum densitySample)]-        layer =-            (defLayer GeomDensity)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "density")-                        }-                , layerStat = StatDensity-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartTitle = Just "Density (KDE)"-            , chartSize = SizeChars 56 14-            }--distplotChart :: Chart-distplotChart =-    let df = fromColumns [("x", ColNum densitySample)]-        hist =-            (defLayer GeomHistogram)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "count")-                        }-                , layerStat = StatBin (BinByCount 8)-                }-        kde =-            (defLayer GeomDensity)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "density")-                        }-                , layerStat = StatDensity-                }-     in emptyChart-            { chartData = df-            , chartLayers = [hist, kde]-            , chartTitle = Just "Distribution"-            , chartSize = SizeChars 56 16-            }--heatmapCoords :: [(Double, Double, Double)]-heatmapCoords =-    [ ( fromIntegral x-      , fromIntegral y-      , sin (fromIntegral x / 2) + cos (fromIntegral y / 2)-      )-    | x <- [0 .. 4 :: Int]-    , y <- [0 .. 4 :: Int]-    ]--heatmapChart :: Chart-heatmapChart =-    let df =-            fromColumns-                [ ("x", ColNum [x | (x, _, _) <- heatmapCoords])-                , ("y", ColNum [y | (_, y, _) <- heatmapCoords])-                , ("z", ColNum [z | (_, _, z) <- heatmapCoords])-                ]-        layer =-            (defLayer GeomTile)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "y")-                        , aesFill = Just (ColumnRef "z")-                        }-                , layerStat = StatIdentity-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartTitle = Just "Heatmap"-            , chartSize = SizeChars 48 16-            }--heatmapAnnotated :: Chart-heatmapAnnotated =-    let labels = [Text.pack (show (round (z * 10) :: Int)) | (_, _, z) <- heatmapCoords]-        df =-            fromColumns-                [ ("x", ColNum [x | (x, _, _) <- heatmapCoords])-                , ("y", ColNum [y | (_, y, _) <- heatmapCoords])-                , ("z", ColNum [z | (_, _, z) <- heatmapCoords])-                , ("label", ColCat labels)-                ]-        tile =-            (defLayer GeomTile)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "y")-                        , aesFill = Just (ColumnRef "z")-                        }-                , layerStat = StatIdentity-                }-        label =-            (defLayer GeomText)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "y")-                        , aesLabel = Just (ColumnRef "label")-                        }-                , layerStat = StatIdentity-                }-     in emptyChart-            { chartData = df-            , chartLayers = [tile, label]-            , chartTitle = Just "Annotated heatmap"-            , chartSize = SizeChars 48 16-            }--funnelChart :: Chart-funnelChart =-    let xs = ["Visited", "Signed up", "Confirmed", "Active", "Paid"]-        ys = [1000, 720, 480, 220, 120] :: [Double]-        df = fromColumns [("stage", ColCat xs), ("count", ColNum ys)]-        layer =-            (defLayer GeomBar)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "stage")-                        , aesY = Just (ColumnRef "count")-                        }-                , layerStat = StatIdentity-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartCoord = CoordFlip-            , chartTitle = Just "Sales funnel"-            , chartSize = SizeChars 60 14-            }--waterfallChart :: Chart-waterfallChart =-    let xs = ["Start", "Q1", "Q2", "Q3", "Refund", "Net"]-        ystart = [0, 100, 130, 180, 175, 0] :: [Double]-        yend = [100, 130, 180, 175, 195, 195] :: [Double]-        df =-            fromColumns-                [ ("x", ColCat xs)-                , ("y", ColNum yend)-                , ("__ybase", ColNum ystart)-                ]-        layer =-            (defLayer GeomBar)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "y")-                        }-                , layerStat = StatIdentity-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartTitle = Just "Waterfall"-            , chartSize = SizeChars 60 14-            }--polarChart :: Chart-polarChart =-    let nSteps = 32 :: Int-        pts =-            [ (theta, abs (sin (2 * theta)))-            | i <- [0 .. nSteps]-            , let theta = (fromIntegral i / fromIntegral nSteps) * 2 * pi-            ]-        df =-            fromColumns-                [ ("theta", ColNum [t | (t, _) <- pts])-                , ("r", ColNum [r | (_, r) <- pts])-                ]-        layer =-            (defLayer GeomLine)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "theta")-                        , aesY = Just (ColumnRef "r")-                        }-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartCoord = CoordPolar ThetaX 0 PolarCCW-            , chartTitle = Just "r = |sin(2 theta)|"-            , chartSize = SizeChars 40 20-            }--facetChart :: Chart-facetChart =-    let df =-            fromColumns-                [ ("x", ColNum [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3])-                , ("y", ColNum [1, 4, 9, 16, 0, 2, 4, 6, 5, 4, 3, 2])-                , ("series", ColCat (replicate 4 "A" <> replicate 4 "B" <> replicate 4 "C"))-                ]-        layer =-            (defLayer GeomPoint)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "y")-                        }-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartFacet = FacetWrap (ColumnRef "series") (Just 3) Nothing ScalesFixed-            , chartTitle = Just "Faceted by series"-            , chartSize = SizeChars 70 18-            }--logYChart :: Chart-logYChart =-    let xs = [1 .. 6] :: [Double]-        ys = [3, 30, 80, 200, 700, 2100] :: [Double]-        df = fromColumns [("x", ColNum xs), ("y", ColNum ys)]-        layer =-            (defLayer GeomPoint)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "y")-                        }-                }-     in emptyChart-            { chartData = df-            , chartLayers = [layer]-            , chartScales = defScales{scaleY = SLog Base10 defScaleOpts}-            , chartTitle = Just "Log Y"-            , chartSize = SizeChars 50 14-            }--barLineChart :: Chart-barLineChart =-    let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]-        sales = [12, 18, 15, 24, 20, 28] :: [Double]-        movavg = [12, 15, 16.5, 19.5, 22, 24] :: [Double]-        df =-            fromColumns-                [ ("month", ColCat months)-                , ("sales", ColNum sales)-                , ("avg", ColNum movavg)-                ]-        bars =-            (defLayer GeomBar)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "month")-                        , aesY = Just (ColumnRef "sales")-                        }-                , layerStat = StatIdentity-                }-        trend =-            (defLayer GeomLine)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "month")-                        , aesY = Just (ColumnRef "avg")-                        }-                , layerAesDef =-                    emptyAesDefaults-                        { defColor = Just (NamedColor BrightRed)-                        , defLineWidth = Just 2.5-                        }-                }-     in emptyChart-            { chartData = df-            , chartLayers = [bars, trend]-            , chartTitle = Just "Monthly sales + trend"-            , chartSize = SizeChars 60 14-            }--scatterRadiusChart :: Chart-scatterRadiusChart =-    let pts =-            [ (0.2, 0.5)-            , (0.8, 1.2)-            , (-0.5, 0.3)-            , (1.1, -0.2)-            , (1.8, 0.7)-            , (-0.3, -0.6)-            , (2.2, 1.5)-            , (1.5, -1.1)-            , (0.1, 2.1)-            , (2.5, 0.3)-            , (-1.0, 1.0)-            , (0.7, -0.4)-            ] ::-                [(Double, Double)]-        points =-            fromColumns-                [ ("lon", ColNum [x | (x, _) <- pts])-                , ("lat", ColNum [y | (_, y) <- pts])-                ]-        poi =-            fromColumns-                [ ("lon", ColNum [0.5])-                , ("lat", ColNum [0.5])-                ]-        radius =-            (defLayer GeomPoint)-                { layerData = Just poi-                , layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "lon")-                        , aesY = Just (ColumnRef "lat")-                        }-                , layerAesDef =-                    emptyAesDefaults-                        { defColor = Just (NamedColor BrightCyan)-                        , defSize = Just 60-                        , defAlpha = Just 0.25-                        }-                }-        scatter =-            (defLayer GeomPoint)-                { layerData = Just points-                , layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "lon")-                        , aesY = Just (ColumnRef "lat")-                        }-                }-     in emptyChart-            { chartLayers = [radius, scatter]-            , chartTitle = Just "Points near POI"-            , chartSize = SizeChars 50 18-            }--scatterFitChart :: Chart-scatterFitChart =-    let xs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] :: [Double]-        noise = [0.3, -0.5, 0.7, -0.2, 0.4, -0.6, 0.1, 0.3, -0.4, 0.5]-        ys = [2 * x + 1 + n | (x, n) <- zip xs noise]-        df = fromColumns [("x", ColNum xs), ("y", ColNum ys)]-        m =-            emptyMapping-                { aesX = Just (ColumnRef "x")-                , aesY = Just (ColumnRef "y")-                }-        points = (defLayer GeomPoint){layerMapping = m}-        fit =-            (defLayer GeomLine)-                { layerMapping = m-                , layerStat = StatSmooth SmoothLm-                , layerAesDef =-                    emptyAesDefaults-                        { defColor = Just (NamedColor BrightRed)-                        , defLineWidth = Just 2-                        }-                }-     in emptyChart-            { chartData = df-            , chartLayers = [points, fit]-            , chartTitle = Just "Scatter + OLS fit"-            , chartSize = SizeChars 50 14-            }--lineRibbonChart :: Chart-lineRibbonChart =-    let xs = [0, 0.5 .. 6.0] :: [Double]-        ys = map sin xs-        los = map (\x -> sin x - 0.3) xs-        his = map (\x -> sin x + 0.3) xs-        df =-            fromColumns-                [ ("x", ColNum xs)-                , ("y", ColNum ys)-                , ("lo", ColNum los)-                , ("hi", ColNum his)-                ]-        ribbon =-            (defLayer GeomRibbon)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesYmin = Just (ColumnRef "lo")-                        , aesYmax = Just (ColumnRef "hi")-                        }-                , layerAesDef =-                    emptyAesDefaults-                        { defColor = Just (NamedColor BrightCyan)-                        , defAlpha = Just 0.3-                        }-                }-        line =-            (defLayer GeomLine)-                { layerMapping =-                    emptyMapping-                        { aesX = Just (ColumnRef "x")-                        , aesY = Just (ColumnRef "y")-                        }-                , layerAesDef =-                    emptyAesDefaults-                        { defColor = Just (NamedColor BrightBlue)-                        , defLineWidth = Just 2-                        }-                }-     in emptyChart-            { chartData = df-            , chartLayers = [ribbon, line]-            , chartTitle = Just "sin(x) +/- 0.3 band"-            , chartSize = SizeChars 56 14-            }
granite.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               granite-version:            0.6.0.0+version:            0.7.0.0 synopsis:           Easy terminal plotting. description:        A terminal plotting library for quick and easy visualisation. license:            MIT@@ -10,7 +10,8 @@                      || ==9.6.7                      || ==9.8.4                      || ==9.10.3-                     || ==9.12.2+                     || ==9.12.4+                     || ==9.14.1  maintainer:         mschavinda@gmail.com copyright:          (c) 2024-2025 Michael Chavinda@@ -18,6 +19,8 @@ category:           Graphics build-type:         Simple extra-doc-files:    CHANGELOG.md README.md+extra-source-files: test/golden/*.txt+                    test/golden/*.svg  source-repository head   type:     git@@ -69,19 +72,7 @@     main-is:          Main.hs     build-depends:         base >=4 && <5,-        granite ^>= 0.6,-        text >= 1 && < 3-    hs-source-dirs:   app-    default-language: GHC2021--executable granite-tutorial-    import: ghc-options-    main-is:          GenerateTutorial.hs-    build-depends:-        base >=4 && <5,-        directory >= 1.3 && < 1.4,-        filepath >= 1.4 && < 1.6,-        granite ^>= 0.6,+        granite ^>= 0.7,         text >= 1 && < 3     hs-source-dirs:   app     default-language: GHC2021@@ -106,7 +97,7 @@      build-depends:         base >= 4.14 && < 5-        , granite ^>= 0.6+        , granite ^>= 0.7         , text >= 1.2 && < 3         , hspec >= 2.7 && < 2.12         , QuickCheck >= 2.14 && < 3
src/Granite/Color.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE Strict #-}  {- |@@ -7,12 +8,44 @@ License     : MIT Maintainer  : mschavinda@gmail.com -Color primitives shared by both the terminal and SVG backends:-the ANSI 16-color palette ('Color'), escape-code helpers, and the-hex mapping used by the SVG backend.+Color primitives shared by the terminal and SVG backends.++'Color' is a 24-bit RGB value. The classic ANSI palette names ('Black',+'BrightBlue', 'Default', …) are provided as bidirectional pattern synonyms+that expand to fixed RGB triples, so they may be used both as expressions+and in patterns exactly as before.++The two backends deliberately diverge on how a colour is realised:++  * the SVG backend renders the RGB exactly via 'colorHex';+  * the terminal backend approximates via 'ansiCode', which maps an+    arbitrary colour to the nearest of the 16 ANSI slots.++Because 'Color' is now a plain RGB triple, its /derived/ 'Show' prints+@Color r g b@ rather than the alias name. -} module Granite.Color (-    Color (..),+    Color (+        ..,+        Black,+        Red,+        Green,+        Yellow,+        Blue,+        Magenta,+        Cyan,+        White,+        BrightBlack,+        BrightRed,+        BrightGreen,+        BrightYellow,+        BrightBlue,+        BrightMagenta,+        BrightCyan,+        BrightWhite,+        Default+    ),+    parseHex,     ansiCode,     ansiOn,     ansiOff,@@ -22,30 +55,75 @@     pieColors, ) where +import Data.Char (digitToInt, isHexDigit)+import Data.List (minimumBy)+import Data.Ord (comparing) import Data.Text (Text) import Data.Text qualified as Text+import Data.Word (Word8)+import Numeric (showHex) --- | Supported ANSI colo(u)rs.-data Color-    = Default-    | Black-    | Red-    | Green-    | Yellow-    | Blue-    | Magenta-    | Cyan-    | White-    | BrightBlack-    | BrightRed-    | BrightGreen-    | BrightYellow-    | BrightBlue-    | BrightMagenta-    | BrightCyan-    | BrightWhite+-- | A 24-bit RGB colour.+data Color = Color !Word8 !Word8 !Word8     deriving (Eq, Show, Read) +-- The classic ANSI palette, as bidirectional pattern-synonym aliases over+-- fixed RGB triples (the historical 'colorHex' values).++pattern Black :: Color+pattern Black = Color 0x2c 0x3e 0x50++pattern Red :: Color+pattern Red = Color 0xc0 0x39 0x2b++pattern Green :: Color+pattern Green = Color 0x27 0xae 0x60++pattern Yellow :: Color+pattern Yellow = Color 0xf3 0x9c 0x12++pattern Blue :: Color+pattern Blue = Color 0x29 0x80 0xb9++pattern Magenta :: Color+pattern Magenta = Color 0x8e 0x44 0xad++pattern Cyan :: Color+pattern Cyan = Color 0x16 0xa0 0x85++pattern White :: Color+pattern White = Color 0xec 0xf0 0xf1++pattern BrightBlack :: Color+pattern BrightBlack = Color 0x7f 0x8c 0x8d++pattern BrightRed :: Color+pattern BrightRed = Color 0xe7 0x4c 0x3c++pattern BrightGreen :: Color+pattern BrightGreen = Color 0x2e 0xcc 0x71++pattern BrightYellow :: Color+pattern BrightYellow = Color 0xf1 0xc4 0x0f++pattern BrightBlue :: Color+pattern BrightBlue = Color 0x34 0x98 0xdb++pattern BrightMagenta :: Color+pattern BrightMagenta = Color 0x9b 0x59 0xb6++pattern BrightCyan :: Color+pattern BrightCyan = Color 0x1a 0xbc 0x9c++pattern BrightWhite :: Color+pattern BrightWhite = Color 0xbd 0xc3 0xc7++pattern Default :: Color+pattern Default = Color 0x55 0x55 0x55++{- | ANSI escape code for a colour. The 16 named slots (plus 'Default') return+their canonical code; any other colour maps to the nearest ANSI slot.+-} ansiCode :: Color -> Int ansiCode Black = 30 ansiCode Red = 31@@ -64,10 +142,48 @@ ansiCode BrightCyan = 96 ansiCode BrightWhite = 97 ansiCode Default = 39+ansiCode (Color r g b) = nearestAnsiCode r g b +{- | Map an arbitrary RGB colour to the nearest of the 16 ANSI slots, using the+canonical (VGA) ANSI palette as anchors — kept consistent with the terminal+chrome quantisation in "Granite.Render.Chrome".+-}+nearestAnsiCode :: Word8 -> Word8 -> Word8 -> Int+nearestAnsiCode r g b =+    snd+        (minimumBy (comparing fst) [(dist anchor, code) | (anchor, code) <- ansiAnchors])+  where+    ri = fromIntegral r :: Int+    gi = fromIntegral g+    bi = fromIntegral b+    dist (ar, ag, ab) = sq (ar - ri) + sq (ag - gi) + sq (ab - bi)+    sq x = x * x++ansiAnchors :: [((Int, Int, Int), Int)]+ansiAnchors =+    [ ((0, 0, 0), 30)+    , ((170, 0, 0), 31)+    , ((0, 170, 0), 32)+    , ((170, 85, 0), 33)+    , ((0, 0, 170), 34)+    , ((170, 0, 170), 35)+    , ((0, 170, 170), 36)+    , ((170, 170, 170), 37)+    , ((85, 85, 85), 90)+    , ((255, 85, 85), 91)+    , ((85, 255, 85), 92)+    , ((255, 255, 85), 93)+    , ((85, 85, 255), 94)+    , ((255, 85, 255), 95)+    , ((85, 255, 255), 96)+    , ((255, 255, 255), 97)+    ]++-- | Opening ANSI escape sequence selecting a colour (its nearest ANSI slot). ansiOn :: Color -> Text ansiOn c = "\ESC[" <> Text.pack (show (ansiCode c)) <> "m" +-- | ANSI reset escape sequence. ansiOff :: Text ansiOff = "\ESC[0m" @@ -75,25 +191,27 @@ paint :: Color -> Char -> Text paint c ch = if ch == ' ' then " " else ansiOn c <> Text.singleton ch <> ansiOff --- | Hex RGB approximation of the ANSI palette, used by the SVG backend.+-- | Exact @#rrggbb@ rendering of a colour, used by the SVG backend. colorHex :: Color -> Text-colorHex Default = "#555555"-colorHex Black = "#2c3e50"-colorHex Red = "#c0392b"-colorHex Green = "#27ae60"-colorHex Yellow = "#f39c12"-colorHex Blue = "#2980b9"-colorHex Magenta = "#8e44ad"-colorHex Cyan = "#16a085"-colorHex White = "#ecf0f1"-colorHex BrightBlack = "#7f8c8d"-colorHex BrightRed = "#e74c3c"-colorHex BrightGreen = "#2ecc71"-colorHex BrightYellow = "#f1c40f"-colorHex BrightBlue = "#3498db"-colorHex BrightMagenta = "#9b59b6"-colorHex BrightCyan = "#1abc9c"-colorHex BrightWhite = "#bdc3c7"+colorHex (Color r g b) = "#" <> hex2 r <> hex2 g <> hex2 b+  where+    hex2 :: Word8 -> Text+    hex2 w =+        let s = showHex w ""+         in Text.pack (if length s < 2 then '0' : s else s)++{- | Parse a @"#rrggbb"@ (or @"rrggbb"@) string into a 'Color'. Returns+'Nothing' on malformed input.+-}+parseHex :: Text -> Maybe Color+parseHex t =+    case Text.unpack (Text.dropWhile (== '#') t) of+        cs@[r1, r2, g1, g2, b1, b2]+            | all isHexDigit cs ->+                Just (Color (byte r1 r2) (byte g1 g2) (byte b1 b2))+        _ -> Nothing+  where+    byte hi lo = fromIntegral (digitToInt hi * 16 + digitToInt lo)  -- | Default palette for layered charts (scatter, line, bars, …). paletteColors :: [Color]
src/Granite/Render/Pipeline.hs view
@@ -23,7 +23,7 @@ import Data.Text (Text) import Data.Text qualified as Text -import Granite.Color (Color (..))+import Granite.Color (Color (..), parseHex) import Granite.Data.Frame (     Column (..),     DataFrame (..),@@ -146,12 +146,13 @@         hasTitle = case chartTitle chart of             Just t -> not (Text.null t)             Nothing -> False-        legendEntries = collectLegend (chartLayers chart) palette+        colorMaps = map (layerColorMap palette (chartData chart)) (chartLayers chart)+        legendEntries = collectLegend (chartLayers chart) palette colorMaps         hasRightLegend = not (null legendEntries)         box = computePlotBox (chartSize chart) theme hasTitle hasRightLegend False          panels = layoutPanels theme box chart-        panelMarks = concatMap (renderPanel theme palette coord) panels+        panelMarks = concatMap (renderPanel theme palette coord colorMaps) panels          legend = legendMarks theme box legendEntries         title = titleMarks theme box (chartTitle chart)@@ -168,12 +169,19 @@     , panelYScale :: TrainedScale     } -renderPanel :: Theme -> [ColorSpec] -> Coord -> PanelSpec -> [Mark]-renderPanel theme palette coord ps =+renderPanel ::+    Theme ->+    [ColorSpec] ->+    Coord ->+    [Maybe [(Text, ColorSpec)]] ->+    PanelSpec ->+    [Mark]+renderPanel theme palette coord colorMaps ps =     let proj = buildProjector coord (panelBox ps) (panelXScale ps) (panelYScale ps)+        cmap i = if i < length colorMaps then colorMaps !! i else Nothing         layerMarks =             concat-                [ runLayer theme (panelBox ps) proj palette i (panelData ps) layer+                [ runLayer theme (panelBox ps) proj palette (cmap i) i (panelData ps) layer                 | (i, layer) <- zip [0 :: Int ..] (panelLayers ps)                 ]         axes = axesMarks theme (panelBox ps) coord (panelXScale ps) (panelYScale ps)@@ -405,11 +413,12 @@     PlotBox ->     Projector ->     [ColorSpec] ->+    Maybe [(Text, ColorSpec)] ->     Int ->     DataFrame ->     Layer ->     [Mark]-runLayer theme box proj palette ix globalFrame layer =+runLayer theme box proj palette colorMap ix globalFrame layer =     let frame0 = fromMaybe globalFrame (layerData layer)         framePostStat = applyStat (layerStat layer) (layerMapping layer) frame0         frame = applyPosition (layerPosition layer) (layerMapping layer) framePostStat@@ -418,14 +427,21 @@         colorSpec = case defColor defaults of             Just c -> c             Nothing -> layerDefaultColor palette ix layer-        col = specToTerminal colorSpec+        col = specToColor colorSpec         radius = case defSize defaults of             Just r -> r             Nothing -> pointSize theme layer         alpha = fromMaybe 1 (defAlpha defaults)         lineW = fromMaybe 2 (defLineWidth defaults)+        -- Per-row point colours: a mapped categorical 'aesColor' (when+        -- 'colorMap' is set for this layer) overrides the constant colour.+        pointColors = case colorMap of+            Just levelColors+                | Just cats <- categoricalColorColumn frame m ->+                    map (\cat -> specToColor (fromMaybe colorSpec (lookup cat levelColors))) cats+            _ -> repeat col      in case layerGeom layer of-            GeomPoint -> drawPoints proj frame m colorSpec radius alpha+            GeomPoint -> drawPoints proj frame m pointColors radius alpha             GeomLine -> drawLine proj frame m colorSpec lineW             GeomBar -> drawBars proj frame m col             GeomCol -> drawBars proj frame m col@@ -442,21 +458,21 @@     Projector ->     DataFrame ->     Mapping ->-    ColorSpec ->+    [Color] ->     Double ->     Double ->     [Mark]-drawPoints proj frame m col r alpha =+drawPoints proj frame m cols r alpha =     case (resolveNumColumn frame (aesX m), resolveNumColumn frame (aesY m)) of         (Just xv, Just yv) ->             [ MCircle                 (proj x y)                 r                 defaultStyle-                    { styleFill = Just (specToTerminal col)+                    { styleFill = Just c                     , styleFillOpacity = alpha                     }-            | (x, y) <- zip xv yv+            | ((x, y), c) <- zip (zip xv yv) cols             ]         _ -> [] @@ -475,7 +491,7 @@              in [ MPolyline                     pts                     defaultStyle-                        { styleStroke = Just (specToTerminal col)+                        { styleStroke = Just (specToColor col)                         , styleStrokeWidth = lineW                         }                 ]@@ -680,8 +696,8 @@                         s                         e                         defaultStyle-                            { styleFill = Just (specToTerminal (colorAt i))-                            , styleStroke = Just (specToTerminal (NamedColor BrightBlack))+                            { styleFill = Just (specToColor (colorAt i))+                            , styleStroke = Just (specToColor (NamedColor BrightBlack))                             , styleStrokeWidth = 1                             }                     | (i, (s, e)) <- zip [0 :: Int ..] (zip starts ends)@@ -877,21 +893,70 @@ pointSize :: Theme -> Layer -> Double pointSize _ _ = 3 -collectLegend :: [Layer] -> [ColorSpec] -> [(Text, ColorSpec)]-collectLegend layers palette =-    [ (legendName ix layer, palette !! (ix `mod` max 1 (length palette)))-    | (ix, layer) <- zip [0 :: Int ..] layers-    ]+{- | Map the distinct values of a categorical column to palette colours, in+first-seen order; shared by the renderer and 'collectLegend' so points and the+legend agree.+-}+categoricalColorMap :: [ColorSpec] -> [Text] -> [(Text, ColorSpec)]+categoricalColorMap palette levels =+    [(lvl, colorAt i) | (i, lvl) <- zip [0 :: Int ..] levels]   where+    colorAt i+        | null palette = NamedColor BrightBlue+        | otherwise = palette !! (i `mod` length palette)++{- | The per-category colour map for a layer, when its 'aesColor' maps to a+categorical ('ColCat') column. Computed once from the whole-chart layer frame so+colours stay consistent across facets and the legend; 'Nothing' otherwise+(numeric colour columns keep the single per-layer colour).+-}+layerColorMap :: [ColorSpec] -> DataFrame -> Layer -> Maybe [(Text, ColorSpec)]+layerColorMap palette globalFrame layer+    | GeomPoint <- layerGeom layer+    , Just cats <-+        categoricalColorColumn+            (fromMaybe globalFrame (layerData layer))+            (layerMapping layer) =+        Just (categoricalColorMap palette (List.nub cats))+    | otherwise = Nothing++{- | The categorical ('ColCat') colour column's per-row values, if 'aesColor'+maps to one.+-}+categoricalColorColumn :: DataFrame -> Mapping -> Maybe [Text]+categoricalColorColumn frame m = case aesColor m of+    Just (ColumnRef n) -> case lookupColumn n frame of+        Just (ColCat xs) -> Just xs+        _ -> Nothing+    Nothing -> Nothing++{- | Legend entries. A categorical-'aesColor' layer contributes one entry per+category value (colours matching the points via 'layerColorMap'); 'GeomText'+layers contribute none; any other layer keeps a single per-layer swatch.+-}+collectLegend ::+    [Layer] -> [ColorSpec] -> [Maybe [(Text, ColorSpec)]] -> [(Text, ColorSpec)]+collectLegend layers palette colorMaps =+    concat+        [ entriesFor ix layer cmap+        | (ix, layer, cmap) <- zip3 [0 :: Int ..] layers (colorMaps ++ repeat Nothing)+        ]+  where+    entriesFor ix layer cmap+        | GeomText <- layerGeom layer = []+        | Just levelColors <- cmap = levelColors+        | otherwise =+            [(legendName ix layer, palette !! (ix `mod` max 1 (length palette)))]     legendName ix layer =         case aesGroup (layerMapping layer) of             Just (ColumnRef n) -> n             Nothing -> "series " <> Text.pack (show ix) -{- | Quantise 'ColorSpec' to the nearest renderable ANSI 'Color' for-the terminal backend. SVG reads RGB / Hex directly.+{- | Resolve a 'ColorSpec' to an exact RGB 'Color'. The SVG backend renders+this exactly (via @colorHex@); the terminal backend quantises it to the nearest+ANSI slot (via @ansiCode@). -}-specToTerminal :: ColorSpec -> Color-specToTerminal (NamedColor c) = c-specToTerminal (RGB{}) = BrightBlue-specToTerminal (Hex _) = BrightBlue+specToColor :: ColorSpec -> Color+specToColor (NamedColor c) = c+specToColor (RGB r g b) = Color r g b+specToColor (Hex t) = fromMaybe Black (parseHex t)
test/GraniteSpec.hs view
@@ -339,6 +339,7 @@ ansiCode BrightCyan = 96 ansiCode BrightWhite = 97 ansiCode Default = 39+ansiCode _ = 39  ansiOn :: Color -> Text ansiOn c = "\ESC[" <> Text.pack (show (ansiCode c)) <> "m"
test/PipelineSpec.hs view
@@ -4,6 +4,7 @@  import Data.Text qualified as Text +import Granite.Color qualified as Col import Granite.Render.Pipeline import Granite.Render.Scene (Scene (..)) import Granite.Spec@@ -83,6 +84,72 @@             -- roughly 24 lines (22 plot rows + title + axis labels).             lineCount `shouldSatisfy` (>= 15)             lineCount `shouldSatisfy` (<= 40)++    describe "colour" $ do+        it "colorHex renders exact, zero-padded, lowercase hex" $ do+            Col.colorHex Col.BrightBlue `shouldBe` "#3498db"+            Col.colorHex Col.BrightYellow `shouldBe` "#f1c40f"+            Col.colorHex (Col.Color 0 0 15) `shouldBe` "#00000f"++        it "ansiCode keeps canonical codes for the named slots" $ do+            Col.ansiCode Col.Black `shouldBe` 30+            Col.ansiCode Col.BrightBlue `shouldBe` 94+            Col.ansiCode Col.Default `shouldBe` 39++        it "parseHex accepts valid hex and rejects junk" $ do+            Col.parseHex "#ff0000" `shouldBe` Just (Col.Color 255 0 0)+            Col.parseHex "1a2b3c" `shouldBe` Just (Col.Color 26 43 60)+            Col.parseHex "nope" `shouldBe` Nothing++        it "SVG renders a custom Hex defColor exactly (not quantised to blue)" $ do+            let chart =+                    emptyChart+                        { chartSize = SizeChars 30 16+                        , chartData =+                            fromColumns [("x", ColNum [1, 2, 3]), ("y", ColNum [1, 2, 3])]+                        , chartLayers =+                            [ (defLayer GeomPoint)+                                { layerMapping =+                                    emptyMapping+                                        { aesX = Just (ColumnRef "x")+                                        , aesY = Just (ColumnRef "y")+                                        }+                                , layerAesDef = emptyAesDefaults{defColor = Just (Hex "#ff0000")}+                                }+                            ]+                        }+            renderChartSvg chart `shouldSatisfy` Text.isInfixOf "#ff0000"++        it "categorical aesColor colours points per category with a per-category legend" $ do+            let chart =+                    emptyChart+                        { chartSize = SizeChars 40 20+                        , chartData =+                            fromColumns+                                [ ("x", ColNum [1, 2, 3])+                                , ("y", ColNum [1, 2, 3])+                                , ("g", ColCat ["a", "b", "c"])+                                ]+                        , chartLayers =+                            [ (defLayer GeomPoint)+                                { layerMapping =+                                    emptyMapping+                                        { aesX = Just (ColumnRef "x")+                                        , aesY = Just (ColumnRef "y")+                                        , aesColor = Just (ColumnRef "g")+                                        }+                                }+                            ]+                        }+                svg = renderChartSvg chart+            -- three categories → the first three palette colours all appear+            svg `shouldSatisfy` Text.isInfixOf "#3498db"+            svg `shouldSatisfy` Text.isInfixOf "#9b59b6"+            svg `shouldSatisfy` Text.isInfixOf "#1abc9c"+            -- the legend is keyed by category value, not a generic "series 0"+            svg `shouldSatisfy` Text.isInfixOf ">a</text>"+            svg `shouldSatisfy` Text.isInfixOf ">c</text>"+            svg `shouldNotSatisfy` Text.isInfixOf "series 0"      describe "Log-scale chart" $ do         let logChart =
+ test/golden/bar-basic.svg view
@@ -0,0 +1,20 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 224" width="400" height="224" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="196.20" x2="280" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="86.76" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="140.59" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="194.41" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="248.24" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">3.0</text>+<text x="49" y="192.49" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="151.53" text-anchor="end" fill="#7f8c8d" font-size="11">5.0</text>+<text x="49" y="110.57" text-anchor="end" fill="#7f8c8d" font-size="11">10.0</text>+<text x="49" y="69.62" text-anchor="end" fill="#7f8c8d" font-size="11">15.0</text>+<rect x="65.23" y="90.52" width="43.06" height="98.30" fill="#3498db"/>+<rect x="119.06" y="131.48" width="43.06" height="57.34" fill="#3498db"/>+<rect x="172.88" y="41.37" width="43.06" height="147.45" fill="#3498db"/>+<rect x="226.71" y="115.10" width="43.06" height="73.73" fill="#3498db"/>+<text x="167.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Bars</text>+<rect x="295" y="39" width="12" height="12" fill="#3498db"/>+<text x="311" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/bar-basic.txt view
@@ -0,0 +1,14 @@+                                        +              [90mBars                      [0m+     [37m│           [0m[94m█████       ██         [0m+     [37m│           [0m[94m█████       ██[0m[90mseries 0 [0m+[90m15.0 [0m[37m│           [0m[94m█████                  [0m+     [37m│[0m[94m█████      █████                  [0m+[90m10.0 [0m[37m│[0m[94m█████      █████                  [0m+     [37m│[0m[94m█████      ██████████             [0m+     [37m│[0m[94m█████████████████████             [0m+ [90m5.0 [0m[37m│[0m[94m█████████████████████             [0m+     [37m│[0m[94m█████████████████████             [0m+     [37m│[0m[94m█████████████████████             [0m+ [90m0.0 [0m[37m┼───────────────────────           [0m+       [90m0.0   1.0  2.0  3.0              [0m
+ test/golden/box-basic.svg view
@@ -0,0 +1,25 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 256" width="400" height="256" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="228.20" x2="280" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="94.45" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="167.50" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="240.55" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="49" y="164.19" text-anchor="end" fill="#7f8c8d" font-size="11">5.0</text>+<text x="49" y="90.63" text-anchor="end" fill="#7f8c8d" font-size="11">10.0</text>+<polygon points="65.23,189.95 123.67,189.95 123.67,131.10 65.23,131.10" fill="#3498db" stroke="#3498db" stroke-width="1" fill-opacity="0.20"/>+<polyline points="65.23,160.52 123.67,160.52" fill="none" stroke="#3498db" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="94.45,189.95 94.45,219.37" fill="none" stroke="#3498db" stroke-width="1" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="94.45,131.10 94.45,101.68" fill="none" stroke="#3498db" stroke-width="1" stroke-linejoin="round" stroke-linecap="round"/>+<polygon points="138.28,160.52 196.72,160.52 196.72,101.68 138.28,101.68" fill="#3498db" stroke="#3498db" stroke-width="1" fill-opacity="0.20"/>+<polyline points="138.28,131.10 196.72,131.10" fill="none" stroke="#3498db" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="167.50,160.52 167.50,189.95" fill="none" stroke="#3498db" stroke-width="1" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="167.50,101.68 167.50,72.25" fill="none" stroke="#3498db" stroke-width="1" stroke-linejoin="round" stroke-linecap="round"/>+<polygon points="211.33,131.10 269.77,131.10 269.77,72.25 211.33,72.25" fill="#3498db" stroke="#3498db" stroke-width="1" fill-opacity="0.20"/>+<polyline points="211.33,101.68 269.77,101.68" fill="none" stroke="#3498db" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="240.55,131.10 240.55,160.52" fill="none" stroke="#3498db" stroke-width="1" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="240.55,72.25 240.55,42.83" fill="none" stroke="#3498db" stroke-width="1" stroke-linejoin="round" stroke-linecap="round"/>+<text x="167.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Boxplot</text>+<rect x="295" y="39" width="12" height="12" fill="#3498db"/>+<text x="311" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/box-basic.txt view
@@ -0,0 +1,16 @@+                                        +             [90mBoxplot                    [0m+     [37m│                  ⡀    [0m[94m██         [0m+     [37m│                  ⡇    [0m[94m██[0m[90mseries 0 [0m+     [37m│          ⢠    [0m[94m⡤⠤⠤⠧⠤⠤⡄            [0m+[90m10.0 [0m[37m│          ⢸    [0m[94m⡇     ⡇            [0m+     [37m│   ⢰    [0m[94m⡖⠒⠚⠒⠒⢲ ⡗⠒⠒⠒⠒⠒⡇            [0m+     [37m│   ⢸    [0m[94m⡇    ⢸ ⡇     ⡇            [0m+     [37m│[0m[94m⢰⠒⠒⠚⠒⠒⢲ ⡗⠒⠒⠒⠒⢺ ⠓⠒⠒⡖⠒⠒⠃            [0m+     [37m│[0m[94m⢸     ⢸ ⡇    ⢸    ⡇               [0m+ [90m5.0 [0m[37m│[0m[94m⢸⠉⠉⠉⠉⠉⢹ ⠉⠉⢹⠉⠉⠉    ⠁               [0m+     [37m│[0m[94m⢸⣀⣀⣀⣀⣀⣸   ⢸                       [0m+     [37m│   ⢸                              [0m+     [37m│   ⢸                              [0m+     [37m┼───────────────────────           [0m+        [90m0.0    1.0     2.0              [0m
+ test/golden/empty.svg view
@@ -0,0 +1,11 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 320" width="600" height="320" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="292.20" x2="580" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="10" x2="55" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="78.86" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="317.50" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.5</text>+<text x="556.14" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="49" y="283.04" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="154.77" text-anchor="end" fill="#7f8c8d" font-size="11">0.5</text>+<text x="49" y="26.49" text-anchor="end" fill="#7f8c8d" font-size="11">1.0</text>+</svg>
+ test/golden/facet-wrap.svg view
@@ -0,0 +1,51 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 288" width="700" height="288" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="60" y1="246.20" x2="225" y2="246.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="60" y1="66" x2="60" y2="246.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="67.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="117.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="167.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="217.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">3.0</text>+<text x="54" y="241.68" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="54" y="190.48" text-anchor="end" fill="#7f8c8d" font-size="11">5.0</text>+<text x="54" y="139.29" text-anchor="end" fill="#7f8c8d" font-size="11">10.0</text>+<text x="54" y="88.10" text-anchor="end" fill="#7f8c8d" font-size="11">15.0</text>+<circle cx="67.50" cy="227.77" r="3" fill="#3498db"/>+<circle cx="117.50" cy="197.05" r="3" fill="#3498db"/>+<circle cx="167.50" cy="145.86" r="3" fill="#3498db"/>+<circle cx="217.50" cy="74.19" r="3" fill="#3498db"/>+<text x="142.50" y="62" text-anchor="middle" fill="#555555" font-size="11">A</text>+<line x1="235" y1="246.20" x2="400" y2="246.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="235" y1="66" x2="235" y2="246.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="242.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="292.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="342.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="392.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">3.0</text>+<text x="229" y="241.68" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="229" y="190.48" text-anchor="end" fill="#7f8c8d" font-size="11">5.0</text>+<text x="229" y="139.29" text-anchor="end" fill="#7f8c8d" font-size="11">10.0</text>+<text x="229" y="88.10" text-anchor="end" fill="#7f8c8d" font-size="11">15.0</text>+<circle cx="242.50" cy="238.01" r="3" fill="#3498db"/>+<circle cx="292.50" cy="217.53" r="3" fill="#3498db"/>+<circle cx="342.50" cy="197.05" r="3" fill="#3498db"/>+<circle cx="392.50" cy="176.58" r="3" fill="#3498db"/>+<text x="317.50" y="62" text-anchor="middle" fill="#555555" font-size="11">B</text>+<line x1="410" y1="246.20" x2="575" y2="246.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="410" y1="66" x2="410" y2="246.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="417.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="467.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="517.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="567.50" y="261.20" text-anchor="middle" fill="#7f8c8d" font-size="11">3.0</text>+<text x="404" y="241.68" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="404" y="190.48" text-anchor="end" fill="#7f8c8d" font-size="11">5.0</text>+<text x="404" y="139.29" text-anchor="end" fill="#7f8c8d" font-size="11">10.0</text>+<text x="404" y="88.10" text-anchor="end" fill="#7f8c8d" font-size="11">15.0</text>+<circle cx="417.50" cy="186.82" r="3" fill="#3498db"/>+<circle cx="467.50" cy="197.05" r="3" fill="#3498db"/>+<circle cx="517.50" cy="207.29" r="3" fill="#3498db"/>+<circle cx="567.50" cy="217.53" r="3" fill="#3498db"/>+<text x="492.50" y="62" text-anchor="middle" fill="#555555" font-size="11">C</text>+<text x="317.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Faceted by series</text>+<rect x="595" y="39" width="12" height="12" fill="#3498db"/>+<text x="611" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/facet-wrap.txt view
@@ -0,0 +1,18 @@+                                                                      +                       [90mFaceted by series                              [0m+                                                           [94m██         [0m+              [39mA                B                 C         [0m[94m██[0m[90mseries 0 [0m+      [37m│              [0m[94m⣠⡀[0m[37m│                 │                            [0m+ [90m15.0 [0m[37m│           [0m[90m15.0 [0m[37m│            [0m[90m15.0 [0m[37m│                            [0m+      [37m│                │                 │                            [0m+      [37m│                │                 │                            [0m+ [90m10.0 [0m[37m│         [0m[94m⢀ [0m[90m10.0 [0m[37m│            [0m[90m10.0 [0m[37m│                            [0m+      [37m│         [0m[94m⠙⠁     [0m[37m│                 │                            [0m+      [37m│                │               [0m[94m⡀ [0m[37m│                            [0m+  [90m5.0 [0m[37m│            [0m[90m5.0 [0m[37m│             [0m[90m5.0 [0m[37m│[0m[94m⡀                           [0m+      [37m│    [0m[94m⠺⠂          [0m[37m│         [0m[94m⠐⠗      [0m[37m│    [0m[94m⠺⠂   ⢀                  [0m+      [37m│                │    [0m[94m⠠⡦           [0m[37m│         [0m[94m⠙⠁   ⢴⠄            [0m+      [37m│[0m[94m⠂               [0m[37m│[0m[94m⡀                [0m[37m│                            [0m+  [90m0.0 [0m[37m┼────────────[0m[90m0.0[0m[37m─┼─────────────[0m[90m0.0[0m[37m─┼────────────────            [0m+     [90m0.0  1.0  2.0  3.00.0  1.0  2.0  3.0.0  1.0  2.0  3.0            [0m+                                                                      
+ test/golden/hist-basic.svg view
@@ -0,0 +1,23 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 440 224" width="440" height="224" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="196.20" x2="320" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="63.96" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="141.17" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="218.39" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">3.0</text>+<text x="295.60" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="49" y="192.49" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="118.77" text-anchor="end" fill="#7f8c8d" font-size="11">5.0</text>+<text x="49" y="45.04" text-anchor="end" fill="#7f8c8d" font-size="11">10.0</text>+<rect x="67.05" y="41.37" width="24.71" height="147.45" fill="#3498db"/>+<rect x="97.93" y="188.83" width="24.71" height="0" fill="#3498db"/>+<rect x="128.82" y="41.37" width="24.71" height="147.45" fill="#3498db"/>+<rect x="159.70" y="115.10" width="24.71" height="73.73" fill="#3498db"/>+<rect x="190.59" y="115.10" width="24.71" height="73.73" fill="#3498db"/>+<rect x="221.47" y="115.10" width="24.71" height="73.73" fill="#3498db"/>+<rect x="252.36" y="115.10" width="24.71" height="73.73" fill="#3498db"/>+<rect x="283.25" y="41.37" width="24.71" height="147.45" fill="#3498db"/>+<text x="187.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Histogram</text>+<rect x="335" y="39" width="12" height="12" fill="#3498db"/>+<text x="351" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/hist-basic.txt view
@@ -0,0 +1,14 @@+                                            +              [90mHistogram                     [0m+[90m10.0 [0m[37m│[0m[94m████  ████            ███  ██         [0m+     [37m│[0m[94m████  ████            ███  ██[0m[90mseries 0 [0m+     [37m│[0m[94m████  ████            ███             [0m+     [37m│[0m[94m████  ████            ███             [0m+     [37m│[0m[94m████  ████            ███             [0m+ [90m5.0 [0m[37m│[0m[94m████  ███████████████████             [0m+     [37m│[0m[94m████  ███████████████████             [0m+     [37m│[0m[94m████  ███████████████████             [0m+     [37m│[0m[94m████  ███████████████████             [0m+     [37m│[0m[94m█████████████████████████             [0m+ [90m0.0 [0m[37m┼───────────────────────────           [0m+     [90m1.0     2.0    3.0     4.0             [0m
+ test/golden/line-basic.svg view
@@ -0,0 +1,16 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 320" width="600" height="320" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="292.20" x2="480" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="74.32" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="203.11" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="331.89" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="460.68" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">6.0</text>+<text x="49" y="286.80" text-anchor="end" fill="#7f8c8d" font-size="11">-1.0</text>+<text x="49" y="167.95" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="49.11" text-anchor="end" fill="#7f8c8d" font-size="11">1.0</text>+<polyline points="74.32,164.29 106.52,107.31 138.71,64.28 170.91,45.74 203.11,56.22 235.30,93.16 267.50,147.51 299.70,205.98 331.89,254.23 364.09,280.46 396.29,278.25 428.48,248.14 460.68,197.49" fill="none" stroke="#3498db" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>+<text x="267.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">sin(x)</text>+<rect x="495" y="39" width="12" height="12" fill="#3498db"/>+<text x="511" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">sine</text>+</svg>
+ test/golden/line-basic.txt view
@@ -0,0 +1,20 @@+                                                            +                       [90msin(x)                               [0m+     [37m│           ⣀                               [0m[94m██         [0m+ [90m1.0 [0m[37m│        ⢀⠔⠊ ⠉⠒⠤⡀                           [0m[94m██[0m[90msine     [0m+     [37m│       ⡰⠁      ⠑⡄                                     [0m+     [37m│      ⡜         ⠈⢆                                    [0m+     [37m│    ⢀⠎           ⠈⢆                                   [0m+     [37m│   ⢀⠎             ⠈⡆                                  [0m+     [37m│   ⡎               ⠘⡄                                 [0m+     [37m│  ⡜                 ⠘⡄                                [0m+ [90m0.0 [0m[37m│ ⠘                   ⠘⡄                               [0m+     [37m│                      ⠘⡄                              [0m+     [37m│                       ⠘⡄              ⢀⠆             [0m+     [37m│                        ⠱⡀            ⢀⠎              [0m+     [37m│                         ⠱⡀          ⢀⠎               [0m+     [37m│                          ⠱⡀        ⢀⠎                [0m+     [37m│                           ⠑⢄⡀     ⡰⠁                 [0m+[90m-1.0 [0m[37m│                             ⠈⠢⠤⠤⠤⠊                   [0m+     [37m┼───────────────────────────────────────────           [0m+      [90m0.0          2.0          4.0          6.0            [0m
+ test/golden/line-coord-flip.svg view
@@ -0,0 +1,19 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 320" width="600" height="320" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="292.20" x2="480" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="74.32" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="170.91" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="267.50" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="364.09" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">6.0</text>+<text x="460.68" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">8.0</text>+<text x="49" y="49.40" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="108.08" text-anchor="end" fill="#7f8c8d" font-size="11">1.0</text>+<text x="49" y="166.77" text-anchor="end" fill="#7f8c8d" font-size="11">2.0</text>+<text x="49" y="225.45" text-anchor="end" fill="#7f8c8d" font-size="11">3.0</text>+<text x="49" y="284.13" text-anchor="end" fill="#7f8c8d" font-size="11">4.0</text>+<polyline points="74.32,45.74 170.91,104.42 267.50,163.10 364.09,221.78 460.68,280.46" fill="none" stroke="#3498db" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>+<text x="267.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">CoordFlip</text>+<rect x="495" y="39" width="12" height="12" fill="#3498db"/>+<text x="511" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">y=2x</text>+</svg>
+ test/golden/line-coord-flip.txt view
@@ -0,0 +1,20 @@+                                                            +                      [90mCoordFlip                             [0m+     [37m│ ⢀                                         [0m[94m██         [0m+ [90m0.0 [0m[37m│  ⠑⠢⡀                                      [0m[94m██[0m[90my=2x     [0m+     [37m│    ⠈⠒⢄                                               [0m+     [37m│       ⠉⠢⡀                                            [0m+ [90m1.0 [0m[37m│         ⠈⠑⢄                                          [0m+     [37m│            ⠉⠢⡀                                       [0m+     [37m│              ⠈⠑⢄⡀                                    [0m+     [37m│                 ⠈⠢⣀                                  [0m+ [90m2.0 [0m[37m│                    ⠑⠤⡀                               [0m+     [37m│                      ⠈⠒⢄⡀                            [0m+     [37m│                         ⠈⠒⢄                          [0m+     [37m│                            ⠉⠢⢄                       [0m+ [90m3.0 [0m[37m│                               ⠑⠢⡀                    [0m+     [37m│                                 ⠈⠒⢄                  [0m+     [37m│                                    ⠉⠢⡀               [0m+ [90m4.0 [0m[37m│                                      ⠈⠑⠄             [0m+     [37m┼───────────────────────────────────────────           [0m+      [90m0.0       2.0      4.0       6.0       8.0            [0m
+ test/golden/line-reverse-y.svg view
@@ -0,0 +1,16 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 320" width="600" height="320" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="292.20" x2="480" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="74.32" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="203.11" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="331.89" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="460.68" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">6.0</text>+<text x="49" y="46.73" text-anchor="end" fill="#7f8c8d" font-size="11">-1.0</text>+<text x="49" y="165.58" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="284.43" text-anchor="end" fill="#7f8c8d" font-size="11">1.0</text>+<polyline points="74.32,161.91 106.52,218.89 138.71,261.92 170.91,280.46 203.11,269.98 235.30,233.04 267.50,178.69 299.70,120.22 331.89,71.97 364.09,45.74 396.29,47.95 428.48,78.06 460.68,128.71" fill="none" stroke="#3498db" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>+<text x="267.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Reverse Y</text>+<rect x="495" y="39" width="12" height="12" fill="#3498db"/>+<text x="511" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">sine</text>+</svg>
+ test/golden/pie-basic.svg view
@@ -0,0 +1,18 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 256" width="300" height="256" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="228.20" x2="180" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="60.68" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="117.50" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.5</text>+<text x="174.32" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="49" y="187.73" text-anchor="end" fill="#7f8c8d" font-size="11">20.0</text>+<text x="49" y="117.11" text-anchor="end" fill="#7f8c8d" font-size="11">30.0</text>+<text x="49" y="46.49" text-anchor="end" fill="#7f8c8d" font-size="11">40.0</text>+<path d="M 117.50 131.10 L 117.50 74.85 A 56.25 56.25 0 0 0 150.56 176.61 Z" fill="#3498db" stroke="#7f8c8d" stroke-width="1"/>+<path d="M 117.50 131.10 L 150.56 176.61 A 56.25 56.25 0 0 0 71.99 164.16 Z" fill="#9b59b6" stroke="#7f8c8d" stroke-width="1"/>+<path d="M 117.50 131.10 L 71.99 164.16 A 56.25 56.25 0 0 0 71.99 98.04 Z" fill="#1abc9c" stroke="#7f8c8d" stroke-width="1"/>+<path d="M 117.50 131.10 L 71.99 98.04 A 56.25 56.25 0 0 0 117.50 74.85 Z" fill="#2ecc71" stroke="#7f8c8d" stroke-width="1"/>+<text x="117.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Pie</text>+<rect x="195" y="39" width="12" height="12" fill="#3498db"/>+<text x="211" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/plotly-area-filled.svg view
@@ -0,0 +1,16 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 224" width="560" height="224" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="196.20" x2="440" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="72.50" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="218.33" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">5.0</text>+<text x="364.17" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">10.0</text>+<text x="49" y="192.49" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="143.30" text-anchor="end" fill="#7f8c8d" font-size="11">1.0</text>+<text x="49" y="94.11" text-anchor="end" fill="#7f8c8d" font-size="11">2.0</text>+<text x="49" y="44.92" text-anchor="end" fill="#7f8c8d" font-size="11">3.0</text>+<polygon points="72.50,90.44 101.67,66.86 130.83,49.05 160.00,41.37 189.17,45.71 218.33,61.00 247.50,83.50 276.67,107.70 305.83,127.67 335.00,138.53 364.17,137.61 393.33,125.15 422.50,104.19 422.50,188.83 393.33,188.83 364.17,188.83 335.00,188.83 305.83,188.83 276.67,188.83 247.50,188.83 218.33,188.83 189.17,188.83 160.00,188.83 130.83,188.83 101.67,188.83 72.50,188.83" fill="#3498db" stroke="#3498db" stroke-width="1" fill-opacity="0.40"/>+<text x="247.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Filled area</text>+<rect x="455" y="39" width="12" height="12" fill="#3498db"/>+<text x="471" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/plotly-area-filled.txt view
@@ -0,0 +1,14 @@+                                                        +                   [90mFilled area                          [0m+ [90m3.0 [0m[37m│        [0m[94m⣀⡠⠤⢄⣀⡀                         ██         [0m+     [37m│     [0m[94m⡠⠔⠉     ⠈⠑⠤⡀                      ██[0m[90mseries 0 [0m+     [37m│   [0m[94m⡠⠊           ⠈⢢                                [0m+ [90m2.0 [0m[37m│ [0m[94m⡠⠊               ⠑⢄                              [0m+     [37m│ [0m[94m⡇                  ⠑⢄             ⢀⡄             [0m+     [37m│ [0m[94m⡇                    ⠑⠤⡀        ⣀⠔⠁⡇             [0m+ [90m1.0 [0m[37m│ [0m[94m⡇                      ⠈⠒⠤⣀⣀⠤⠤⠒⠉   ⡇             [0m+     [37m│ [0m[94m⡇                                  ⡇             [0m+     [37m│ [0m[94m⡇                                  ⡇             [0m+     [37m│ [0m[94m⣇⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⡇             [0m+ [90m0.0 [0m[37m┼───────────────────────────────────────           [0m+      [90m0.0           5.0           10.0                  [0m
+ test/golden/plotly-bar-grouped.svg view
@@ -0,0 +1,27 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 256" width="600" height="256" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="228.20" x2="480" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="110.87" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="215.29" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="319.71" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="424.13" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">3.0</text>+<text x="49" y="223.04" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="142.79" text-anchor="end" fill="#7f8c8d" font-size="11">10.0</text>+<text x="49" y="62.54" text-anchor="end" fill="#7f8c8d" font-size="11">20.0</text>+<rect x="74.32" y="123.08" width="20.88" height="96.30" fill="#3498db"/>+<rect x="100.42" y="155.17" width="20.88" height="64.20" fill="#3498db"/>+<rect x="126.53" y="187.27" width="20.88" height="32.10" fill="#3498db"/>+<rect x="178.74" y="99.00" width="20.88" height="120.37" fill="#3498db"/>+<rect x="204.85" y="139.12" width="20.88" height="80.25" fill="#3498db"/>+<rect x="230.95" y="171.22" width="20.88" height="48.15" fill="#3498db"/>+<rect x="283.16" y="74.93" width="20.88" height="144.45" fill="#3498db"/>+<rect x="309.27" y="123.08" width="20.88" height="96.30" fill="#3498db"/>+<rect x="335.37" y="155.17" width="20.88" height="64.20" fill="#3498db"/>+<rect x="387.59" y="42.83" width="20.88" height="176.55" fill="#3498db"/>+<rect x="413.69" y="107.03" width="20.88" height="112.35" fill="#3498db"/>+<rect x="439.80" y="139.12" width="20.88" height="80.25" fill="#3498db"/>+<text x="267.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Sales by quarter (grouped)</text>+<rect x="495" y="39" width="12" height="12" fill="#3498db"/>+<text x="511" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">product</text>+</svg>
+ test/golden/plotly-bar-grouped.txt view
@@ -0,0 +1,16 @@+                                                            +             [90mSales by quarter (grouped)                     [0m+     [37m│                                [0m[94m███        ██         [0m+[90m20.0 [0m[37m│                                [0m[94m███        ██[0m[90mproduct  [0m+     [37m│                      [0m[94m███       ███                   [0m+     [37m│                      [0m[94m███       ███                   [0m+     [37m│           [0m[94m███        ███       ██████                [0m+     [37m│ [0m[94m███       ███        █████     ██████                [0m+[90m10.0 [0m[37m│ [0m[94m███       ██████     █████     ████████              [0m+     [37m│ [0m[94m██████    ██████     ████████  ████████              [0m+     [37m│ [0m[94m██████    █████████  ████████  ████████              [0m+     [37m│ [0m[94m████████  █████████  ████████  ████████              [0m+     [37m│ [0m[94m████████  █████████  ████████  ████████              [0m+ [90m0.0 [0m[37m│ [0m[94m████████  █████████  ████████  ████████              [0m+     [37m┼───────────────────────────────────────────           [0m+          [90m0.0       1.0       2.0        3.0                [0m
+ test/golden/plotly-bar-horizontal.svg view
@@ -0,0 +1,21 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 256" width="600" height="256" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="228.20" x2="480" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="74.32" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="168.55" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">20.0</text>+<text x="262.79" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">40.0</text>+<text x="357.02" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">60.0</text>+<text x="451.26" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">80.0</text>+<text x="49" y="61.21" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="134.77" text-anchor="end" fill="#7f8c8d" font-size="11">2.0</text>+<text x="49" y="208.33" text-anchor="end" fill="#7f8c8d" font-size="11">4.0</text>+<rect x="74.32" y="42.83" width="386.36" height="29.42" fill="#3498db"/>+<rect x="74.32" y="79.61" width="315.69" height="29.42" fill="#3498db"/>+<rect x="74.32" y="116.39" width="212.03" height="29.42" fill="#3498db"/>+<rect x="74.32" y="153.17" width="155.49" height="29.42" fill="#3498db"/>+<rect x="74.32" y="189.95" width="98.95" height="29.42" fill="#3498db"/>+<text x="267.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Top 5 (horizontal)</text>+<rect x="495" y="39" width="12" height="12" fill="#3498db"/>+<text x="511" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/plotly-bar-horizontal.txt view
@@ -0,0 +1,16 @@+                                                            +                 [90mTop 5 (horizontal)                         [0m+     [37m│ [0m[94m███████████████████████████████████████   ██         [0m+ [90m0.0 [0m[37m│ [0m[94m███████████████████████████████████████   ██[0m[90mseries 0 [0m+     [37m│ [0m[94m███████████████████████████████████████              [0m+     [37m│ [0m[94m████████████████████████████████                     [0m+     [37m│ [0m[94m████████████████████████████████                     [0m+     [37m│ [0m[94m██████████████████████                               [0m+ [90m2.0 [0m[37m│ [0m[94m██████████████████████                               [0m+     [37m│ [0m[94m██████████████████████                               [0m+     [37m│ [0m[94m████████████████                                     [0m+     [37m│ [0m[94m████████████████                                     [0m+     [37m│ [0m[94m███████████                                          [0m+ [90m4.0 [0m[37m│ [0m[94m███████████                                          [0m+     [37m┼───────────────────────────────────────────           [0m+      [90m0.0     20.0      40.0     60.0      80.0             [0m
+ test/golden/plotly-bar-stacked.svg view
@@ -0,0 +1,27 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 256" width="600" height="256" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="228.20" x2="480" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="114.99" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="216.66" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="318.34" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="420.01" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">3.0</text>+<text x="49" y="223.04" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="146.28" text-anchor="end" fill="#7f8c8d" font-size="11">20.0</text>+<text x="49" y="69.52" text-anchor="end" fill="#7f8c8d" font-size="11">40.0</text>+<rect x="74.32" y="173.32" width="81.34" height="46.06" fill="#3498db"/>+<rect x="74.32" y="142.61" width="81.34" height="30.70" fill="#3498db"/>+<rect x="74.32" y="127.26" width="81.34" height="15.35" fill="#3498db"/>+<rect x="175.99" y="161.80" width="81.34" height="57.57" fill="#3498db"/>+<rect x="175.99" y="123.42" width="81.34" height="38.38" fill="#3498db"/>+<rect x="175.99" y="100.40" width="81.34" height="23.03" fill="#3498db"/>+<rect x="277.67" y="150.29" width="81.34" height="69.08" fill="#3498db"/>+<rect x="277.67" y="104.23" width="81.34" height="46.06" fill="#3498db"/>+<rect x="277.67" y="73.53" width="81.34" height="30.70" fill="#3498db"/>+<rect x="379.34" y="134.94" width="81.34" height="84.43" fill="#3498db"/>+<rect x="379.34" y="81.21" width="81.34" height="53.73" fill="#3498db"/>+<rect x="379.34" y="42.83" width="81.34" height="38.38" fill="#3498db"/>+<text x="267.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Sales by quarter (stacked)</text>+<rect x="495" y="39" width="12" height="12" fill="#3498db"/>+<text x="511" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">product</text>+</svg>
+ test/golden/plotly-bar-stacked.txt view
@@ -0,0 +1,16 @@+                                                            +             [90mSales by quarter (stacked)                     [0m+     [37m│                               [0m[94m█████████   ██         [0m+     [37m│                               [0m[94m█████████   ██[0m[90mproduct  [0m+[90m40.0 [0m[37m│                     [0m[94m█████████ █████████              [0m+     [37m│                     [0m[94m█████████ █████████              [0m+     [37m│           [0m[94m█████████ █████████ █████████              [0m+     [37m│ [0m[94m█████████ █████████ █████████ █████████              [0m+     [37m│ [0m[94m█████████ █████████ █████████ █████████              [0m+[90m20.0 [0m[37m│ [0m[94m█████████ █████████ █████████ █████████              [0m+     [37m│ [0m[94m█████████ █████████ █████████ █████████              [0m+     [37m│ [0m[94m█████████ █████████ █████████ █████████              [0m+     [37m│ [0m[94m█████████ █████████ █████████ █████████              [0m+ [90m0.0 [0m[37m│ [0m[94m█████████ █████████ █████████ █████████              [0m+     [37m┼───────────────────────────────────────────           [0m+          [90m0.0       1.0       2.0        3.0                [0m
+ test/golden/plotly-density.svg view
@@ -0,0 +1,17 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 224" width="560" height="224" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="196.20" x2="440" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="112.36" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="216.31" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="320.27" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="424.22" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">6.0</text>+<text x="49" y="192.79" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="143.13" text-anchor="end" fill="#7f8c8d" font-size="11">0.1</text>+<text x="49" y="93.46" text-anchor="end" fill="#7f8c8d" font-size="11">0.2</text>+<text x="49" y="43.80" text-anchor="end" fill="#7f8c8d" font-size="11">0.3</text>+<polyline points="72.50,188.81 75.26,188.70 78.01,188.57 80.77,188.39 83.52,188.17 86.28,187.90 89.04,187.55 91.79,187.12 94.55,186.60 97.30,185.96 100.06,185.18 102.81,184.25 105.57,183.15 108.33,181.85 111.08,180.33 113.84,178.56 116.59,176.53 119.35,174.22 122.11,171.60 124.86,168.67 127.62,165.40 130.37,161.80 133.13,157.87 135.89,153.60 138.64,149.02 141.40,144.14 144.15,138.99 146.91,133.60 149.67,128.01 152.42,122.26 155.18,116.40 157.93,110.49 160.69,104.58 163.44,98.73 166.20,92.98 168.96,87.40 171.71,82.03 174.47,76.91 177.22,72.09 179.98,67.58 182.74,63.43 185.49,59.65 188.25,56.25 191.00,53.23 193.76,50.58 196.52,48.31 199.27,46.40 202.03,44.83 204.78,43.59 207.54,42.65 210.30,41.98 213.05,41.56 215.81,41.37 218.56,41.38 221.32,41.57 224.07,41.90 226.83,42.36 229.59,42.93 232.34,43.57 235.10,44.27 237.85,45.01 240.61,45.78 243.37,46.56 246.12,47.33 248.88,48.10 251.63,48.85 254.39,49.58 257.15,50.29 259.90,50.99 262.66,51.69 265.41,52.39 268.17,53.11 270.93,53.87 273.68,54.68 276.44,55.56 279.19,56.54 281.95,57.64 284.70,58.87 287.46,60.27 290.22,61.84 292.97,63.62 295.73,65.61 298.48,67.83 301.24,70.29 304.00,73.01 306.75,75.98 309.51,79.22 312.26,82.71 315.02,86.45 317.78,90.42 320.53,94.62 323.29,99.01 326.04,103.56 328.80,108.26 331.56,113.06 334.31,117.93 337.07,122.83 339.82,127.72 342.58,132.56 345.33,137.31 348.09,141.94 350.85,146.40 353.60,150.68 356.36,154.74 359.11,158.57 361.87,162.14 364.63,165.45 367.38,168.50 370.14,171.27 372.89,173.77 375.65,176.01 378.41,178.00 381.16,179.76 383.92,181.29 386.67,182.62 389.43,183.76 392.19,184.74 394.94,185.56 397.70,186.25 400.45,186.82 403.21,187.29 405.96,187.68 408.72,187.99 411.48,188.25 414.23,188.45 416.99,188.61 419.74,188.73 422.50,188.83" fill="none" stroke="#3498db" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>+<text x="247.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Density (KDE)</text>+<rect x="455" y="39" width="12" height="12" fill="#3498db"/>+<text x="471" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/plotly-density.txt view
@@ -0,0 +1,14 @@+                                                        +                  [90mDensity (KDE)                         [0m+ [90m0.3 [0m[37m│              ⣀⡤⢤⣀⡀                    [0m[94m██         [0m+     [37m│            ⢀⠞⠁   ⠈⠙⠒⠢⢄⡀               [0m[94m██[0m[90mseries 0 [0m+     [37m│           ⢠⠊          ⠈⢦                         [0m+ [90m0.2 [0m[37m│          ⢀⠎             ⠣⡀                       [0m+     [37m│          ⡜               ⠱⡀                      [0m+     [37m│         ⡰⠁                ⢱                      [0m+ [90m0.1 [0m[37m│        ⢰⠁                  ⠱⡀                    [0m+     [37m│       ⢠⠃                    ⢣                    [0m+     [37m│      ⡠⠃                      ⠳⣀                  [0m+     [37m│ ⣀⣀⣠⠤⠚⠁                        ⠈⠲⠤⣀⣀⡀             [0m+ [90m0.0 [0m[37m┼───────────────────────────────────────           [0m+          [90m0.0       2.0        4.0       6.0            [0m
+ test/golden/plotly-distplot.svg view
@@ -0,0 +1,27 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 256" width="560" height="256" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="228.20" x2="440" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="118.49" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="217.73" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="316.97" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="416.20" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">6.0</text>+<text x="49" y="223.04" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="164.19" text-anchor="end" fill="#7f8c8d" font-size="11">1.0</text>+<text x="49" y="105.34" text-anchor="end" fill="#7f8c8d" font-size="11">2.0</text>+<text x="49" y="46.49" text-anchor="end" fill="#7f8c8d" font-size="11">3.0</text>+<rect x="170.10" y="42.83" width="15.88" height="176.55" fill="#3498db"/>+<rect x="189.94" y="42.83" width="15.88" height="176.55" fill="#3498db"/>+<rect x="209.79" y="101.68" width="15.88" height="117.70" fill="#3498db"/>+<rect x="229.64" y="42.83" width="15.88" height="176.55" fill="#3498db"/>+<rect x="249.48" y="160.52" width="15.88" height="58.85" fill="#3498db"/>+<rect x="269.33" y="42.83" width="15.88" height="176.55" fill="#3498db"/>+<rect x="289.18" y="101.68" width="15.88" height="117.70" fill="#3498db"/>+<rect x="309.03" y="42.83" width="15.88" height="176.55" fill="#3498db"/>+<polyline points="80.44,219.33 83.07,219.32 85.70,219.31 88.33,219.29 90.96,219.26 93.59,219.23 96.22,219.19 98.86,219.14 101.49,219.07 104.12,219.00 106.75,218.91 109.38,218.80 112.01,218.66 114.64,218.51 117.27,218.33 119.90,218.12 122.53,217.88 125.16,217.61 127.79,217.30 130.43,216.95 133.06,216.56 135.69,216.14 138.32,215.67 140.95,215.16 143.58,214.62 146.21,214.04 148.84,213.43 151.47,212.79 154.10,212.13 156.73,211.45 159.37,210.76 162.00,210.06 164.63,209.36 167.26,208.66 169.89,207.98 172.52,207.32 175.15,206.68 177.78,206.08 180.41,205.50 183.04,204.97 185.67,204.48 188.31,204.03 190.94,203.63 193.57,203.27 196.20,202.96 198.83,202.69 201.46,202.46 204.09,202.28 206.72,202.13 209.35,202.02 211.98,201.94 214.61,201.89 217.24,201.87 219.88,201.87 222.51,201.89 225.14,201.93 227.77,201.98 230.40,202.05 233.03,202.13 235.66,202.21 238.29,202.30 240.92,202.39 243.55,202.48 246.18,202.57 248.82,202.66 251.45,202.75 254.08,202.84 256.71,202.92 259.34,203.01 261.97,203.09 264.60,203.17 267.23,203.26 269.86,203.35 272.49,203.44 275.12,203.55 277.76,203.66 280.39,203.79 283.02,203.94 285.65,204.10 288.28,204.29 290.91,204.50 293.54,204.74 296.17,205.00 298.80,205.29 301.43,205.61 304.06,205.97 306.69,206.35 309.33,206.76 311.96,207.21 314.59,207.68 317.22,208.17 319.85,208.69 322.48,209.23 325.11,209.79 327.74,210.36 330.37,210.94 333.00,211.52 335.63,212.10 338.27,212.67 340.90,213.23 343.53,213.78 346.16,214.31 348.79,214.82 351.42,215.30 354.05,215.75 356.68,216.18 359.31,216.57 361.94,216.93 364.57,217.26 367.21,217.55 369.84,217.82 372.47,218.05 375.10,218.26 377.73,218.44 380.36,218.60 382.99,218.74 385.62,218.85 388.25,218.95 390.88,219.03 393.51,219.10 396.14,219.16 398.78,219.20 401.41,219.24 404.04,219.27 406.67,219.29 409.30,219.31 411.93,219.33 414.56,219.34" fill="none" stroke="#9b59b6" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>+<text x="247.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Distribution</text>+<rect x="455" y="39" width="12" height="12" fill="#3498db"/>+<text x="471" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+<rect x="455" y="56" width="12" height="12" fill="#9b59b6"/>+<text x="471" y="66" text-anchor="start" fill="#7f8c8d" font-size="11">series 1</text>+</svg>
+ test/golden/plotly-distplot.txt view
@@ -0,0 +1,16 @@+                                                        +                  [90mDistribution                          [0m+ [90m3.0 [0m[37m│           [0m[94m████ ███ ███ ███            ██         [0m+     [37m│           [0m[94m████ ███ ███ ███            [0m[95m██[0m[90mseries 0 [0m+     [37m│           [0m[94m████ ███ ███ ███            [0m[95m██[0m[90mseries 1 [0m+     [37m│           [0m[94m████ ███ ███ ███                       [0m+ [90m2.0 [0m[37m│           [0m[94m████████ ███████                       [0m+     [37m│           [0m[94m████████ ███████                       [0m+     [37m│           [0m[94m████████ ███████                       [0m+     [37m│           [0m[94m████████ ███████                       [0m+ [90m1.0 [0m[37m│           [0m[94m████████████████                       [0m+     [37m│           [0m[94m████████████████                       [0m+     [37m│           [0m[94m████████████████                       [0m+ [90m0.0 [0m[37m│  ⣀⣀⣀⣀⡠⠤⠤⠒⠊[0m[94m████████████████⠒⠲⠤⠤⣄⣀⣀⣀⣀              [0m+     [37m┼───────────────────────────────────────           [0m+          [90m0.0       2.0       4.0       6.0             [0m
+ test/golden/plotly-error-bars.svg view
@@ -0,0 +1,35 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 224" width="560" height="224" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="196.20" x2="440" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="174.58" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="320.42" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="49" y="178.84" text-anchor="end" fill="#7f8c8d" font-size="11">2.0</text>+<text x="49" y="124.23" text-anchor="end" fill="#7f8c8d" font-size="11">4.0</text>+<text x="49" y="69.62" text-anchor="end" fill="#7f8c8d" font-size="11">6.0</text>+<polyline points="101.67,188.83 101.67,156.06" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="90.00,188.83 113.33,188.83" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="90.00,156.06 113.33,156.06" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="174.58,153.33 174.58,115.10" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="162.92,153.33 186.25,153.33" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="162.92,115.10 186.25,115.10" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="247.50,104.18 247.50,71.41" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="235.83,104.18 259.17,104.18" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="235.83,71.41 259.17,71.41" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="320.42,120.56 320.42,82.33" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="308.75,120.56 332.08,120.56" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="308.75,82.33 332.08,82.33" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="393.33,85.06 393.33,41.37" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="381.67,85.06 405.00,85.06" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="381.67,41.37 405.00,41.37" fill="none" stroke="#3498db" stroke-width="1.50" stroke-linejoin="round" stroke-linecap="round"/>+<circle cx="101.67" cy="172.44" r="3" fill="#9b59b6"/>+<circle cx="174.58" cy="134.21" r="3" fill="#9b59b6"/>+<circle cx="247.50" cy="87.79" r="3" fill="#9b59b6"/>+<circle cx="320.42" cy="101.45" r="3" fill="#9b59b6"/>+<circle cx="393.33" cy="63.22" r="3" fill="#9b59b6"/>+<text x="247.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Measurements ± CI</text>+<rect x="455" y="39" width="12" height="12" fill="#3498db"/>+<text x="471" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+<rect x="455" y="56" width="12" height="12" fill="#9b59b6"/>+<text x="471" y="66" text-anchor="start" fill="#7f8c8d" font-size="11">series 1</text>+</svg>
+ test/golden/plotly-error-bars.txt view
@@ -0,0 +1,14 @@+                                                        +                [90mMeasurements ± CI                       [0m+     [37m│                                ⠤⢤⠤    [0m[94m██         [0m+     [37m│                                 [0m[95m⢸     ██[0m[90mseries 0 [0m+ [90m6.0 [0m[37m│                 ⠠⢤⠤⠄            [0m[95m⢹⠁    ██[0m[90mseries 1 [0m+     [37m│                  [0m[95m⢼⠄     ⠒⡖⠂    ⠒⠚⠒               [0m+     [37m│                 ⠠⠼⠤⠄    [0m[95m⠐⡗                       [0m+ [90m4.0 [0m[37m│          ⠐⢲⠒            ⠤⠧⠄                      [0m+     [37m│           [0m[95m⢼⠄                                     [0m+     [37m│   ⣀⣀⣀    ⠠⠼⠤                                     [0m+     [37m│   [0m[95m⢀⣇                                             [0m+ [90m2.0 [0m[37m│   ⣀[0m[95m⣇⣀                                            [0m+     [37m┼───────────────────────────────────────           [0m+                [90m2.0            4.0                      [0m
+ test/golden/plotly-funnel.svg view
@@ -0,0 +1,19 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 224" width="600" height="224" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="196.20" x2="480" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="74.32" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="267.50" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">500.0</text>+<text x="460.68" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1000.0</text>+<text x="49" y="57.33" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="118.77" text-anchor="end" fill="#7f8c8d" font-size="11">2.0</text>+<text x="49" y="180.21" text-anchor="end" fill="#7f8c8d" font-size="11">4.0</text>+<rect x="74.32" y="41.37" width="386.36" height="24.58" fill="#3498db"/>+<rect x="74.32" y="72.09" width="278.18" height="24.58" fill="#3498db"/>+<rect x="74.32" y="102.81" width="185.45" height="24.58" fill="#3498db"/>+<rect x="74.32" y="133.53" width="85.00" height="24.58" fill="#3498db"/>+<rect x="74.32" y="164.25" width="46.36" height="24.58" fill="#3498db"/>+<text x="267.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Sales funnel</text>+<rect x="495" y="39" width="12" height="12" fill="#3498db"/>+<text x="511" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/plotly-funnel.txt view
@@ -0,0 +1,14 @@+                                                            +                    [90mSales funnel                            [0m+     [37m│ [0m[94m███████████████████████████████████████   ██         [0m+ [90m0.0 [0m[37m│ [0m[94m███████████████████████████████████████   ██[0m[90mseries 0 [0m+     [37m│ [0m[94m███████████████████████████████████████              [0m+     [37m│ [0m[94m█████████████████████████████                        [0m+     [37m│ [0m[94m███████████████████                                  [0m+ [90m2.0 [0m[37m│ [0m[94m███████████████████                                  [0m+     [37m│ [0m[94m█████████                                            [0m+     [37m│ [0m[94m█████████                                            [0m+     [37m│ [0m[94m█████                                                [0m+ [90m4.0 [0m[37m│ [0m[94m█████                                                [0m+     [37m┼───────────────────────────────────────────           [0m+      [90m0.0               500.0              1000.0           [0m
+ test/golden/plotly-heatmap-annotated.svg view
@@ -0,0 +1,64 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 256" width="480" height="256" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="228.20" x2="360" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="91.97" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="207.50" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="323.03" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="49" y="208.33" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="134.77" text-anchor="end" fill="#7f8c8d" font-size="11">2.0</text>+<text x="49" y="61.21" text-anchor="end" fill="#7f8c8d" font-size="11">4.0</text>+<rect x="63.09" y="186.27" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="63.09" y="149.49" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="63.09" y="112.71" width="57.77" height="36.78" fill="#1abc9c"/>+<rect x="63.09" y="75.93" width="57.77" height="36.78" fill="#3498db"/>+<rect x="63.09" y="39.15" width="57.77" height="36.78" fill="#2980b9"/>+<rect x="120.85" y="186.27" width="57.77" height="36.78" fill="#f1c40f"/>+<rect x="120.85" y="149.49" width="57.77" height="36.78" fill="#f1c40f"/>+<rect x="120.85" y="112.71" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="120.85" y="75.93" width="57.77" height="36.78" fill="#1abc9c"/>+<rect x="120.85" y="39.15" width="57.77" height="36.78" fill="#3498db"/>+<rect x="178.62" y="186.27" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="178.62" y="149.49" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="178.62" y="112.71" width="57.77" height="36.78" fill="#f1c40f"/>+<rect x="178.62" y="75.93" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="178.62" y="39.15" width="57.77" height="36.78" fill="#1abc9c"/>+<rect x="236.38" y="186.27" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="236.38" y="149.49" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="236.38" y="112.71" width="57.77" height="36.78" fill="#f1c40f"/>+<rect x="236.38" y="75.93" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="236.38" y="39.15" width="57.77" height="36.78" fill="#1abc9c"/>+<rect x="294.15" y="186.27" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="294.15" y="149.49" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="294.15" y="112.71" width="57.77" height="36.78" fill="#f1c40f"/>+<rect x="294.15" y="75.93" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="294.15" y="39.15" width="57.77" height="36.78" fill="#1abc9c"/>+<text x="91.97" y="204.66" text-anchor="middle" fill="#9b59b6" font-size="11">1.0</text>+<text x="91.97" y="167.88" text-anchor="middle" fill="#9b59b6" font-size="11">0.9</text>+<text x="91.97" y="131.10" text-anchor="middle" fill="#9b59b6" font-size="11">0.5</text>+<text x="91.97" y="94.32" text-anchor="middle" fill="#9b59b6" font-size="11">0.1</text>+<text x="91.97" y="57.54" text-anchor="middle" fill="#9b59b6" font-size="11">-0.4</text>+<text x="149.73" y="204.66" text-anchor="middle" fill="#9b59b6" font-size="11">1.5</text>+<text x="149.73" y="167.88" text-anchor="middle" fill="#9b59b6" font-size="11">1.4</text>+<text x="149.73" y="131.10" text-anchor="middle" fill="#9b59b6" font-size="11">1.0</text>+<text x="149.73" y="94.32" text-anchor="middle" fill="#9b59b6" font-size="11">0.6</text>+<text x="149.73" y="57.54" text-anchor="middle" fill="#9b59b6" font-size="11">0.1</text>+<text x="207.50" y="204.66" text-anchor="middle" fill="#9b59b6" font-size="11">1.8</text>+<text x="207.50" y="167.88" text-anchor="middle" fill="#9b59b6" font-size="11">1.7</text>+<text x="207.50" y="131.10" text-anchor="middle" fill="#9b59b6" font-size="11">1.4</text>+<text x="207.50" y="94.32" text-anchor="middle" fill="#9b59b6" font-size="11">0.9</text>+<text x="207.50" y="57.54" text-anchor="middle" fill="#9b59b6" font-size="11">0.4</text>+<text x="265.27" y="204.66" text-anchor="middle" fill="#9b59b6" font-size="11">2.0</text>+<text x="265.27" y="167.88" text-anchor="middle" fill="#9b59b6" font-size="11">1.9</text>+<text x="265.27" y="131.10" text-anchor="middle" fill="#9b59b6" font-size="11">1.5</text>+<text x="265.27" y="94.32" text-anchor="middle" fill="#9b59b6" font-size="11">1.1</text>+<text x="265.27" y="57.54" text-anchor="middle" fill="#9b59b6" font-size="11">0.6</text>+<text x="323.03" y="204.66" text-anchor="middle" fill="#9b59b6" font-size="11">1.9</text>+<text x="323.03" y="167.88" text-anchor="middle" fill="#9b59b6" font-size="11">1.8</text>+<text x="323.03" y="131.10" text-anchor="middle" fill="#9b59b6" font-size="11">1.4</text>+<text x="323.03" y="94.32" text-anchor="middle" fill="#9b59b6" font-size="11">1.0</text>+<text x="323.03" y="57.54" text-anchor="middle" fill="#9b59b6" font-size="11">0.5</text>+<text x="207.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Annotated heatmap</text>+<rect x="375" y="39" width="12" height="12" fill="#3498db"/>+<text x="391" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/plotly-heatmap-annotated.txt view
@@ -0,0 +1,16 @@+                                                +            [90mAnnotated heatmap                   [0m+     [37m│[0m[34m██████[0m[94m█████[0m[96m███████████████████ [0m[94m██         [0m+ [90m4.0 [0m[37m│[0m[34m█[0m[95m-0.4[0m[34m█[0m[94m█[0m[95m0.1[0m[94m█[0m[96m██[0m[95m0.4[0m[96m███[0m[95m0.6[0m[96m███[0m[95m0.5[0m[96m██ [0m[94m██[0m[90mseries 0 [0m+     [37m│[0m[34m██████[0m[94m█████[0m[96m███████████████████            [0m+     [37m│[0m[94m██[0m[95m0.1[0m[94m█[0m[96m█[0m[95m0.6[0m[96m█[0m[92m██[0m[95m0.9[0m[92m███[0m[95m1.1[0m[92m███[0m[95m1.0[0m[92m██            [0m+     [37m│[0m[94m██████[0m[96m█████[0m[92m███████████████████            [0m+     [37m│[0m[96m██████[0m[92m█████[0m[93m███████████████████            [0m+ [90m2.0 [0m[37m│[0m[96m██[0m[95m0.5[0m[96m█[0m[92m█[0m[95m1.0[0m[92m█[0m[93m██[0m[95m1.4[0m[93m███[0m[95m1.5[0m[93m███[0m[95m1.4[0m[93m██            [0m+     [37m│[0m[96m██████[0m[92m█████[0m[93m███████████████████            [0m+     [37m│[0m[92m██[0m[95m0.9[0m[92m█[0m[93m█[0m[95m1.4[0m[93m█[0m[91m██[0m[95m1.7[0m[91m███[0m[95m1.9[0m[91m███[0m[95m1.8[0m[91m██            [0m+     [37m│[0m[92m██████[0m[93m█████[0m[91m███████████████████            [0m+     [37m│[0m[92m██[0m[95m1.0[0m[92m█[0m[93m█[0m[95m1.5[0m[93m█[0m[91m██[0m[95m1.8[0m[91m███[0m[95m2.0[0m[91m███[0m[95m1.9[0m[91m██            [0m+ [90m0.0 [0m[37m│[0m[92m██████[0m[93m█████[0m[91m███████████████████            [0m+     [37m┼───────────────────────────────           [0m+        [90m0.0        2.0         4.0              [0m
+ test/golden/plotly-heatmap.svg view
@@ -0,0 +1,39 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 256" width="480" height="256" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="228.20" x2="360" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="228.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="91.97" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="207.50" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="323.03" y="243.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="49" y="208.33" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="134.77" text-anchor="end" fill="#7f8c8d" font-size="11">2.0</text>+<text x="49" y="61.21" text-anchor="end" fill="#7f8c8d" font-size="11">4.0</text>+<rect x="63.09" y="186.27" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="63.09" y="149.49" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="63.09" y="112.71" width="57.77" height="36.78" fill="#1abc9c"/>+<rect x="63.09" y="75.93" width="57.77" height="36.78" fill="#3498db"/>+<rect x="63.09" y="39.15" width="57.77" height="36.78" fill="#2980b9"/>+<rect x="120.85" y="186.27" width="57.77" height="36.78" fill="#f1c40f"/>+<rect x="120.85" y="149.49" width="57.77" height="36.78" fill="#f1c40f"/>+<rect x="120.85" y="112.71" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="120.85" y="75.93" width="57.77" height="36.78" fill="#1abc9c"/>+<rect x="120.85" y="39.15" width="57.77" height="36.78" fill="#3498db"/>+<rect x="178.62" y="186.27" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="178.62" y="149.49" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="178.62" y="112.71" width="57.77" height="36.78" fill="#f1c40f"/>+<rect x="178.62" y="75.93" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="178.62" y="39.15" width="57.77" height="36.78" fill="#1abc9c"/>+<rect x="236.38" y="186.27" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="236.38" y="149.49" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="236.38" y="112.71" width="57.77" height="36.78" fill="#f1c40f"/>+<rect x="236.38" y="75.93" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="236.38" y="39.15" width="57.77" height="36.78" fill="#1abc9c"/>+<rect x="294.15" y="186.27" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="294.15" y="149.49" width="57.77" height="36.78" fill="#e74c3c"/>+<rect x="294.15" y="112.71" width="57.77" height="36.78" fill="#f1c40f"/>+<rect x="294.15" y="75.93" width="57.77" height="36.78" fill="#2ecc71"/>+<rect x="294.15" y="39.15" width="57.77" height="36.78" fill="#1abc9c"/>+<text x="207.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Heatmap</text>+<rect x="375" y="39" width="12" height="12" fill="#3498db"/>+<text x="391" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/plotly-heatmap.txt view
@@ -0,0 +1,16 @@+                                                +                 [90mHeatmap                        [0m+     [37m│[0m[34m██████[0m[94m█████[0m[96m███████████████████ [0m[94m██         [0m+ [90m4.0 [0m[37m│[0m[34m██████[0m[94m█████[0m[96m███████████████████ [0m[94m██[0m[90mseries 0 [0m+     [37m│[0m[34m██████[0m[94m█████[0m[96m███████████████████            [0m+     [37m│[0m[94m██████[0m[96m█████[0m[92m███████████████████            [0m+     [37m│[0m[94m██████[0m[96m█████[0m[92m███████████████████            [0m+     [37m│[0m[96m██████[0m[92m█████[0m[93m███████████████████            [0m+ [90m2.0 [0m[37m│[0m[96m██████[0m[92m█████[0m[93m███████████████████            [0m+     [37m│[0m[96m██████[0m[92m█████[0m[93m███████████████████            [0m+     [37m│[0m[92m██████[0m[93m█████[0m[91m███████████████████            [0m+     [37m│[0m[92m██████[0m[93m█████[0m[91m███████████████████            [0m+     [37m│[0m[92m██████[0m[93m█████[0m[91m███████████████████            [0m+ [90m0.0 [0m[37m│[0m[92m██████[0m[93m█████[0m[91m███████████████████            [0m+     [37m┼───────────────────────────────           [0m+        [90m0.0        2.0         4.0              [0m
+ test/golden/plotly-waterfall.svg view
@@ -0,0 +1,20 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 224" width="600" height="224" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="196.20" x2="480" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="196.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="100.96" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="234.19" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="367.42" y="211.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="49" y="192.49" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="116.88" text-anchor="end" fill="#7f8c8d" font-size="11">100.0</text>+<text x="49" y="41.26" text-anchor="end" fill="#7f8c8d" font-size="11">200.0</text>+<rect x="74.32" y="113.21" width="53.29" height="75.62" fill="#3498db"/>+<rect x="140.93" y="90.52" width="53.29" height="22.69" fill="#3498db"/>+<rect x="207.55" y="52.72" width="53.29" height="37.81" fill="#3498db"/>+<rect x="274.16" y="52.72" width="53.29" height="3.78" fill="#3498db"/>+<rect x="340.78" y="41.37" width="53.29" height="15.12" fill="#3498db"/>+<rect x="407.39" y="41.37" width="53.29" height="147.45" fill="#3498db"/>+<text x="267.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Waterfall</text>+<rect x="495" y="39" width="12" height="12" fill="#3498db"/>+<text x="511" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">series 0</text>+</svg>
+ test/golden/plotly-waterfall.txt view
@@ -0,0 +1,14 @@+                                                            +                      [90mWaterfall                             [0m+[90m00.0 [0m[37m│                            [0m[94m████████████   ██         [0m+     [37m│              [0m[94m██████ ██████ ████████████   ██[0m[90mseries 0 [0m+     [37m│              [0m[94m██████              ██████              [0m+     [37m│        [0m[94m████████████              ██████              [0m+     [37m│        [0m[94m██████                    ██████              [0m+[90m00.0 [0m[37m│ [0m[94m██████ ██████                    ██████              [0m+     [37m│ [0m[94m██████                           ██████              [0m+     [37m│ [0m[94m██████                           ██████              [0m+     [37m│ [0m[94m██████                           ██████              [0m+     [37m│ [0m[94m██████                           ██████              [0m+ [90m0.0 [0m[37m┼───────────────────────────────────────────           [0m+         [90m0.0          2.0          4.0                      [0m
+ test/golden/polar-rose.svg view
@@ -0,0 +1,22 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 320" width="400" height="320" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<polyline points="172.61,163.10 172.59,163.60 172.52,164.10 172.39,164.58 172.22,165.06 172.01,165.51 171.75,165.94 171.45,166.34 171.12,166.72 170.74,167.05 170.34,167.35 169.91,167.61 169.46,167.82 168.98,167.99 168.50,168.12 168.00,168.19 167.50,168.21 167.00,168.19 166.50,168.12 166.02,167.99 165.54,167.82 165.09,167.61 164.66,167.35 164.26,167.05 163.88,166.72 163.55,166.34 163.25,165.94 162.99,165.51 162.78,165.06 162.61,164.58 162.48,164.10 162.41,163.60 162.39,163.10 162.41,162.60 162.48,162.10 162.61,161.62 162.78,161.14 162.99,160.69 163.25,160.26 163.55,159.86 163.88,159.48 164.26,159.15 164.66,158.85 165.09,158.59 165.54,158.38 166.02,158.21 166.50,158.08 167.00,158.01 167.50,157.99 168.00,158.01 168.50,158.08 168.98,158.21 169.46,158.38 169.91,158.59 170.34,158.85 170.74,159.15 171.12,159.48 171.45,159.86 171.75,160.26 172.01,160.69 172.22,161.14 172.39,161.62 172.52,162.10 172.59,162.60 172.61,163.10" fill="none" stroke="#bdc3c7" stroke-width="0.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="223.75,163.10 223.48,168.61 222.67,174.07 221.33,179.43 219.47,184.63 217.11,189.62 214.27,194.35 210.98,198.78 207.27,202.87 203.18,206.58 198.75,209.87 194.02,212.71 189.03,215.07 183.83,216.93 178.47,218.27 173.01,219.08 167.50,219.35 161.99,219.08 156.53,218.27 151.17,216.93 145.97,215.07 140.98,212.71 136.25,209.87 131.82,206.58 127.73,202.87 124.02,198.78 120.73,194.35 117.89,189.62 115.53,184.63 113.67,179.43 112.33,174.07 111.52,168.61 111.25,163.10 111.52,157.59 112.33,152.13 113.67,146.77 115.53,141.57 117.89,136.58 120.73,131.85 124.02,127.42 127.73,123.33 131.82,119.62 136.25,116.33 140.98,113.49 145.97,111.13 151.17,109.27 156.53,107.93 161.99,107.12 167.50,106.85 173.01,107.12 178.47,107.93 183.83,109.27 189.03,111.13 194.02,113.49 198.75,116.33 203.18,119.62 207.27,123.33 210.98,127.42 214.27,131.85 217.11,136.58 219.47,141.57 221.33,146.77 222.67,152.13 223.48,157.59 223.75,163.10" fill="none" stroke="#bdc3c7" stroke-width="0.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="274.89,163.10 274.37,173.63 272.82,184.05 270.26,194.27 266.71,204.19 262.21,213.72 256.79,222.76 250.51,231.23 243.43,239.03 235.63,246.11 227.16,252.39 218.12,257.81 208.59,262.31 198.67,265.86 188.45,268.42 178.03,269.97 167.50,270.49 156.97,269.97 146.55,268.42 136.33,265.86 126.41,262.31 116.88,257.81 107.84,252.39 99.37,246.11 91.57,239.03 84.49,231.23 78.21,222.76 72.79,213.72 68.29,204.19 64.74,194.27 62.18,184.05 60.63,173.63 60.11,163.10 60.63,152.57 62.18,142.15 64.74,131.93 68.29,122.01 72.79,112.48 78.21,103.44 84.49,94.97 91.57,87.17 99.37,80.09 107.84,73.81 116.88,68.39 126.41,63.89 136.33,60.34 146.55,57.78 156.97,56.23 167.50,55.71 178.03,56.23 188.45,57.78 198.67,60.34 208.59,63.89 218.12,68.39 227.16,73.81 235.63,80.09 243.43,87.17 250.51,94.97 256.79,103.44 262.21,112.48 266.71,122.01 270.26,131.93 272.82,142.15 274.37,152.57 274.89,163.10" fill="none" stroke="#bdc3c7" stroke-width="0.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="167.50,163.10 275.44,131.41" fill="none" stroke="#bdc3c7" stroke-width="0.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="167.50,163.10 110.34,66.20" fill="none" stroke="#bdc3c7" stroke-width="0.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="167.50,163.10 87.55,242.25" fill="none" stroke="#bdc3c7" stroke-width="0.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="167.50,163.10 263.82,221.23" fill="none" stroke="#bdc3c7" stroke-width="0.50" stroke-linejoin="round" stroke-linecap="round"/>+<polyline points="280,163.10 279.46,174.13 277.84,185.05 275.16,195.76 271.44,206.15 266.72,216.13 261.04,225.60 254.46,234.47 247.05,242.65 238.87,250.06 230.00,256.64 220.53,262.32 210.55,267.04 200.16,270.76 189.45,273.44 178.53,275.06 167.50,275.60 156.47,275.06 145.55,273.44 134.84,270.76 124.45,267.04 114.47,262.32 105.00,256.64 96.13,250.06 87.95,242.65 80.54,234.47 73.96,225.60 68.28,216.13 63.56,206.15 59.84,195.76 57.16,185.05 55.54,174.13 55,163.10 55.54,152.07 57.16,141.15 59.84,130.44 63.56,120.05 68.28,110.07 73.96,100.60 80.54,91.73 87.95,83.55 96.13,76.14 105.00,69.56 114.47,63.88 124.45,59.16 134.84,55.44 145.55,52.76 156.47,51.14 167.50,50.60 178.53,51.14 189.45,52.76 200.16,55.44 210.55,59.16 220.53,63.88 230.00,69.56 238.87,76.14 247.05,83.55 254.46,91.73 261.04,100.60 266.72,110.07 271.44,120.05 275.16,130.44 277.84,141.15 279.46,152.07 280,163.10" fill="none" stroke="#ecf0f1" stroke-width="1" stroke-linejoin="round" stroke-linecap="round"/>+<text x="285.04" y="132.25" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="105.26" y="61.26" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="80.45" y="252.95" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="272.38" y="230.07" text-anchor="middle" fill="#7f8c8d" font-size="11">6.0</text>+<text x="175.61" y="161.10" text-anchor="start" fill="#7f8c8d" font-size="11">0.0</text>+<text x="226.75" y="161.10" text-anchor="start" fill="#7f8c8d" font-size="11">0.5</text>+<text x="277.89" y="161.10" text-anchor="start" fill="#7f8c8d" font-size="11">1.0</text>+<polyline points="172.41,161.66 207.07,143.29 229.49,116.70 235.37,90.20 225.56,72.76 205.62,71.08 183.96,87.44 169.08,118.88 166.77,158.04 153.53,121.11 130.39,95.14 105.00,85.55 86.34,92.78 81.84,112.28 94.95,136.04 123.95,155.24 162.39,163.10 123.95,170.96 94.95,190.16 81.84,213.92 86.34,233.42 105.00,240.65 130.39,231.06 153.53,205.09 166.77,168.16 169.08,207.32 183.96,238.76 205.62,255.12 225.56,253.44 235.37,236.00 229.49,209.50 207.07,182.91 172.41,164.54" fill="none" stroke="#3498db" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>+<text x="167.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Polar rose</text>+<rect x="295" y="39" width="12" height="12" fill="#3498db"/>+<text x="311" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">r = |sin(2θ)|</text>+</svg>
+ test/golden/polar-rose.txt view
@@ -0,0 +1,20 @@+                                        +           [90mPolar rose                   [0m+                             [94m██         [0m+         [90m2.0⣀⣤⠶⠶⠶⠶⠶⢦⣄⡀       [0m[94m██[0m[90mr = |sin([0m+         ⢀⢔⢟⠉       ⡨⠽⢷⡢⡀               +        ⣠⡾⠕⠬⣆     ⢠⠊   ⢫⠺⣄              +       ⡰⡝   ⠈⢗⡄⣀⣀⣠⣃    ⡸ ⠱⣇             +      ⢠⡟⠱⡀   ⡜⢏⢢ ⡎ ⠉⢢ ⢠⠃  ⢫⡆            +     ⢀⡷⠁ ⠱⡀⢀⠎ ⠈⢎⣆⠇   ⡵⡁ ⢀⣀⠤[90m0.0          [0m+     ⢸⡇   ⠈⡞⢄⡀ ⠈⢿⣀⡠⣔⡪⠤⢳⠉⠁  ⢸⡇           +     ⢸⡇    ⡇⢀⡨⠭⠕⣺[90m0.0  0.5  1.0          [0m+     ⢸⡇   ⡠⠵⡁ ⢀⠔⡿⡀ ⠑⠢⣀⠎    ⢸⡇           +      ⢿⡀⢀⠎  ⠣⡠⠃⢰⠁⡇  ⢀⠜⢕⢄⡀ ⢠⡻            +      ⠘⡵⡎  ⢀⠎⠉⡢⢇⣀⣣⡠⠔⠉  ⡇⠈⢢⣷⠁            +       ⠘⣿⣀⡔⢁⡠⠜    ⢇    ⢸⢀⢮[90m6.0           [0m+       [90m4.0⡭⡁      ⠈⠢⡀⢀⣀⡮⡞⠁              [0m+          ⠈⠪⢦⡤⢄⣀⡀ ⣀⣀⠬⣕⠕⠉                +             ⠈⠉⠑⠚⠛⠒⠉⠉                   +                                        +                                        
+ test/golden/primitives.svg view
@@ -0,0 +1,8 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 120" width="240" height="120" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<rect x="10" y="10" width="40" height="30" fill="#3498db"/>+<circle cx="120" cy="50" r="12" fill="#e74c3c"/>+<polyline points="20,100 80,80 140,90 220,70" fill="none" stroke="#2ecc71" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>+<text x="120" y="25" text-anchor="start" fill="#f1c40f" font-size="11">Primitives</text>+<path d="M 200 50 L 220 50 A 20 20 0 0 0 201.41 69.95 Z" fill="#9b59b6"/>+</svg>
+ test/golden/primitives.txt view
@@ -0,0 +1,8 @@+ [94m████                   [0m+ [94m████       [0m[93mPrimitives  [0m+ [94m████      [0m[91m⢠⣦           [0m+           [91m⠹⡿⠁       [0m[95m⢀⡇ [0m+                   ⣀[95m⣒⠮⠄ [0m+    ⣀⠤⠔⠊⠉⠑⠒⠒⠢⠤⠤⠒⠒⠉⠉     +  ⠒⠉                    +                        
+ test/golden/scatter-basic.svg view
@@ -0,0 +1,29 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 320" width="600" height="320" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="292.20" x2="480" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="74.32" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="170.91" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="267.50" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="364.09" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">3.0</text>+<text x="460.68" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="49" y="284.13" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="210.78" text-anchor="end" fill="#7f8c8d" font-size="11">5.0</text>+<text x="49" y="137.43" text-anchor="end" fill="#7f8c8d" font-size="11">10.0</text>+<text x="49" y="64.07" text-anchor="end" fill="#7f8c8d" font-size="11">15.0</text>+<circle cx="74.32" cy="280.46" r="3" fill="#3498db"/>+<circle cx="170.91" cy="265.79" r="3" fill="#3498db"/>+<circle cx="267.50" cy="221.78" r="3" fill="#3498db"/>+<circle cx="364.09" cy="148.43" r="3" fill="#3498db"/>+<circle cx="460.68" cy="45.74" r="3" fill="#3498db"/>+<circle cx="74.32" cy="265.79" r="3" fill="#9b59b6"/>+<circle cx="170.91" cy="236.45" r="3" fill="#9b59b6"/>+<circle cx="267.50" cy="192.44" r="3" fill="#9b59b6"/>+<circle cx="364.09" cy="133.76" r="3" fill="#9b59b6"/>+<circle cx="460.68" cy="60.41" r="3" fill="#9b59b6"/>+<text x="267.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Random points</text>+<rect x="495" y="39" width="12" height="12" fill="#3498db"/>+<text x="511" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">A</text>+<rect x="495" y="56" width="12" height="12" fill="#9b59b6"/>+<text x="511" y="66" text-anchor="start" fill="#7f8c8d" font-size="11">B</text>+</svg>
+ test/golden/scatter-basic.txt view
@@ -0,0 +1,20 @@+                                                            +                    [90mRandom points                           [0m+     [37m│                                       [0m[94m⢀⣄  ██         [0m+     [37m│                                       [0m[95m⢀⣅  ██[0m[90mA        [0m+[90m15.0 [0m[37m│                                        [0m[95m⠁  ██[0m[90mB        [0m+     [37m│                                                      [0m+     [37m│                                                      [0m+     [37m│                                                      [0m+[90m10.0 [0m[37m│                              [0m[95m⠺⠂                      [0m+     [37m│                              [0m[94m⠺⠂                      [0m+     [37m│                                                      [0m+     [37m│                    [0m[95m⢀                                 [0m+     [37m│                    [0m[95m⠙⠁                                [0m+ [90m5.0 [0m[37m│                    [0m[94m⣠⡀                                [0m+     [37m│          [0m[95m⢀⣄        [0m[94m⠈                                 [0m+     [37m│           [0m[95m⠁                                          [0m+     [37m│ [0m[95m⢴⠄       [0m[94m⠠⡦                                          [0m+ [90m0.0 [0m[37m│ [0m[94m⢴⠄                                                   [0m+     [37m┼───────────────────────────────────────────           [0m+      [90m0.0       1.0      2.0       3.0       4.0            [0m
+ test/golden/scatter-log-y.svg view
@@ -0,0 +1,28 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 320" width="600" height="320" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="292.20" x2="480" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="74.32" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="170.91" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="267.50" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="364.09" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">3.0</text>+<text x="460.68" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="49" y="221.86" text-anchor="end" fill="#7f8c8d" font-size="11">0.1</text>+<text x="49" y="143.62" text-anchor="end" fill="#7f8c8d" font-size="11">1.0</text>+<text x="49" y="65.37" text-anchor="end" fill="#7f8c8d" font-size="11">10.0</text>+<circle cx="74.32" cy="280.46" r="3" fill="#3498db"/>+<circle cx="170.91" cy="139.95" r="3" fill="#3498db"/>+<circle cx="267.50" cy="92.84" r="3" fill="#3498db"/>+<circle cx="364.09" cy="65.29" r="3" fill="#3498db"/>+<circle cx="460.68" cy="45.74" r="3" fill="#3498db"/>+<circle cx="74.32" cy="139.95" r="3" fill="#9b59b6"/>+<circle cx="170.91" cy="102.62" r="3" fill="#9b59b6"/>+<circle cx="267.50" cy="79.07" r="3" fill="#9b59b6"/>+<circle cx="364.09" cy="61.71" r="3" fill="#9b59b6"/>+<circle cx="460.68" cy="47.93" r="3" fill="#9b59b6"/>+<text x="267.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Log Y</text>+<rect x="495" y="39" width="12" height="12" fill="#3498db"/>+<text x="511" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">A</text>+<rect x="495" y="56" width="12" height="12" fill="#9b59b6"/>+<text x="511" y="66" text-anchor="start" fill="#7f8c8d" font-size="11">B</text>+</svg>
+ test/golden/scatter-no-title.svg view
@@ -0,0 +1,28 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 320" width="600" height="320" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="292.20" x2="480" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="10" x2="55" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="74.32" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="170.91" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="267.50" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="364.09" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">3.0</text>+<text x="460.68" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="49" y="283.04" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="202.87" text-anchor="end" fill="#7f8c8d" font-size="11">5.0</text>+<text x="49" y="122.70" text-anchor="end" fill="#7f8c8d" font-size="11">10.0</text>+<text x="49" y="42.53" text-anchor="end" fill="#7f8c8d" font-size="11">15.0</text>+<circle cx="74.32" cy="279.37" r="3" fill="#3498db"/>+<circle cx="170.91" cy="263.34" r="3" fill="#3498db"/>+<circle cx="267.50" cy="215.24" r="3" fill="#3498db"/>+<circle cx="364.09" cy="135.07" r="3" fill="#3498db"/>+<circle cx="460.68" cy="22.83" r="3" fill="#3498db"/>+<circle cx="74.32" cy="263.34" r="3" fill="#9b59b6"/>+<circle cx="170.91" cy="231.27" r="3" fill="#9b59b6"/>+<circle cx="267.50" cy="183.17" r="3" fill="#9b59b6"/>+<circle cx="364.09" cy="119.03" r="3" fill="#9b59b6"/>+<circle cx="460.68" cy="38.86" r="3" fill="#9b59b6"/>+<rect x="495" y="15" width="12" height="12" fill="#3498db"/>+<text x="511" y="25" text-anchor="start" fill="#7f8c8d" font-size="11">A</text>+<rect x="495" y="32" width="12" height="12" fill="#9b59b6"/>+<text x="511" y="42" text-anchor="start" fill="#7f8c8d" font-size="11">B</text>+</svg>
+ test/golden/scatter-responsive.svg view
@@ -0,0 +1,29 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 320" preserveAspectRatio="xMidYMid meet" width="100%" style="height:auto" font-family="system-ui, -apple-system, sans-serif">+<rect width="100%" height="100%" fill="white"/>+<line x1="55" y1="292.20" x2="392" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<line x1="55" y1="34" x2="55" y2="292.20" stroke="#ecf0f1" stroke-width="1"/>+<text x="70.32" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">0.0</text>+<text x="146.91" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">1.0</text>+<text x="223.50" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">2.0</text>+<text x="300.09" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">3.0</text>+<text x="376.68" y="307.20" text-anchor="middle" fill="#7f8c8d" font-size="11">4.0</text>+<text x="49" y="284.13" text-anchor="end" fill="#7f8c8d" font-size="11">0.0</text>+<text x="49" y="210.78" text-anchor="end" fill="#7f8c8d" font-size="11">5.0</text>+<text x="49" y="137.43" text-anchor="end" fill="#7f8c8d" font-size="11">10.0</text>+<text x="49" y="64.07" text-anchor="end" fill="#7f8c8d" font-size="11">15.0</text>+<circle cx="70.32" cy="280.46" r="3" fill="#3498db"/>+<circle cx="146.91" cy="265.79" r="3" fill="#3498db"/>+<circle cx="223.50" cy="221.78" r="3" fill="#3498db"/>+<circle cx="300.09" cy="148.43" r="3" fill="#3498db"/>+<circle cx="376.68" cy="45.74" r="3" fill="#3498db"/>+<circle cx="70.32" cy="265.79" r="3" fill="#9b59b6"/>+<circle cx="146.91" cy="236.45" r="3" fill="#9b59b6"/>+<circle cx="223.50" cy="192.44" r="3" fill="#9b59b6"/>+<circle cx="300.09" cy="133.76" r="3" fill="#9b59b6"/>+<circle cx="376.68" cy="60.41" r="3" fill="#9b59b6"/>+<text x="223.50" y="26" text-anchor="middle" fill="#7f8c8d" font-size="14">Responsive</text>+<rect x="407" y="39" width="12" height="12" fill="#3498db"/>+<text x="423" y="49" text-anchor="start" fill="#7f8c8d" font-size="11">A</text>+<rect x="407" y="56" width="12" height="12" fill="#9b59b6"/>+<text x="423" y="66" text-anchor="start" fill="#7f8c8d" font-size="11">B</text>+</svg>