packages feed

granite 0.1.0.0 → 0.1.0.1

raw patch · 4 files changed

+155/−60 lines, 4 filesdep ~granitedep ~randomPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: granite, random

API changes (from Hackage documentation)

- Granite: bars :: String -> [(String, Double)] -> Plot -> IO String
+ Granite: bars :: String -> [(String, Double)] -> Plot -> String
- Granite: boxPlot :: String -> [(String, [Double])] -> Plot -> IO String
+ Granite: boxPlot :: String -> [(String, [Double])] -> Plot -> String
- Granite: heatmap :: String -> [[Double]] -> Plot -> IO String
+ Granite: heatmap :: String -> [[Double]] -> Plot -> String
- Granite: histogram :: String -> Bins -> [Double] -> Plot -> IO String
+ Granite: histogram :: String -> Bins -> [Double] -> Plot -> String
- Granite: lineGraph :: String -> [(String, [(Double, Double)])] -> Plot -> IO String
+ Granite: lineGraph :: String -> [(String, [(Double, Double)])] -> Plot -> String
- Granite: pie :: String -> [(String, Double)] -> Plot -> IO String
+ Granite: pie :: String -> [(String, Double)] -> Plot -> String
- Granite: scatter :: String -> [(String, [(Double, Double)])] -> Plot -> IO String
+ Granite: scatter :: String -> [(String, [(Double, Double)])] -> Plot -> String
- Granite: stackedBars :: String -> [(String, [(String, Double)])] -> Plot -> IO String
+ Granite: stackedBars :: String -> [(String, [(String, Double)])] -> Plot -> String

Files

CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for granite +## 0.1.0.1 -- 2025-08-20++* Remove IO monad from plotting functions.+* Faster plotting after moving from lists as arrays to AVL trees.+* All functions are now strict by default.+ ## 0.1.0.0 -- 2025-08-18  * Support for pie charts, bar charts, heatmaps, line graphs, box plots, and histograms.
app/Main.hs view
@@ -14,38 +14,38 @@   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)] defPlot{widthChars=68,heightChars=22}+  putStrLn $ scatter "Random points" [series "A" (zip ptsA_x ptsA_y), series "B" (zip ptsB_x ptsB_y)] defPlot{widthChars=68,heightChars=22}    -- histogram   heights <- replicateM 5000 (uniformRM (160 :: Double, 190 :: Double) g)-  putStrLn =<< histogram "Heights (cm)" (bins 30 155 195) heights defPlot{widthChars=68,heightChars=18,legendPos=LegendBottom}+  putStrLn $ histogram "Heights (cm)" (bins 30 155 195) heights defPlot{widthChars=68,heightChars=18,legendPos=LegendBottom}    -- bars-  putStrLn =<< bars "Sales" [("Q1",12),("Q2",18),("Q3",9),("Q4",15)] defPlot+  putStrLn $ bars "Sales" [("Q1",12),("Q2",18),("Q3",9),("Q4",15)] defPlot    -- pie-  putStrLn =<< pie "Share" [("Alpha",0.35),("Beta",0.25),("Gamma",0.20),("Delta",0.20)]+  putStrLn $ pie "Share" [("Alpha",0.35),("Beta",0.25),("Gamma",0.20),("Delta",0.20)]                          defPlot{widthChars=46,heightChars=18,legendPos=LegendRight}    -- line graph-  putStrLn =<< lineGraph "Monthly Sales Trends" [ ("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+  putStrLn $ lineGraph "Monthly Sales Trends" [ ("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      -- box plot-  putStrLn =<< boxPlot "Test Score Distribution by Class" [ ("Class A", [78, 82, 85, 88, 90, 92, 85, 87, 89, 91, 76, 94, 88])-                                                          , ("Class B", [70, 75, 72, 80, 85, 78, 82, 77, 79, 81, 74, 83])-                                                          , ("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+  putStrLn $ boxPlot "Test Score Distribution by Class" [ ("Class A", [78, 82, 85, 88, 90, 92, 85, 87, 89, 91, 76, 94, 88])+                                                        , ("Class B", [70, 75, 72, 80, 85, 78, 82, 77, 79, 81, 74, 83])+                                                        , ("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      -- stacked bar chart-  putStrLn =<< stackedBars "Quarterly Revenue Breakdown" [ ("Q1", [("Hardware", 120), ("Software", 200), ("Services", 80)])-                                                         , ("Q2", [("Hardware", 135), ("Software", 220), ("Services", 95)])-                                                         , ("Q3", [("Hardware", 110), ("Software", 240), ("Services", 110)])-                                                         , ("Q4", [("Hardware", 145), ("Software", 260), ("Services", 125)])-                                                         ] defPlot+  putStrLn $ stackedBars "Quarterly Revenue Breakdown" [ ("Q1", [("Hardware", 120), ("Software", 200), ("Services", 80)])+                                                       , ("Q2", [("Hardware", 135), ("Software", 220), ("Services", 95)])+                                                       , ("Q3", [("Hardware", 110), ("Software", 240), ("Services", 110)])+                                                       , ("Q4", [("Hardware", 145), ("Software", 260), ("Services", 125)])+                                                       ] defPlot      -- heatmap   let matrix = [ [1.0,  0.8,  0.3, -0.2,  0.1]@@ -54,4 +54,4 @@                , [-0.2, -0.1, 0.6,  1.0,  0.7]                , [0.1,  0.2,  0.4,  0.7,  1.0]                ]-  putStrLn =<< heatmap "Correlation Matrix" matrix defPlot+  putStrLn $ heatmap "Correlation Matrix" matrix defPlot
granite.cabal view
@@ -1,18 +1,22 @@ cabal-version:      3.0 name:               granite-version:            0.1.0.0+version:            0.1.0.1 synopsis:           Easy terminal plotting. description:        A terminal plotting library for quick and easy visualisation. license:            MIT license-file:       LICENSE author:             Michael Chavinda maintainer:         mschavinda@gmail.com--- copyright:+copyright:          (c) 2024-2025 Michael Chavinda+bug-reports:        https://github.com/mchav/granite/issues category:           Graphics build-type:         Simple extra-doc-files:    CHANGELOG.md--- extra-source-files: +source-repository head+  type:     git+  location: https://github.com/mchav/granite+ common warnings     ghc-options: -Wall @@ -28,12 +32,10 @@ executable granite     import:           warnings     main-is:          Main.hs-    -- other-modules:-    -- other-extensions:     build-depends:         base >=4 && <5,-        granite,-        random+        granite ^>= 0.1,+        random >= 1.2 && <= 1.3     hs-source-dirs:   app     default-language: Haskell2010 @@ -45,5 +47,5 @@     main-is:          Main.hs     build-depends:         base ^>=4,-        granite+        granite ^>= 0.1 
src/Granite.hs view
@@ -1,4 +1,6 @@+-- TODO: Remove bang patterns now that strict is enabled. {-# LANGUAGE BangPatterns #-}+{-# LANGUAGE Strict #-}  module Granite   (@@ -97,18 +99,18 @@ palette :: [Pat] palette = [Solid, Checker, DiagA, DiagB, Sparse] -data Array2D a = A2D !Int !Int ![a]+data Array2D a = A2D !Int !Int !(Arr a)  getA2D :: Array2D a -> Int -> Int -> a-getA2D (A2D w _ xs) x y = xs !! (y*w + x)+getA2D (A2D w _ xs) x y = indexA xs (y*w + x)  setA2D :: Array2D a -> Int -> Int -> a -> Array2D a setA2D (A2D w h xs) x y v =   let i = y*w + x-  in A2D w h (take i xs ++ v : drop (i+1) xs)+  in A2D w h (setA xs i v)  newA2D :: Int -> Int -> a -> Array2D a-newA2D w h v = A2D w h (replicate (w*h) v)+newA2D w h v = A2D w h (fromList (replicate (w*h) v))  toBit :: Int -> Int -> Int toBit ry rx = case (ry,rx) of@@ -216,7 +218,7 @@   in unlines (attachY ++ [xBar, xLine])  axisifyGrid :: Plot -> [[(Char, Maybe Color)]] -> (Double,Double) -> (Double,Double) -> String-axisifyGrid cfg grid (xmin,xmax) (ymin,ymax) =+axisifyGrid cfg grid (!xmin,!xmax) (!ymin,!ymax) =   let plotH = length grid       plotW = if null grid then 0 else length (head grid)       left  = leftMargin cfg@@ -284,8 +286,8 @@ series :: String -> [(Double,Double)] -> (String, [(Double,Double)]) series = (,) -scatter :: String -> [(String, [(Double,Double)])] -> Plot -> IO String-scatter title sers cfg = pure $+scatter :: String -> [(String, [(Double,Double)])] -> Plot -> String+scatter title sers cfg =   let wC = widthChars cfg; hC = heightChars cfg       plotC = newCanvas wC hC       (xmin,xmax,ymin,ymax) = boundsXY (concatMap snd sers)@@ -293,9 +295,9 @@       sy y = clamp 0 (hC*4-1)  $ round ((ymax - y) / (ymax - ymin + eps) * fromIntegral (hC*4-1))       pats = cycle palette       cols = cycle paletteColors-      withSty = zipWith3 (\(n,ps) p c -> (n,ps,p,c)) sers pats cols-      drawOne (_name, pts, pat, col) c0 =-        foldl' (\c (x,y) -> let xd = sx x; yd = sy y+      withSty = zipWith3 (\(!n,!ps) p c -> (n,ps,p,c)) sers pats cols+      drawOne (!_name, !pts, !pat, !col) c0 =+        foldl' (\c (!x,!y) -> let xd = sx x; yd = sy y                             in if ink pat xd yd then setDotC c xd yd (Just col) else c)                c0 pts       cDone = foldl' (flip drawOne) plotC withSty@@ -342,8 +344,8 @@ bins :: Int -> Double -> Double -> Bins bins n a b = Bins (max 1 n) (min a b) (max a b) -histogram :: String -> Bins -> [Double] -> Plot -> IO String-histogram title (Bins n a b) xs cfg = pure $+histogram :: String -> Bins -> [Double] -> Plot -> String+histogram title (Bins n a b) xs cfg =   let step    = (b - a) / fromIntegral n       binIx x = clamp 0 (n-1) $ floor ((x - a) / step)       counts  = foldl' (\acc x ->@@ -374,8 +376,8 @@ addAt :: [Int] -> Int -> Int -> [Int] addAt xs i v = take i xs ++ [xs !! i + v] ++ drop (i+1) xs -bars :: String -> [(String, Double)] -> Plot -> IO String-bars title kvs cfg = pure $+bars :: String -> [(String, Double)] -> Plot -> String+bars title kvs cfg =   let wC   = widthChars cfg       hC   = heightChars cfg       vals = map snd kvs@@ -383,7 +385,7 @@        cats :: [(String, Double, Color)]       cats = [ (name, abs v / vmax, col)-             | ((name, v), col) <- zip kvs (cycle paletteColors) ]+             | (!(!name, !v), !col) <- zip kvs (cycle paletteColors) ]        nCats = length cats @@ -411,8 +413,8 @@       titled = if null title then "" else title   in drawFrame cfg titled ax legend -pie :: String -> [(String, Double)] -> Plot -> IO String-pie title parts0 cfg = pure $+pie :: String -> [(String, Double)] -> Plot -> String+pie title parts0 cfg =   let parts = normalize parts0       wC = widthChars cfg; hC = heightChars cfg       plotC = newCanvas wC hC@@ -421,13 +423,13 @@       cx    = wDots `div` 2       cy    = hDots `div` 2       toAng p = p * 2*pi-      wedges = scanl (\a (_,p) -> a + toAng p) 0 parts+      wedges = scanl (\a (_,!p) -> a + toAng p) 0 parts       angles = zip wedges (tail wedges)       names  = map fst parts       cols   = cycle pieColors       withP  = zipWith3 (\n ang col -> (n,ang,col)) names angles cols -      drawOne (_name,(a0,a1),col) c0 =+      drawOne (!_name,(!a0,!a1),!col) c0 =         let inside x y =               let dx  = fromIntegral (x - cx)                   dy  = fromIntegral (cy - y)@@ -447,7 +449,7 @@ normalize :: [(String, Double)] -> [(String, Double)] normalize xs =   let s = sum (map (abs . snd) xs) + 1e-12-  in [ (n, max 0 (v / s)) | (n,v) <- xs ]+  in [ (n, max 0 (v / s)) | (!n,!v) <- xs ]  angleWithin :: Double -> Double -> Double -> Bool angleWithin ang a0 a1@@ -455,7 +457,7 @@   | otherwise = ang >= a0 || ang <= a1  lineDotsC :: (Int,Int) -> (Int,Int) -> Maybe Color -> Canvas -> Canvas-lineDotsC (x0,y0) (x1,y1) mcol c0 =+lineDotsC (!x0,!y0) (!x1,!y1) mcol c0 =   let dx = abs (x1 - x0)       sx = if x0 < x1 then 1 else -1       dy = negate (abs (y1 - y0))@@ -464,13 +466,13 @@         | x == x1 && y == y1 = setDotC c x y mcol         | otherwise =             let e2 = 2*err-                (x', err') = if e2 >= dy then (x + sx, err + dy) else (x, err)-                (y', err'')= if e2 <= dx then (y + sy, err' + dx) else (y, err')+                (!x', !err') = if e2 >= dy then (x + sx, err + dy) else (x, err)+                (!y', !err'')= if e2 <= dx then (y + sy, err' + dx) else (y, err')             in go x' y' err'' (setDotC c x y mcol)   in go x0 y0 (dx + dy) c0 -lineGraph :: String -> [(String, [(Double,Double)])] -> Plot -> IO String-lineGraph title sers cfg = pure $+lineGraph :: String -> [(String, [(Double,Double)])] -> Plot -> String+lineGraph title sers cfg =   let wC = widthChars cfg; hC = heightChars cfg       plotC = newCanvas wC hC       (xmin,xmax,ymin,ymax) = boundsXY (concatMap snd sers)@@ -490,7 +492,7 @@       cDone = foldl' (flip drawSeries) plotC withSty       ax = axisify cfg cDone (xmin,xmax) (ymin,ymax)       legend = legendBlock (legendPos cfg) (leftMargin cfg + widthChars cfg)-                 [(n, Solid, col) | ((n,_), col) <- withSty]+                 [(n, Solid, col) | (!(!n,_), !col) <- withSty]       titled = if null title then "" else title   in drawFrame cfg titled ax legend @@ -506,12 +508,12 @@      then let m = sum xs / fromIntegral n in (m,m,m,m,m)      else (head sorted, getIdx q1Idx, getIdx q2Idx, getIdx q3Idx, last sorted) -boxPlot :: String -> [(String, [Double])] -> Plot -> IO String-boxPlot title datasets cfg = pure $+boxPlot :: String -> [(String, [Double])] -> Plot -> String+boxPlot title datasets cfg =   let wC = widthChars cfg       hC = heightChars cfg -      stats = [(name, quartiles vals) | (name, vals) <- datasets]+      stats = [(name, quartiles vals) | (!name, !vals) <- datasets]        allVals = concatMap snd datasets       ymin = if null allVals then 0 else minimum allVals - abs (minimum allVals) * 0.1@@ -577,8 +579,8 @@       else grid       where setAt row i v = take i row ++ [v] ++ drop (i+1) row -heatmap :: String -> [[Double]] -> Plot -> IO String-heatmap title matrix cfg = pure $+heatmap :: String -> [[Double]] -> Plot -> String+heatmap title matrix cfg =   let rows = length matrix       cols = if null matrix then 0 else length (head matrix) @@ -633,8 +635,8 @@       titled = if null title then "" else title   in drawFrame cfg titled ax gradientLegend -stackedBars :: String -> [(String, [(String, Double)])] -> Plot -> IO String-stackedBars title categories cfg = pure $+stackedBars :: String -> [(String, [(String, Double)])] -> Plot -> String+stackedBars title categories cfg =   let wC = widthChars cfg       hC = heightChars cfg @@ -681,3 +683,88 @@                  [(name, Solid, col) | (name, col) <- seriesColors]       titled = if null title then "" else title   in drawFrame cfg titled ax legend++-- 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+-- the dependencies very light (wouldn't want to install all of containers+-- just to get an int map).+data Arr a = E | N {-# UNPACK #-} !Int {-# UNPACK #-} !Int !(Arr a) a !(Arr a)++size :: Arr a -> Int+size E               = 0+size (N sz _ _ _ _)  = sz++height :: Arr a -> Int+height E               = 0+height (N _ h _ _ _)   = h++mk :: Arr a -> a -> Arr a -> Arr a+mk l x r = N sz h l x r+  where+    sl = size l+    sr = size r+    hl = height l+    hr = height r+    sz = 1 + sl + sr+    h  = 1 + (if hl >= hr then hl else hr)++rotateL :: Arr a -> Arr a+rotateL (N _ _ l x (N _ _ rl y rr)) = mk (mk l x rl) y rr+rotateL _ = error "rotateL: malformed tree"++rotateR :: Arr a -> Arr a+rotateR (N _ _ (N _ _ ll y lr) x r) = mk ll y (mk lr x r)+rotateR _ = error "rotateR: malformed tree"++balance :: Arr a -> Arr a+balance t@(N _ _ l x r)+  | height l > height r + 1 =+      case l of+        N _ _ ll _ lr ->+          if height ll >= height lr+             then rotateR t+             else rotateR (mk (rotateL l) x r)+        _ -> t+  | height r > height l + 1 =+      case r of+        N _ _ rl _ rr ->+          if height rr >= height rl+             then rotateL t+             else rotateL (mk l x (rotateR r))+        _ -> t+  | otherwise = mk l x r+balance t = t++indexA :: Arr a -> Int -> a+indexA t i =+  case t of+    E -> error ("index out of bounds: " ++ show i)+    N _ _ l x r ->+      let sl = size l in+      if i < 0 || i >= 1 + sl + size r then error ("index out of bounds: " ++ show i)+      else if i < sl then indexA l i+      else if i == sl then x+      else indexA r (i - sl - 1)++setA :: Arr a -> Int -> a -> Arr a+setA t i y =+  case t of+    E -> error ("index out of bounds when setting: " ++ show i)+    N _ _ l x r ->+      let sl = size l in+      if i < 0 || i >= 1 + sl + size r then error ("index out of bounds: " ++ show i)+      else if i < sl then balance (mk (setA l i y) x r)+      else if i == sl then mk l y r+      else balance (mk l x (setA r (i - sl - 1) y))++fromList :: [a] -> Arr a+fromList xs = fst (build (length xs) xs)+  where+    build :: Int -> [a] -> (Arr a, [a])+    build 0 ys = (E, ys)+    build n ys =+      let (!l, !ys1)   = build (n `div` 2) ys+          (x:ys2)      = ys1+          (!r, !ys3)   = build (n - n `div` 2 - 1) ys2+      in (mk l x r, ys3)