packages feed

granite 0.3.0.4 → 0.3.0.5

raw patch · 6 files changed

+151/−114 lines, 6 filesdep ~QuickCheckPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: QuickCheck

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,8 @@ # Revision history for granite +## 0.3.0.5 -- 2025-11-10+* Fix x-axis label alignment for continuous histograms.+ ## 0.3.0.4 -- 2025-09-26 * Fix issue with x-axis spacing when categories is greater than width. 
README.md view
@@ -15,11 +15,11 @@ ## Examples  ### Scatter plot-![Scatter Plot](https://github.com/mchav/granite/blob/main/static/scatter_plot.png)+![Scatter Plot](https://github.com/mchav/granite/blob/main/static/scatter_plot.png?raw=true)  ```haskell-import           Control.Monad-import           System.Random.Stateful+import Control.Monad+import System.Random.Stateful  import Granite.String @@ -31,12 +31,12 @@   ptsA_y <- replicateM 600 (uniformRM range g)   ptsB_x <- replicateM 600 (uniformRM range g)   ptsB_y <- replicateM 600 (uniformRM range g)-  putStrLn (scatter "Random points" [series "A" (zip ptsA_x ptsA_y), series "B" (zip ptsB_x ptsB_y)]+  putStrLn (scatter [series "A" (zip ptsA_x ptsA_y), series "B" (zip ptsB_x ptsB_y)]             defPlot{widthChars=68,heightChars=22,plotTitle="Random points"}) ```  ### Bar chart-![Bar chart](https://github.com/mchav/granite/blob/main/static/bar_chart.png)+![Bar chart](https://github.com/mchav/granite/blob/main/static/bar_chart.png?raw=true)  ```haskell {-# LANGUAGE OverloadedStrings #-}@@ -49,7 +49,7 @@ ```  ### Stacked bar chart-![Stacked bar chart](https://github.com/mchav/granite/blob/main/static/stacked_bar.png)+![Stacked bar chart](https://github.com/mchav/granite/blob/main/static/stacked_bar.png?raw=true)  ```haskell {-# LANGUAGE OverloadedStrings #-}@@ -66,7 +66,7 @@ ```  ### Pie chart-![Pie chart](https://github.com/mchav/granite/blob/main/static/pie_chart.png)+![Pie chart](https://github.com/mchav/granite/blob/main/static/pie_chart.png?raw=true)  ```haskell {-# LANGUAGE OverloadedStrings #-}@@ -79,7 +79,7 @@ ```  ### Box plot-![Box plot](https://github.com/mchav/granite/blob/main/static/box_plot.png)+![Box plot](https://github.com/mchav/granite/blob/main/static/box_plot.png?raw=true)  ```haskell {-# LANGUAGE OverloadedStrings #-}@@ -96,7 +96,7 @@ ```  ### Line graph-![Line graph](https://github.com/mchav/granite/blob/main/static/line_graph.png)+![Line graph](https://github.com/mchav/granite/blob/main/static/line_graph.png?raw=true)  ```haskell {-# LANGUAGE OverloadedStrings #-}@@ -106,13 +106,13 @@  main :: IO () main = T.putStrLn $ lineGraph [ ("Product A", [(1, 100), (2, 120), (3, 115), (4, 140), (5, 155), (6, 148)])-                                              , ("Product B", [(1, 80), (2, 85), (3, 95), (4, 92), (5, 110), (6, 125)])-                                              , ("Product C", [(1, 60), (2, 62), (3, 70), (4, 85), (5, 82), (6, 90)])-                                              ] defPlot {plotTitle="Monthly Sales Trends"}+                              , ("Product B", [(1, 80), (2, 85), (3, 95), (4, 92), (5, 110), (6, 125)])+                              , ("Product C", [(1, 60), (2, 62), (3, 70), (4, 85), (5, 82), (6, 90)])+                              ] defPlot {plotTitle="Monthly Sales Trends"} ```  ### Heatmap-![Heatmap](https://github.com/mchav/granite/blob/main/static/heatmap.png)+![Heatmap](https://github.com/mchav/granite/blob/main/static/heatmap.png?raw=true)  ```haskell {-# LANGUAGE OverloadedStrings #-}@@ -132,18 +132,21 @@ ```  ### Histogram-![Histogram](https://github.com/mchav/granite/blob/main/static/histogram.png)+![Histogram](https://github.com/mchav/granite/blob/main/static/histogram.png?raw=true)  ```haskell {-# LANGUAGE OverloadedStrings #-}+import qualified Data.Text as T import qualified Data.Text.IO as T-+import Control.Monad import Granite+import System.Random.Stateful  main :: IO () main = do+  g <- newIOGenM =<< newStdGen   heights <- replicateM 5000 (uniformRM (160 :: Double, 190 :: Double) g)-  Text.putStrLn $+  T.putStrLn $       histogram           (bins 30 155 195)           heights@@ -151,7 +154,7 @@               { widthChars = 68               , heightChars = 18               , legendPos = LegendBottom-              , xFormatter = \_ _ v -> Text.pack (show (round v :: Int))+              , xFormatter = \_ _ v -> T.pack (show (round v :: Int))               , xNumTicks = 10               , yNumTicks = 5               , plotTitle = "Heights (cm)"
app/Main.hs view
@@ -21,10 +21,10 @@             defPlot{widthChars = 68, heightChars = 22, plotTitle = "Random points"}      -- histogram-    let heights = [100 .. 200]+    let heights = concat $ zipWith replicate [0 .. 40] [160 .. 200]     Text.putStrLn $         histogram-            (bins 30 155 195)+            (bins 40 155 195)             heights             defPlot                 { widthChars = 68@@ -70,7 +70,7 @@             , ("Class C", [88, 92, 95, 90, 93, 89, 91, 94, 96, 87, 90, 92])             , ("Class D", [65, 70, 72, 68, 75, 80, 73, 71, 69, 74, 77, 76])             ]-            defPlot{plotTitle = "Test Score Distribution by Class"}+            defPlot{plotTitle = "Test Score Distribution by Class", xNumTicks = 4}      -- stacked bar chart     Text.putStrLn $
granite.cabal view
@@ -1,12 +1,17 @@ cabal-version:      3.0 name:               granite-version:            0.3.0.4+version:            0.3.0.5 synopsis:           Easy terminal plotting. description:        A terminal plotting library for quick and easy visualisation. license:            MIT license-file:       LICENSE author:             Michael Chavinda-tested-with:        GHC ==9.8.2+tested-with:        GHC ==9.4.8+                     || ==9.6.7+                     || ==9.8.4+                     || ==9.10.3+                     || ==9.12.2+ maintainer:         mschavinda@gmail.com copyright:          (c) 2024-2025 Michael Chavinda bug-reports:        https://github.com/mchav/granite/issues@@ -58,18 +63,18 @@     type: exitcode-stdio-1.0     default-language: GHC2021     main-is: Spec.hs-    +     other-modules:         GraniteSpec-    +     build-depends:         base >= 4.14 && < 5         , granite ^>= 0.3         , text >= 1.2 && < 3         , hspec >= 2.7 && < 2.12-        , QuickCheck >= 2.14 && < 2.15-    +        , QuickCheck >= 2.14 && < 2.17+     hs-source-dirs: test-    +     build-tool-depends:         hspec-discover:hspec-discover >= 2.7
src/Granite.hs view
@@ -152,8 +152,8 @@   , plotTitle    = ""   , legendPos    = LegendRight   , colorPalette = [ BrightBlue, BrightMagenta, BrightCyan, BrightGreen, BrightYellow, BrightRed, BrightWhite, BrightBlack]-  , xFormatter   = \ _ _ v -> show v-  , yFormatter   = \ _ _ v -> show v+  , xFormatter   = \\ _ _ v -> show v+  , yFormatter   = \\ _ _ v -> show v   , xNumTicks    = 2   , yNumTicks    = 2   }@@ -181,9 +181,11 @@ {- | Axis-aware, width-limited, tick-label formatter.  Given:+    * axis context    * a per-tick width budget (in terminal cells)    * and the raw tick value.+ returns the label to render. -} type LabelFormatter =@@ -278,7 +280,7 @@         pats = cycle palette         cols = cycle (colorPalette cfg)         withSty = zipWith3 (\(n, ps) p c -> (n, ps, p, c)) sers pats cols-        drawOne (_name, pts, pat, col) c0 =+        drawOne c0 (_name, pts, pat, col) =             List.foldl'                 ( \c (x, y) ->                     let xd = sx x; yd = sy y@@ -286,7 +288,7 @@                 )                 c0                 pts-        cDone = List.foldl' (flip drawOne) plotC withSty+        cDone = List.foldl' drawOne plotC withSty         ax = axisify cfg cDone (xmin, xmax) (ymin, ymax)         legend =             legendBlock@@ -358,8 +360,8 @@ ==== __Example__  @-let data = [("Apple", 45.2), ("Banana", 38.1), ("Orange", 52.7)]-    chart = bars data defPlot { plotTitle = "Fruit Sales" }+let fruits = [(\"Apple\", 45.2), (\"Banana\", 38.1), (\"Orange\", 52.7)]+    chart = bars fruits defPlot { plotTitle = "Fruit Sales" } @ -} bars ::@@ -372,7 +374,7 @@ bars kvs cfg =     let wC = widthChars cfg         hC = heightChars cfg-        vals = map snd kvs+        (catNames, vals) = unzip kvs         vmax = maximum' (map abs vals)          cats :: [(Text, Double, Color)]@@ -408,8 +410,8 @@                 grid                 (0, fromIntegral (max 1 nCats))                 (0, vmax)-                (map fst kvs)-                (fmap (+ 1) (safeHead widths))+                catNames+                (fmap (+ 1) (listToMaybe widths))         legendWidth = leftMargin cfg + 1 + gridWidth grid         legend =             legendBlock@@ -425,9 +427,9 @@ ==== __Example__  @-let data = [("Q1", [("Product A", 100), ("Product B", 150)]),-            ("Q2", [("Product A", 120), ("Product B", 180)])]-    chart = stackedBars data defPlot+let sales = [(\"Q1\", [(\"Product A\", 100), (\"Product B\", 150)]),+             (\"Q2\", [(\"Product A\", 120), (\"Product B\", 180)])]+    chart = stackedBars sales defPlot @ -} stackedBars ::@@ -489,7 +491,7 @@                 (0, fromIntegral (max 1 nCats))                 (0, maxHeight)                 (map fst categories)-                (fmap (+ 1) (safeHead widths))+                (fmap (+ 1) (listToMaybe widths))         legend :: Text         legend =             legendBlock@@ -562,17 +564,14 @@         colsF = resampleToWidth wData fracs0          dataCols = [(colGlyphs hC f, Just BrightCyan) | f <- colsF]-        gutterCol = (replicate hC ' ', Nothing)-        columns = List.intercalate [gutterCol] (map pure dataCols)-         grid :: [[(Char, Maybe Color)]]         grid =-            [ [(fst col !! y, snd col) | col <- columns]+            [ [(fst col !! y, snd col) | col <- dataCols]             | y <- [0 .. hC - 1]             ]          ax =-            axisifyGrid cfg grid (a, b) (0, fromIntegral (maximum (1 : counts))) [] Nothing+            axisifyGrid cfg grid (a, b) (0, maxC) [] Nothing         legendWidth = leftMargin cfg + 1 + gridWidth grid         legend = legendBlock (legendPos cfg) legendWidth [("count", Solid, BrightCyan)]      in drawFrame cfg ax legend@@ -584,8 +583,8 @@ ==== __Example__  @-let data = [("Chrome", 65), ("Firefox", 20), ("Safari", 10), ("Other", 5)]-    chart = pie data defPlot { plotTitle = "Browser Market Share" }+let browsers = [(\"Chrome\", 65), (\"Firefox\", 20), (\"Safari\", 10), (\"Other\", 5)]+    chart = pie browsers defPlot { plotTitle = "Browser Market Share" } @ -} pie ::@@ -780,8 +779,8 @@                  grid7 = drawHLine grid6 xStart xEnd medRow '═' (Just col) -                grid8 = setGridChar grid7 xMid minRow '┬' (Just col)-                grid9 = setGridChar grid8 xMid maxRow '┴' (Just col)+                grid8 = setGridChar grid7 xMid minRow '┴' (Just col)+                grid9 = setGridChar grid8 xMid maxRow '┬' (Just col)              in grid9          finalGrid = List.foldl' drawBox emptyGrid (zip [0 ..] stats)@@ -814,11 +813,7 @@          in List.foldl' (\g x -> setGridChar g x y ch col) grid [xStart .. xEnd]      setGridChar grid x y ch col =-        if y >= 0 && y < length grid && x >= 0 && x < gridWidth grid-            then take y grid <> [setAt (grid !! y) x (ch, col)] <> drop (y + 1) grid-            else grid-      where-        setAt row i v = take i row <> [v] <> drop (i + 1) row+        updateAt grid y (\row -> setAt row x (ch, col))  ansiCode :: Color -> Int ansiCode Black = 30@@ -923,10 +918,8 @@ setDotC c xDot yDot mcol     | xDot < 0 || yDot < 0 || xDot >= cW c * 2 || yDot >= cH c * 4 = c     | otherwise =-        let cx = xDot `div` 2-            cy = yDot `div` 4-            rx = xDot - 2 * cx-            ry = yDot - 4 * cy+        let (cx, rx) = xDot `divMod` 2+            (cy, ry) = yDot `divMod` 4             b = toBit ry rx             m = getA2D (buffer c) cx cy             c' = c{buffer = setA2D (buffer c) cx cy (m .|. b)}@@ -1038,17 +1031,15 @@         baseLbl :: [Text]         baseLbl = replicate plotH pad -        setAt :: [Text] -> Int -> Text -> [Text]-        setAt xs i v-            | i < 0 || i >= length xs = xs-            | otherwise = take i xs <> [v] <> drop (i + 1) xs-         yEnv n = AxisEnv (ymin, ymax) n 3         ySlot = max 1 left         yLabels =             List.foldl'                 ( \acc (row, v) ->-                    setAt acc row (justifyRight left (yFormatter cfg (yEnv row) ySlot v))+                    setAt acc row+                        . ellipsisize left+                        . justifyRight left+                        $ yFormatter cfg (yEnv row) ySlot v                 )                 baseLbl                 yTicks@@ -1067,7 +1058,7 @@             placeLabels                 (Text.replicate (left + 1 + plotW) " ")                 (left + 1)-                [(x, xFormatter cfg (xEnv x) slotW v) | (x, v) <- xTicks]+                [(x, xFormatter cfg (xEnv i) slotW v) | (i, (x, v)) <- zip [0 ..] xTicks]      in Text.unlines (attachY <> [xBar, xLine])  axisifyGrid ::@@ -1087,16 +1078,15 @@         yTicks = ticks1D plotH (yNumTicks cfg) (ymin, ymax) True         baseLbl = List.replicate plotH pad -        setAt :: [Text] -> Int -> Text -> [Text]-        setAt xs i v-            | i < 0 || i >= length xs = xs-            | otherwise = take i xs <> [v] <> drop (i + 1) xs--        yEnv n = AxisEnv (ymin, ymax) n 3+        yEnv n = AxisEnv (ymin, ymax) n (yNumTicks cfg)         ySlot = max 1 left         yLabels =             List.foldl'-                ( \acc (row, v) -> setAt acc row (justifyRight left (yFormatter cfg (yEnv row) ySlot v))+                ( \acc (row, v) ->+                    setAt acc row+                        . ellipsisize left+                        . justifyRight left+                        $ yFormatter cfg (yEnv row) ySlot v                 )                 baseLbl                 yTicks@@ -1110,25 +1100,32 @@          xBar = pad <> "└" <> Text.replicate plotW "─" -        slotW =-            fromMaybe-                ( slotBudget-                    plotW-                    (max 1 (xNumTicks cfg))-                )-                w-        nSlots = plotW `div` slotW-        hasCategories = not (null (filter (not . Text.null) categories))-        xTicks = ticks1D plotW nSlots (xmin, xmax) False-        xEnv n = AxisEnv (xmin, xmax) n nSlots+        hasCategories = not (null categories) && not (all Text.null categories)+         xLine =-            placeGridLabels-                (Text.replicate (left + 1) " ")-                slotW-                ( if hasCategories-                    then (keepPercentiles (xNumTicks cfg) (length xTicks + 1) categories)-                    else [xFormatter cfg (xEnv i) slotW v | (i, (_, v)) <- zip [0 ..] xTicks]-                )+            if hasCategories+                then+                    let slotW =+                            fromMaybe+                                ( slotBudget+                                    plotW+                                    (max 1 (xNumTicks cfg))+                                )+                                w+                        nSlots = plotW `div` slotW+                        xTicks = ticks1D plotW nSlots (xmin, xmax) False+                     in placeGridLabels+                            (Text.replicate (left + 1) " ")+                            slotW+                            (keepPercentiles (xNumTicks cfg) (length xTicks + 1) categories)+                else+                    let xTicks = ticks1D plotW (xNumTicks cfg) (xmin, xmax) False+                        xEnv i = AxisEnv (xmin, xmax) i (xNumTicks cfg)+                        slotW = slotBudget plotW (max 1 (length xTicks))+                     in placeLabels+                            (Text.replicate (left + 1 + plotW) " ")+                            (left + 1)+                            [(x, xFormatter cfg (xEnv i) slotW v) | (i, (x, v)) <- zip [0 ..] xTicks]      in Text.unlines (attachY <> [xBar, xLine])  keepPercentiles :: Int -> Int -> [Text] -> [Text]@@ -1136,7 +1133,7 @@     | k <= 0 = []     | null xs = replicate k ""     | n <= 1 = replicate k ""-    | otherwise = (init (map (valueAt pairs) [0 .. k - 1])) ++ [last xs]+    | otherwise = map valueAt [0 .. k - 2] ++ [last xs]   where     m = length xs     pairs :: [(Int, Text)]@@ -1148,12 +1145,10 @@         , let srcIx = (i * (m - 1)) `div` (n - 1)         , let slotIx = (i * (k - 1)) `div` (n - 1)         ]-    valueAt :: [(Int, Text)] -> Int -> Text-    valueAt [] _ = ""-    valueAt ((j, v) : rest) i-        | i == j = v-        | otherwise = valueAt rest i +    valueAt :: Int -> Text+    valueAt i = fromMaybe "" $ List.lookup i pairs+ placeLabels :: Text -> Int -> [(Int, Text)] -> Text placeLabels base off = List.foldl' place base   where@@ -1166,7 +1161,7 @@ placeGridLabels base slotW = List.foldl' place base   where     place :: Text -> Text -> Text-    place acc s = acc <> Text.take slotW (s <> (Text.replicate slotW " "))+    place acc s = acc <> Text.take slotW (s <> Text.replicate slotW " ")  legendBlock :: LegendPos -> Int -> [(Text, Pat, Color)] -> Text legendBlock LegendBottom width entries =@@ -1200,8 +1195,7 @@  boundsXY :: Plot -> [(Double, Double)] -> (Double, Double, Double, Double) boundsXY cfg pts =-    let xs = map fst pts-        ys = map snd pts+    let (xs, ys) = unzip pts         xmin = minimum' xs         xmax = maximum' xs         ymin = minimum' ys@@ -1261,7 +1255,7 @@                 ]  addAt :: [Int] -> Int -> Int -> [Int]-addAt xs i v = take i xs <> [xs !! i + v] <> drop (i + 1) xs+addAt xs i v = updateAt xs i (+ v)  normalize :: [(Text, Double)] -> [(Text, Double)] normalize xs =@@ -1300,7 +1294,7 @@      in if n < 5             then let m = sum xs / fromIntegral n in (m, m, m, m, m)             else-                ( maybe 0 fst (List.uncons sorted)+                ( fromMaybe 0 (listToMaybe sorted)                 , getIdx q1Idx                 , getIdx q2Idx                 , getIdx q3Idx@@ -1318,10 +1312,6 @@ maximum' [] = 1 maximum' xs = maximum xs -safeHead :: [a] -> Maybe a-safeHead [] = Nothing-safeHead (x : _) = Just x- -- AVL Tree we'll use as an array. -- This improves upon the previous implementation that relies -- on linked list for indexing and update (both O(n)) while keeping@@ -1420,3 +1410,39 @@                 (v : vs) -> (v, vs)             (r, ys3) = build (n - n `div` 2 - 1) ys2          in (mk l x r, ys3)++-- >>> setAt "abc" (-1) 'x'+-- "abc"+-- >>> setAt "abc" 0 'x'+-- "xbc"+-- >>> setAt "abc" 2 'x'+-- "abx"+-- >>> setAt "abc" 10 'x'+-- "abc"+setAt :: [a] -> Int -> a -> [a]+setAt xs i v = updateAt xs i (const v)++updateAt :: [a] -> Int -> (a -> a) -> [a]+updateAt xs i f+    | i < 0 = xs+    | otherwise = go xs i+  where+    go [] _ = []+    go (x : rest) 0 = f x : rest+    go (x : rest) n = x : go rest (n - 1)++{- | Ensure the text fits within maxWidth. If it doesn't, truncate and append an ellipsis.+>>> ellipsisize 5 "Hello, World!"+"Hell\8230"+>>> ellipsisize 1 "Hi"+"\8230"+>>> ellipsisize 0 "Hello, World!"+""+>>> ellipsisize 20 "Hello, World!"+"Hello, World!"+-}+ellipsisize :: Int -> Text -> Text+ellipsisize maxWidth lbl+    | maxWidth <= 0 = ""+    | wcswidth lbl > maxWidth = Text.take (maxWidth - 1) lbl <> "…"+    | otherwise = lbl
src/Granite/String.hs view
@@ -142,8 +142,8 @@   , plotTitle    = ""   , legendPos    = LegendRight   , colorPalette = [BrightBlue, BrightMagenta, BrightCyan, BrightGreen, BrightYellow, BrightRed, BrightWhite, BrightBlack]-  , xFormatter   = \_ d -> show d-  , yFormatter   = \_ d -> show d+  , xFormatter   = \\_ _ d -> show d+  , yFormatter   = \\_ _ d -> show d   , xNumTicks    = 2   , yNumTicks    = 2   }@@ -241,8 +241,8 @@ ==== __Example__  @-let data = [("Apple", 45.2), ("Banana", 38.1), ("Orange", 52.7)]-    chart = bars data defPlot { plotTitle = "Fruit Sales" }+let fruits = [(\"Apple\", 45.2), (\"Banana\", 38.1), (\"Orange\", 52.7)]+    chart = bars fruits defPlot { plotTitle = "Fruit Sales" } @ -} bars ::@@ -262,9 +262,9 @@ ==== __Example__  @-let data = [("Q1", [("Product A", 100), ("Product B", 150)]),-            ("Q2", [("Product A", 120), ("Product B", 180)])]-    chart = stackedBars data defPlot+let sales = [(\"Q1\", [(\"Product A\", 100), (\"Product B\", 150)]),+             (\"Q2\", [(\"Product A\", 120), (\"Product B\", 180)])]+    chart = stackedBars sales defPlot @ -} stackedBars ::@@ -313,8 +313,8 @@ ==== __Example__  @-let data = [("Chrome", 65), ("Firefox", 20), ("Safari", 10), ("Other", 5)]-    chart = pie data defPlot { plotTitle = "Browser Market Share" }+let browsers = [(\"Chrome\", 65), (\"Firefox\", 20), (\"Safari\", 10), (\"Other\", 5)]+    chart = pie browsers defPlot { plotTitle = "Browser Market Share" } @ -} pie ::