diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Revision history for granite
 
+## 0.3.0.1 -- 2025-09-17
+* Left edge of plots now has an elbow instead of disjoint bars.
+* Remove dependency on random.
+
 ## 0.3.0.0 -- 2025-08-31
 * Export a `LabelFormatter` function along with AxisEnv to define smarter labels.
 * Change API of formatter to use AxisEnv and slot budget.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
 # Granite
-A library for producing terminal plots. It depends only on Haskell's base and text (only for efficieny but it could use `String`. In fact, we expose an API that uses `String` for easier use in GHCi) packages so it should be easy to use and install.
+A library for producing terminal plots. It depends only on Haskell's base and text (only for efficiency but it could use `String`. In fact, we expose an API that uses `String` for easier use in GHCi) packages so it should be easy to use and install.
 
 # Supported graph types
 
@@ -89,10 +89,10 @@
 
 main :: IO ()
 main = T.putStrLn $ boxPlot [ ("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 {plotTitle="Test Score Distribution by Class"}
+                            , ("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 {plotTitle="Test Score Distribution by Class"}
 ```
 
 ### Line graph
@@ -129,4 +129,31 @@
                , [0.1,  0.2,  0.4,  0.7,  1.0]
                ]
   T.putStrLn $ heatmap matrix defPlot {plotTitle="Correlation Matrix"}
+```
+
+### Histogram
+![Histogram](https://github.com/mchav/granite/blob/main/static/histogram.png)
+
+```haskell
+{-# LANGUAGE OverloadedStrings #-}
+import qualified Data.Text.IO as T
+
+import Granite
+
+main :: IO ()
+main = do
+  heights <- replicateM 5000 (uniformRM (160 :: Double, 190 :: Double) g)
+  Text.putStrLn $
+      histogram
+          (bins 30 155 195)
+          heights
+          defPlot
+              { widthChars = 68
+              , heightChars = 18
+              , legendPos = LegendBottom
+              , xFormatter = \_ _ v -> Text.pack (show (round v :: Int))
+              , xNumTicks = 10
+              , yNumTicks = 5
+              , plotTitle = "Heights (cm)"
+              }
 ```
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -5,23 +5,20 @@
 import Control.Monad
 import Data.Text qualified as Text
 import Data.Text.IO qualified as Text
-import System.Random.Stateful
 
 import Granite
 
 main :: IO ()
 main = do
     -- scatter
-    g <- newIOGenM =<< newStdGen
-    let range = (0 :: Double, 1 :: Double)
-    ptsA_x <- replicateM 600 (uniformRM range g)
-    ptsA_y <- replicateM 600 (uniformRM range g)
-    ptsB_x <- replicateM 600 (uniformRM range g)
-    ptsB_y <- replicateM 600 (uniformRM range g)
+    let ptsA_x = [0..599]
+    let ptsA_y = [0..599]
+    let ptsB_x = [0..599]
+    let ptsB_y = [599,598..0]
     Text.putStrLn $ scatter [series "A" (zip ptsA_x ptsA_y), series "B" (zip ptsB_x ptsB_y)] defPlot{widthChars = 68, heightChars = 22, plotTitle = "Random points"}
 
     -- histogram
-    heights <- replicateM 5000 (uniformRM (160 :: Double, 190 :: Double) g)
+    let heights = [100..200]
     Text.putStrLn $
         histogram
             (bins 30 155 195)
@@ -32,7 +29,7 @@
                 , legendPos = LegendBottom
                 , xFormatter = \_ _ v -> Text.pack (show (round v :: Int))
                 , xNumTicks = 10
-                , yNumTicks = 10
+                , yNumTicks = 5
                 , plotTitle = "Heights (cm)"
                 }
 
diff --git a/granite.cabal b/granite.cabal
--- a/granite.cabal
+++ b/granite.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               granite
-version:            0.3.0.0
+version:            0.3.0.1
 synopsis:           Easy terminal plotting.
 description:        A terminal plotting library for quick and easy visualisation.
 license:            MIT
@@ -49,7 +49,6 @@
     build-depends:
         base >=4 && <5,
         granite ^>= 0.3,
-        random >= 1.2 && < 1.3,
         text >= 1 && < 3
     hs-source-dirs:   app
     default-language: GHC2021
diff --git a/src/Granite.hs b/src/Granite.hs
--- a/src/Granite.hs
+++ b/src/Granite.hs
@@ -392,7 +392,7 @@
             | y <- [0 .. hC - 1]
             ]
 
-        ax = axisifyGrid cfg grid (0, fromIntegral (max 1 nCats)) (0, vmax)
+        ax = axisifyGrid cfg grid (0, fromIntegral (max 1 nCats)) (0, vmax) (map fst kvs) (fmap (+ 1) (safeHead widths))
         legendWidth = leftMargin cfg + 1 + gridWidth grid
         legend =
             legendBlock
@@ -465,7 +465,7 @@
         grid = [[col !! y | col <- columns] | y <- [0 .. hC - 1]]
 
         ax :: Text
-        ax = axisifyGrid cfg grid (0, fromIntegral (max 1 nCats)) (0, maxHeight)
+        ax = axisifyGrid cfg grid (0, fromIntegral (max 1 nCats)) (0, maxHeight) (map fst categories) (fmap (+ 1) (safeHead widths))
         legend :: Text
         legend =
             legendBlock
@@ -547,7 +547,7 @@
             | y <- [0 .. hC - 1]
             ]
 
-        ax = axisifyGrid cfg grid (a, b) (0, fromIntegral (maximum (1 : counts)))
+        ax = axisifyGrid cfg grid (a, b) (0, fromIntegral (maximum (1 : counts))) [] Nothing
         legendWidth = leftMargin cfg + 1 + gridWidth grid
         legend = legendBlock (legendPos cfg) legendWidth [("count", Solid, BrightCyan)]
      in drawFrame cfg ax legend
@@ -672,7 +672,7 @@
             | i <- [0 .. plotH - 1]
             ]
 
