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 +6/−0
- app/GenerateTutorial.hs +0/−796
- granite.cabal +7/−16
- src/Granite/Color.hs +159/−41
- src/Granite/Render/Pipeline.hs +92/−27
- test/GraniteSpec.hs +1/−0
- test/PipelineSpec.hs +67/−0
- test/golden/bar-basic.svg +20/−0
- test/golden/bar-basic.txt +14/−0
- test/golden/box-basic.svg +25/−0
- test/golden/box-basic.txt +16/−0
- test/golden/empty.svg +11/−0
- test/golden/facet-wrap.svg +51/−0
- test/golden/facet-wrap.txt +18/−0
- test/golden/hist-basic.svg +23/−0
- test/golden/hist-basic.txt +14/−0
- test/golden/line-basic.svg +16/−0
- test/golden/line-basic.txt +20/−0
- test/golden/line-coord-flip.svg +19/−0
- test/golden/line-coord-flip.txt +20/−0
- test/golden/line-reverse-y.svg +16/−0
- test/golden/pie-basic.svg +18/−0
- test/golden/plotly-area-filled.svg +16/−0
- test/golden/plotly-area-filled.txt +14/−0
- test/golden/plotly-bar-grouped.svg +27/−0
- test/golden/plotly-bar-grouped.txt +16/−0
- test/golden/plotly-bar-horizontal.svg +21/−0
- test/golden/plotly-bar-horizontal.txt +16/−0
- test/golden/plotly-bar-stacked.svg +27/−0
- test/golden/plotly-bar-stacked.txt +16/−0
- test/golden/plotly-density.svg +17/−0
- test/golden/plotly-density.txt +14/−0
- test/golden/plotly-distplot.svg +27/−0
- test/golden/plotly-distplot.txt +16/−0
- test/golden/plotly-error-bars.svg +35/−0
- test/golden/plotly-error-bars.txt +14/−0
- test/golden/plotly-funnel.svg +19/−0
- test/golden/plotly-funnel.txt +14/−0
- test/golden/plotly-heatmap-annotated.svg +64/−0
- test/golden/plotly-heatmap-annotated.txt +16/−0
- test/golden/plotly-heatmap.svg +39/−0
- test/golden/plotly-heatmap.txt +16/−0
- test/golden/plotly-waterfall.svg +20/−0
- test/golden/plotly-waterfall.txt +14/−0
- test/golden/polar-rose.svg +22/−0
- test/golden/polar-rose.txt +20/−0
- test/golden/primitives.svg +8/−0
- test/golden/primitives.txt +8/−0
- test/golden/scatter-basic.svg +29/−0
- test/golden/scatter-basic.txt +20/−0
- test/golden/scatter-log-y.svg +28/−0
- test/golden/scatter-no-title.svg +28/−0
- test/golden/scatter-responsive.svg +29/−0
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 [ ""- , ""- ]--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>