-        ax = axisifyGrid cfg displayGrid (0, fromIntegral cols - 1) (0, fromIntegral rows - 1)
+        ax = axisifyGrid cfg displayGrid (0, fromIntegral cols - 1) (0, fromIntegral rows - 1) [] (Just (plotW `div` cols))
 
         gradientLegend =
             Text.pack (printf "%.2f " vmin)
@@ -752,7 +752,7 @@
 
         finalGrid = List.foldl' drawBox emptyGrid (zip [0 ..] stats)
 
-        ax = axisifyGrid cfg finalGrid (0, fromIntegral nBoxes) (ymin, ymax)
+        ax = axisifyGrid cfg finalGrid (0, fromIntegral nBoxes) (ymin, ymax) (map fst datasets) (Just (boxWidth + spacing))
         legend =
             legendBlock
                 (legendPos cfg)
@@ -935,14 +935,11 @@
     | otherwise = Text.pack (showFFloat (Just 1) v "")
 
 drawFrame :: Plot -> Text -> Text -> Text
-drawFrame _cfg contentWithAxes legendBlockStr =
+drawFrame cfg contentWithAxes legendBlockStr =
     Text.unlines $
         filter
             (not . Text.null)
-            ( [plotTitle _cfg | not (Text.null (plotTitle _cfg))]
-                <> [contentWithAxes]
-                <> [legendBlockStr | not (Text.null legendBlockStr)]
-            )
+            [plotTitle cfg, contentWithAxes, legendBlockStr]
 
 {- | Evenly spaced tick positions in screen space paired with data values.
   If invertY = True, 0 maps to ymax (top row) and 1 maps to ymin (bottom).
@@ -1008,7 +1005,7 @@
         canvasLines = Text.lines (renderCanvas c)
         attachY = zipWith (\lbl line -> lbl <> "│" <> line) yLabels canvasLines
 
-        xBar = pad <> "│" <> Text.replicate plotW "─"
+        xBar = pad <> "└" <> Text.replicate plotW "─"
 
         xTicks :: [(Int, Double)]
         xTicks = ticks1D plotW (xNumTicks cfg) (xmin, xmax) False
@@ -1022,8 +1019,8 @@
                 [(x, xFormatter cfg (xEnv x) slotW v) | (x, v) <- xTicks]
      in Text.unlines (attachY <> [xBar, xLine])
 
-axisifyGrid :: Plot -> [[(Char, Maybe Color)]] -> (Double, Double) -> (Double, Double) -> Text
-axisifyGrid cfg grid (xmin, xmax) (ymin, ymax) =
+axisifyGrid :: Plot -> [[(Char, Maybe Color)]] -> (Double, Double) -> (Double, Double) -> [Text] -> Maybe Int -> Text
+axisifyGrid cfg grid (xmin, xmax) (ymin, ymax) categories w =
     let plotH = length grid
         plotW = gridWidth grid
         left = leftMargin cfg
@@ -1046,21 +1043,22 @@
                 yTicks
 
         renderRow :: [(Char, Maybe Color)] -> Text
-        renderRow cells =
-            Text.concat (fmap (\(ch, mc) -> maybe (Text.singleton ch) (`paint` ch) mc) cells)
+        renderRow cells = Text.concat (fmap (\(ch, mc) -> maybe (Text.singleton ch) (`paint` ch) mc) cells)
 
         attachY = zipWith (\lbl cells -> lbl <> "│" <> renderRow cells) yLabels grid
 
-        xBar = pad <> "│" <> Text.replicate plotW "─"
+        xBar = pad <> "└" <> Text.replicate plotW "─"
 
-        xTicks = ticks1D plotW (xNumTicks cfg) (xmin, xmax) False
-        xEnv n = AxisEnv (xmin, xmax) n 3
-        slotW = slotBudget plotW (max 1 (length xTicks))
+        slotW = fromMaybe (slotBudget plotW (max 1 ((if hasCategories then length categories else 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
         xLine =
-            placeLabels
-                (Text.replicate (left + 1 + plotW) " ")
-                (left + 1)
-                [(x, xFormatter cfg (xEnv x) slotW v) | (x, v) <- xTicks]
+            placeGridLabels
+                (Text.replicate (left + 1) " ")
+                slotW
+                (if hasCategories then categories else [xFormatter cfg (xEnv i) slotW v | (i, (_, v)) <- zip [0 ..] xTicks])
      in Text.unlines (attachY <> [xBar, xLine])
 
 placeLabels :: Text -> Int -> [(Int, Text)] -> Text
@@ -1071,6 +1069,12 @@
         let i = off + x
          in Text.take i acc <> s <> Text.drop (i + wcswidth s) acc
 
+placeGridLabels :: Text -> Int -> [Text] -> Text
+placeGridLabels base slotW = List.foldl' place base
+  where
+    place :: Text -> Text -> Text
+    place acc s = acc <> Text.take slotW (s <> (Text.replicate slotW " "))
+
 legendBlock :: LegendPos -> Int -> [(Text, Pat, Color)] -> Text
 legendBlock LegendBottom width entries =
     let cells = [sample pat col <> " " <> name | (name, pat, col) <- entries]
@@ -1212,6 +1216,10 @@
 minimum' xs = minimum xs
 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
diff --git a/test/GraniteSpec.hs b/test/GraniteSpec.hs
--- a/test/GraniteSpec.hs
+++ b/test/GraniteSpec.hs
@@ -3,317 +3,324 @@
 
 module GraniteSpec (spec) where
 
-import Test.Hspec
-import Test.QuickCheck
 import Data.Text (Text)
-import qualified Data.Text as Text
-import Numeric (showEFloat, showFFloat)
+import Data.Text qualified as Text
 import Granite
+import Numeric (showEFloat, showFFloat)
+import Test.Hspec
+import Test.QuickCheck
 
 spec :: Spec
 spec = do
-  describe "Data Types and Constructors" $ do
-    testDataTypes
-    
-  describe "Core Plotting Functions" $ do
-    testPlottingFunctions
-    
-  describe "Helper Functions" $ do
-    testHelperFunctions
-    
-  describe "Canvas and Rendering" $ do
-    testCanvasOperations
-    
-  describe "Formatting and Axis Handling" $ do
-    testFormatting
-    
-  describe "Edge Cases and Error Handling" $ do
-    testEdgeCases
+    describe "Data Types and Constructors" $ do
+        testDataTypes
 
+    describe "Core Plotting Functions" $ do
+        testPlottingFunctions
+
+    describe "Helper Functions" $ do
+        testHelperFunctions
+
+    describe "Canvas and Rendering" $ do
+        testCanvasOperations
+
+    describe "Formatting and Axis Handling" $ do
+        testFormatting
+
+    describe "Edge Cases and Error Handling" $ do
+        testEdgeCases
+
+    describe "Property tests" $ do
+        propTests
+
 testDataTypes :: Spec
 testDataTypes = do
-  describe "Plot record" $ do
-    it "defPlot should have correct default values" $ do
-      widthChars defPlot `shouldBe` 60
-      heightChars defPlot `shouldBe` 20
-      leftMargin defPlot `shouldBe` 6
-      bottomMargin defPlot `shouldBe` 2
-      titleMargin defPlot `shouldBe` 1
-      xBounds defPlot `shouldBe` (Nothing, Nothing)
-      yBounds defPlot `shouldBe` (Nothing, Nothing)
-      plotTitle defPlot `shouldBe` ""
-      legendPos defPlot `shouldBe` LegendRight
-      xNumTicks defPlot `shouldBe` 3
-      yNumTicks defPlot `shouldBe` 3
-      
-    it "should allow customization via record update" $ do
-      let customPlot = defPlot { widthChars = 80, plotTitle = "Test Chart" }
-      widthChars customPlot `shouldBe` 80
-      plotTitle customPlot `shouldBe` "Test Chart"
-      heightChars customPlot `shouldBe` 20
+    describe "Plot record" $ do
+        it "defPlot should have correct default values" $ do
+            widthChars defPlot `shouldBe` 60
+            heightChars defPlot `shouldBe` 20
+            leftMargin defPlot `shouldBe` 6
+            bottomMargin defPlot `shouldBe` 2
+            titleMargin defPlot `shouldBe` 1
+            xBounds defPlot `shouldBe` (Nothing, Nothing)
+            yBounds defPlot `shouldBe` (Nothing, Nothing)
+            plotTitle defPlot `shouldBe` ""
+            legendPos defPlot `shouldBe` LegendRight
+            xNumTicks defPlot `shouldBe` 3
+            yNumTicks defPlot `shouldBe` 3
 
-  describe "LegendPos enum" $ do
-    it "should have correct Show instances" $ do
-      show LegendRight `shouldBe` "LegendRight"
-      show LegendBottom `shouldBe` "LegendBottom"
-      
-    it "should have correct Eq instances" $ do
-      LegendRight `shouldBe` LegendRight
-      LegendBottom `shouldBe` LegendBottom
-      LegendRight `shouldNotBe` LegendBottom
+        it "should allow customization via record update" $ do
+            let customPlot = defPlot{widthChars = 80, plotTitle = "Test Chart"}
+            widthChars customPlot `shouldBe` 80
+            plotTitle customPlot `shouldBe` "Test Chart"
+            heightChars customPlot `shouldBe` 20
 
-  describe "Color enum" $ do
-    it "should convert to correct ANSI codes" $ do
-      ansiCode Black `shouldBe` 30
-      ansiCode Red `shouldBe` 31
-      ansiCode BrightBlue `shouldBe` 94
-      ansiCode Default `shouldBe` 39
-      
-  describe "Bins constructor" $ do
-    it "should create bins with correct values" $ do
-      let b = bins 10 0 100
-      nBins b `shouldBe` 10
-      lo b `shouldBe` 0
-      hi b `shouldBe` 100
-      
-    it "should handle reversed bounds" $ do
-      let b = bins 10 100 0
-      lo b `shouldBe` 0
-      hi b `shouldBe` 100
-      
-    it "should enforce minimum of 1 bin" $ do
-      let b = bins (-5) 0 100
-      nBins b `shouldBe` 1
+    describe "LegendPos enum" $ do
+        it "should have correct Show instances" $ do
+            show LegendRight `shouldBe` "LegendRight"
+            show LegendBottom `shouldBe` "LegendBottom"
 
+        it "should have correct Eq instances" $ do
+            LegendRight `shouldBe` LegendRight
+            LegendBottom `shouldBe` LegendBottom
+            LegendRight `shouldNotBe` LegendBottom
+
+    describe "Color enum" $ do
+        it "should convert to correct ANSI codes" $ do
+            ansiCode Black `shouldBe` 30
+            ansiCode Red `shouldBe` 31
+            ansiCode BrightBlue `shouldBe` 94
+            ansiCode Default `shouldBe` 39
+
+    describe "Bins constructor" $ do
+        it "should create bins with correct values" $ do
+            let b = bins 10 0 100
+            nBins b `shouldBe` 10
+            lo b `shouldBe` 0
+            hi b `shouldBe` 100
+
+        it "should handle reversed bounds" $ do
+            let b = bins 10 100 0
+            lo b `shouldBe` 0
+            hi b `shouldBe` 100
+
+        it "should enforce minimum of 1 bin" $ do
+            let b = bins (-5) 0 100
+            nBins b `shouldBe` 1
+
 testPlottingFunctions :: Spec
 testPlottingFunctions = do
-  describe "series function" $ do
-    it "should create a named data series" $ do
-      let s = series "test" [(1, 2), (3, 4)]
-      fst s `shouldBe` "test"
-      snd s `shouldBe` [(1, 2), (3, 4)]
+    describe "series function" $ do
+        it "should create a named data series" $ do
+            let s = series "test" [(1, 2), (3, 4)]
+            fst s `shouldBe` "test"
+            snd s `shouldBe` [(1, 2), (3, 4)]
 
-  describe "scatter plot" $ do
-    it "should generate non-empty output for valid data" $ do
-      let points = [(1, 1), (2, 2), (3, 3)]
-          chart = scatter [series "line" points] defPlot
-      Text.length chart `shouldSatisfy` (> 0)
-      
-    it "should handle empty data gracefully" $ do
-      let chart = scatter [] defPlot
-      Text.length chart `shouldSatisfy` (> 0)
-      
-    it "should include series name in legend" $ do
-      let points = [(1, 1), (2, 2)]
-          chart = scatter [series "test series" points] defPlot
-      chart `shouldSatisfy` Text.isInfixOf "test series"
-      
-    it "should handle multiple series" $ do
-      let s1 = series "Series 1" [(1, 1), (2, 2)]
-          s2 = series "Series 2" [(1, 2), (2, 1)]
-          chart = scatter [s1, s2] defPlot
-      chart `shouldSatisfy` Text.isInfixOf "Series 1"
-      chart `shouldSatisfy` Text.isInfixOf "Series 2"
+    describe "scatter plot" $ do
+        it "should generate non-empty output for valid data" $ do
+            let points = [(1, 1), (2, 2), (3, 3)]
+                chart = scatter [series "line" points] defPlot
+            Text.length chart `shouldSatisfy` (> 0)
 
-  describe "line graph" $ do
-    it "should generate non-empty output" $ do
-      let points = [(1, sin 1), (2, sin 2), (3, sin 3)]
-          chart = lineGraph [series "sine" points] defPlot
-      Text.length chart `shouldSatisfy` (> 0)
-      
-    it "should sort points by x-coordinate internally" $ do
-      let unsorted = [(3, 3), (1, 1), (2, 2)]
-          chart = lineGraph [series "unsorted" unsorted] defPlot
-      Text.length chart `shouldSatisfy` (> 0)
+        it "should handle empty data gracefully" $ do
+            let chart = scatter [] defPlot
+            Text.length chart `shouldSatisfy` (> 0)
 
-  describe "bar chart" $ do
-    it "should handle categorical data" $ do
-      let data' = [("A", 10), ("B", 20), ("C", 15)]
-          chart = bars data' defPlot
-      Text.length chart `shouldSatisfy` (> 0)
-      
-    it "should include category names" $ do
-      let data' = [("Apple", 10), ("Banana", 20)]
-          chart = bars data' defPlot
-      chart `shouldSatisfy` Text.isInfixOf "Apple"
-      chart `shouldSatisfy` Text.isInfixOf "Banana"
+        it "should include series name in legend" $ do
+            let points = [(1, 1), (2, 2)]
+                chart = scatter [series "test series" points] defPlot
+            chart `shouldSatisfy` Text.isInfixOf "test series"
 
-  describe "histogram" $ do
-    it "should bin numerical data" $ do
-      let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
-          chart = histogram (bins 5 0 10) values defPlot
-      Text.length chart `shouldSatisfy` (> 0)
-      
-    it "should handle empty data" $ do
-      let chart = histogram (bins 5 0 10) [] defPlot
-      Text.length chart `shouldSatisfy` (> 0)
+        it "should handle multiple series" $ do
+            let s1 = series "Series 1" [(1, 1), (2, 2)]
+                s2 = series "Series 2" [(1, 2), (2, 1)]
+                chart = scatter [s1, s2] defPlot
+            chart `shouldSatisfy` Text.isInfixOf "Series 1"
+            chart `shouldSatisfy` Text.isInfixOf "Series 2"
 
-  describe "pie chart" $ do
-    it "should handle proportional data" $ do
-      let data' = [("A", 25), ("B", 50), ("C", 25)]
-          chart = pie data' defPlot
-      Text.length chart `shouldSatisfy` (> 0)
-      
-    it "should normalize values to proportions" $ do
-      let data' = [("A", 1), ("B", 2), ("C", 1)]
-          chart = pie data' defPlot
-      Text.length chart `shouldSatisfy` (> 0)
+    describe "line graph" $ do
+        it "should generate non-empty output" $ do
+            let points = [(1, sin 1), (2, sin 2), (3, sin 3)]
+                chart = lineGraph [series "sine" points] defPlot
+            Text.length chart `shouldSatisfy` (> 0)
 
-  describe "heatmap" $ do
-    it "should handle 2D matrix data" $ do
-      let matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
-          chart = heatmap matrix defPlot
-      Text.length chart `shouldSatisfy` (> 0)
+        it "should sort points by x-coordinate internally" $ do
+            let unsorted = [(3, 3), (1, 1), (2, 2)]
+                chart = lineGraph [series "unsorted" unsorted] defPlot
+            Text.length chart `shouldSatisfy` (> 0)
 
-  describe "box plot" $ do
-    it "should calculate quartiles correctly" $ do
-      let data1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
-          data2 = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
-          chart = boxPlot [("Group1", data1), ("Group2", data2)] defPlot
-      Text.length chart `shouldSatisfy` (> 0)
-      
-    it "should handle single data point" $ do
-      let chart = boxPlot [("Single", [5.0])] defPlot
-      Text.length chart `shouldSatisfy` (> 0)
+    describe "bar chart" $ do
+        it "should handle categorical data" $ do
+            let data' = [("A", 10), ("B", 20), ("C", 15)]
+                chart = bars data' defPlot
+            Text.length chart `shouldSatisfy` (> 0)
 
+        it "should include category names" $ do
+            let data' = [("Apple", 10), ("Banana", 20)]
+                chart = bars data' defPlot
+            chart `shouldSatisfy` Text.isInfixOf "Apple"
+            chart `shouldSatisfy` Text.isInfixOf "Banana"
+
+    describe "histogram" $ do
+        it "should bin numerical data" $ do
+            let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+                chart = histogram (bins 5 0 10) values defPlot
+            Text.length chart `shouldSatisfy` (> 0)
+
+        it "should handle empty data" $ do
+            let chart = histogram (bins 5 0 10) [] defPlot
+            Text.length chart `shouldSatisfy` (> 0)
+
+    describe "pie chart" $ do
+        it "should handle proportional data" $ do
+            let data' = [("A", 25), ("B", 50), ("C", 25)]
+                chart = pie data' defPlot
+            Text.length chart `shouldSatisfy` (> 0)
+
+        it "should normalize values to proportions" $ do
+            let data' = [("A", 1), ("B", 2), ("C", 1)]
+                chart = pie data' defPlot
+            Text.length chart `shouldSatisfy` (> 0)
+
+    describe "heatmap" $ do
+        it "should handle 2D matrix data" $ do
+            let matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
+                chart = heatmap matrix defPlot
+            Text.length chart `shouldSatisfy` (> 0)
+
+    describe "box plot" $ do
+        it "should calculate quartiles correctly" $ do
+            let data1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+                data2 = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
+                chart = boxPlot [("Group1", data1), ("Group2", data2)] defPlot
+            Text.length chart `shouldSatisfy` (> 0)
+
+        it "should handle single data point" $ do
+            let chart = boxPlot [("Single", [5.0])] defPlot
+            Text.length chart `shouldSatisfy` (> 0)
+
 testHelperFunctions :: Spec
 testHelperFunctions = do
-  describe "ANSI color functions" $ do
-    it "ansiOn should generate correct escape sequence" $ do
-      let red = ansiOn Red
-      red `shouldSatisfy` Text.isPrefixOf "\ESC["
-      red `shouldSatisfy` Text.isSuffixOf "m"
-      
-    it "ansiOff should generate reset sequence" $ do
-      ansiOff `shouldBe` "\ESC[0m"
-      
-    it "paint should colorize characters correctly" $ do
-      let painted = paint Red 'X'
-      painted `shouldSatisfy` Text.isInfixOf "X"
-      painted `shouldSatisfy` Text.isPrefixOf "\ESC["
-      
-    it "paint should handle space characters specially" $ do
-      paint Red ' ' `shouldBe` " "
+    describe "ANSI color functions" $ do
+        it "ansiOn should generate correct escape sequence" $ do
+            let red = ansiOn Red
+            red `shouldSatisfy` Text.isPrefixOf "\ESC["
+            red `shouldSatisfy` Text.isSuffixOf "m"
 
-  describe "clamp function (internal)" $ do
-    it "should constrain values to range" $ property $ \(x :: Double) ->
-      let clamped = max 0 (min 10 x)
-      in clamped >= 0 && clamped <= 10
-      
-  describe "bounds calculation" $ do
-    it "should handle single point" $ do
-      let chart = scatter [series "single" [(5, 10)]] defPlot
-      Text.length chart `shouldSatisfy` (> 0)
+        it "ansiOff should generate reset sequence" $ do
+            ansiOff `shouldBe` "\ESC[0m"
 
+        it "paint should colorize characters correctly" $ do
+            let painted = paint Red 'X'
+            painted `shouldSatisfy` Text.isInfixOf "X"
+            painted `shouldSatisfy` Text.isPrefixOf "\ESC["
+
+        it "paint should handle space characters specially" $ do
+            paint Red ' ' `shouldBe` " "
+
+    describe "clamp function (internal)" $ do
+        it "should constrain values to range" $ property $ \(x :: Double) ->
+            let clamped = max 0 (min 10 x)
+             in clamped >= 0 && clamped <= 10
+
+    describe "bounds calculation" $ do
+        it "should handle single point" $ do
+            let chart = scatter [series "single" [(5, 10)]] defPlot
+            Text.length chart `shouldSatisfy` (> 0)
+
 testCanvasOperations :: Spec
 testCanvasOperations = do
-  describe "plot dimensions" $ do
-    it "should respect widthChars setting" $ do
-      let narrow = defPlot { widthChars = 20 }
-          wide = defPlot { widthChars = 100 }
-          chartNarrow = scatter [series "test" [(1, 1)]] narrow
-          chartWide = scatter [series "test" [(1, 1)]] wide
-      let narrowLines = Text.lines chartNarrow
-          wideLines = Text.lines chartWide
-      if not (null narrowLines) && not (null wideLines)
-        then Text.length (head wideLines) `shouldSatisfy` (> Text.length (head narrowLines))
-        else pendingWith "Charts generated empty lines"
-        
-    it "should respect heightChars setting" $ do
-      let short = defPlot { heightChars = 5 }
-          tall = defPlot { heightChars = 30 }
-          chartShort = scatter [series "test" [(1, 1)]] short
-          chartTall = scatter [series "test" [(1, 1)]] tall
-      length (Text.lines chartTall) `shouldSatisfy` (> length (Text.lines chartShort))
+    describe "plot dimensions" $ do
+        it "should respect widthChars setting" $ do
+            let narrow = defPlot{widthChars = 20}
+                wide = defPlot{widthChars = 100}
+                chartNarrow = scatter [series "test" [(1, 1)]] narrow
+                chartWide = scatter [series "test" [(1, 1)]] wide
+            let narrowLines = case Text.lines chartNarrow of
+                    [] -> ""
+                    (t : _) -> t
+                wideLines = case Text.lines chartWide of
+                    [] -> ""
+                    (t : _) -> t
+            if not (Text.null narrowLines) && not (Text.null wideLines)
+                then Text.length wideLines `shouldSatisfy` (> Text.length narrowLines)
+                else pendingWith "Charts generated empty lines"
 
+        it "should respect heightChars setting" $ do
+            let short = defPlot{heightChars = 5}
+                tall = defPlot{heightChars = 30}
+                chartShort = scatter [series "test" [(1, 1)]] short
+                chartTall = scatter [series "test" [(1, 1)]] tall
+            length (Text.lines chartTall) `shouldSatisfy` (> length (Text.lines chartShort))
+
 testFormatting :: Spec
 testFormatting = do
-  describe "default formatter" $ do
-    it "should format small numbers appropriately" $ do
-      let env = AxisEnv (0, 10) 0 5
-          formatted = fmt env 10 5.5
-      Text.length formatted `shouldSatisfy` (> 0)
-      
-    it "should use scientific notation for large numbers" $ do
-      let env = AxisEnv (0, 100000) 0 5
-          formatted = fmt env 10 50000
-      formatted `shouldSatisfy` Text.isInfixOf "e"
-      
-    it "should handle zero correctly" $ do
-      let env = AxisEnv (-1, 1) 0 5
-          formatted = fmt env 10 0
-      formatted `shouldSatisfy` (== "0.0")
+    describe "default formatter" $ do
+        it "should format small numbers appropriately" $ do
+            let env = AxisEnv (0, 10) 0 5
+                formatted = fmt env 10 5.5
+            Text.length formatted `shouldSatisfy` (> 0)
 
-  describe "plot titles" $ do
-    it "should include title when set" $ do
-      let titled = defPlot { plotTitle = "My Test Chart" }
-          chart = scatter [series "data" [(1, 1)]] titled
-      chart `shouldSatisfy` Text.isInfixOf "My Test Chart"
+        it "should use scientific notation for large numbers" $ do
+            let env = AxisEnv (0, 100000) 0 5
+                formatted = fmt env 10 50000
+            formatted `shouldSatisfy` Text.isInfixOf "e"
 
+        it "should handle zero correctly" $ do
+            let env = AxisEnv (-1, 1) 0 5
+                formatted = fmt env 10 0
+            formatted `shouldSatisfy` (== "0.0")
+
+    describe "plot titles" $ do
+        it "should include title when set" $ do
+            let titled = defPlot{plotTitle = "My Test Chart"}
+                chart = scatter [series "data" [(1, 1)]] titled
+            chart `shouldSatisfy` Text.isInfixOf "My Test Chart"
+
 testEdgeCases :: Spec
 testEdgeCases = do
-  describe "boundary conditions" $ do
-    it "should handle zero-width plots gracefully" $ do
-      let zeroWidth = defPlot { widthChars = 0 }
-          chart = scatter [series "test" [(1, 1)]] zeroWidth
-      Text.length chart `shouldSatisfy` (>= 0)
-      
-    it "should handle zero-height plots gracefully" $ do
-      let zeroHeight = defPlot { heightChars = 0 }
-          chart = scatter [series "test" [(1, 1)]] zeroHeight
-      Text.length chart `shouldSatisfy` (>= 0)
+    describe "boundary conditions" $ do
+        it "should handle zero-width plots gracefully" $ do
+            let zeroWidth = defPlot{widthChars = 0}
+                chart = scatter [series "test" [(1, 1)]] zeroWidth
+            Text.length chart `shouldSatisfy` (>= 0)
 
-  describe "data edge cases" $ do
-    it "should handle identical points" $ do
-      let identical = replicate 10 (5.0, 5.0)
-          chart = scatter [series "identical" identical] defPlot
-      Text.length chart `shouldSatisfy` (> 0)
-      
-    it "should handle very large numbers" $ do
-      let large = [(1e10, 1e10), (2e10, 2e10)]
-          chart = scatter [series "large" large] defPlot
-      Text.length chart `shouldSatisfy` (> 0)
-      
-    it "should handle very small numbers" $ do
-      let small = [(1e-10, 1e-10), (2e-10, 2e-10)]
-          chart = scatter [series "small" small] defPlot
-      Text.length chart `shouldSatisfy` (> 0)
+        it "should handle zero-height plots gracefully" $ do
+            let zeroHeight = defPlot{heightChars = 0}
+                chart = scatter [series "test" [(1, 1)]] zeroHeight
+            Text.length chart `shouldSatisfy` (>= 0)
 
-  describe "custom bounds" $ do
-    it "should respect manual x bounds" $ do
-      let bounded = defPlot { xBounds = (Just 0, Just 100) }
-          chart = scatter [series "test" [(50, 50)]] bounded
-      Text.length chart `shouldSatisfy` (> 0)
-      
-    it "should respect manual y bounds" $ do
-      let bounded = defPlot { yBounds = (Just (-10), Just 10) }
-          chart = scatter [series "test" [(0, 0)]] bounded
-      Text.length chart `shouldSatisfy` (> 0)
+    describe "data edge cases" $ do
+        it "should handle identical points" $ do
+            let identical = replicate 10 (5.0, 5.0)
+                chart = scatter [series "identical" identical] defPlot
+            Text.length chart `shouldSatisfy` (> 0)
 
-  describe "legend positioning" $ do
-    it "should handle bottom legend" $ do
-      let bottomLegend = defPlot { legendPos = LegendBottom }
-          chart = scatter [series "test" [(1, 1)]] bottomLegend
-      Text.length chart `shouldSatisfy` (> 0)
-      
-    it "should handle right legend" $ do
-      let rightLegend = defPlot { legendPos = LegendRight }
-          chart = scatter [series "test" [(1, 1)]] rightLegend
-      Text.length chart `shouldSatisfy` (> 0)
+        it "should handle very large numbers" $ do
+            let large = [(1e10, 1e10), (2e10, 2e10)]
+                chart = scatter [series "large" large] defPlot
+            Text.length chart `shouldSatisfy` (> 0)
 
-propTests :: Spec  
+        it "should handle very small numbers" $ do
+            let small = [(1e-10, 1e-10), (2e-10, 2e-10)]
+                chart = scatter [series "small" small] defPlot
+            Text.length chart `shouldSatisfy` (> 0)
+
+    describe "custom bounds" $ do
+        it "should respect manual x bounds" $ do
+            let bounded = defPlot{xBounds = (Just 0, Just 100)}
+                chart = scatter [series "test" [(50, 50)]] bounded
+            Text.length chart `shouldSatisfy` (> 0)
+
+        it "should respect manual y bounds" $ do
+            let bounded = defPlot{yBounds = (Just (-10), Just 10)}
+                chart = scatter [series "test" [(0, 0)]] bounded
+            Text.length chart `shouldSatisfy` (> 0)
+
+    describe "legend positioning" $ do
+        it "should handle bottom legend" $ do
+            let bottomLegend = defPlot{legendPos = LegendBottom}
+                chart = scatter [series "test" [(1, 1)]] bottomLegend
+            Text.length chart `shouldSatisfy` (> 0)
+
+        it "should handle right legend" $ do
+            let rightLegend = defPlot{legendPos = LegendRight}
+                chart = scatter [series "test" [(1, 1)]] rightLegend
+            Text.length chart `shouldSatisfy` (> 0)
+
+propTests :: Spec
 propTests = describe "Property-based tests" $ do
-  describe "QuickCheck properties" $ do
-    it "scatter plots should always produce non-empty output for non-empty data" $ 
-      property $ \(NonEmpty points) -> 
-        let chart = scatter [series "prop test" (points :: [(Double, Double)])] defPlot
-        in Text.length chart > 0
-        
-    it "bins should always have positive count and valid bounds" $
-      property $ \(Positive n) (a :: Double) (b :: Double) ->
-        let b' = bins n a b
-        in nBins b' >= 1 && lo b' <= hi b'
+    describe "QuickCheck properties" $ do
+        it "scatter plots should always produce non-empty output for non-empty data" $
+            property $ \(NonEmpty points) ->
+                let chart = scatter [series "prop test" (points :: [(Double, Double)])] defPlot
+                 in Text.length chart > 0
 
+        it "bins should always have positive count and valid bounds" $
+            property $ \(Positive n) (a :: Double) (b :: Double) ->
+                let b' = bins n a b
+                 in nBins b' >= 1 && lo b' <= hi b'
+
 ansiCode :: Color -> Int
 ansiCode Black = 30
 ansiCode Red = 31
@@ -346,6 +353,3 @@
 fmt _ _ v
     | abs v >= 10000 || abs v < 0.01 && v /= 0 = Text.pack (showEFloat (Just 1) v "")
     | otherwise = Text.pack (showFFloat (Just 1) v "")
-
-runTests :: IO ()
-runTests = hspec spec
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
