diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-<!-- scripths: 0.5.3.0 -->
+<!-- scripths: 0.5.5.0 -->
 
 <!--
   This file is the runnable scripths source for dataframe-viz's README.
@@ -42,7 +42,7 @@
 -- cabal: packages: ../dataframe-operations, ../dataframe-viz
 -- cabal: default-extensions: OverloadedStrings, TypeApplications, OverloadedLabels
 -- cabal: default-extensions: DataKinds, TypeOperators, FlexibleContexts
-import DataFrame.Internal.DataFrame (DataFrame, fromNamedColumns)
+import DataFrame.Internal.DataFrame (DataFrame, fromColumns)
 import DataFrame.Internal.Column (fromList)
 import DataFrame.Expression.Operators ((|>))
 import qualified DataFrame.Functions as F
@@ -60,7 +60,7 @@
 import qualified Data.Aeson.KeyMap as KM
 import qualified Data.Text.Lazy as TL
 
-df = fromNamedColumns
+df = fromColumns
     [ ("income", fromList [1.5, 2.0, 3.1, 4.2, 5.0, 2.2, 3.3, 1.1 :: Double])
     , ("value",  fromList [100, 150, 200, 250, 300, 180, 220, 90 :: Double])
     , ("region", fromList (["INLAND","NEAR BAY","INLAND","NEAR OCEAN","ISLAND","INLAND","NEAR BAY","INLAND"] :: [Text]))
diff --git a/dataframe-viz.cabal b/dataframe-viz.cabal
--- a/dataframe-viz.cabal
+++ b/dataframe-viz.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.4
 name:               dataframe-viz
-version:            1.3.2.0
+version:            1.3.2.1
 synopsis:           Visualisation/plotting helpers for the dataframe ecosystem.
 description:
     Display harness plus terminal and web plotters. Built on top of
@@ -37,7 +37,7 @@
     build-depends:      base >= 4 && < 5,
                         aeson >= 0.11.0.0 && < 3,
                         containers >= 0.6.7 && < 0.10,
-                        dataframe-core >= 2.5 && < 2.6,
+                        dataframe-core >= 2.6 && < 2.7,
                         directory >= 1.3.0.0 && < 2,
                         granite >= 0.6 && < 1,
                         process ^>= 1.6,
diff --git a/src/DataFrame/Display.hs b/src/DataFrame/Display.hs
--- a/src/DataFrame/Display.hs
+++ b/src/DataFrame/Display.hs
@@ -8,17 +8,11 @@
 import qualified DataFrame.Internal.DataFrame as D
 
 data DisplayOptions = DisplayOptions
-    { {-- | Controls truncation on all three axes (rows, columns, cell width).
-      --
-      --   * 'Nothing' renders every row and column at full width.
-      --   * 'Just cfg' caps each axis whose limit in @cfg@ is positive.
-      --}
-      displayTruncate :: Maybe D.TruncateConfig
+    { displayTruncate :: Maybe D.TruncateConfig
+    -- ^ 'Nothing' shows everything; 'Just cfg' caps rows, columns, widths.
     }
 
-{- | Defaults aimed at terminal use: 10 rows, plus the standard column and cell
-caps from 'D.defaultTruncateConfig'.
--}
+-- | Terminal defaults: 10 rows plus the standard column and cell caps.
 defaultDisplayOptions :: DisplayOptions
 defaultDisplayOptions =
     DisplayOptions
diff --git a/src/DataFrame/Display/Internal/Common.hs b/src/DataFrame/Display/Internal/Common.hs
--- a/src/DataFrame/Display/Internal/Common.hs
+++ b/src/DataFrame/Display/Internal/Common.hs
@@ -7,10 +7,7 @@
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeApplications #-}
 
-{- |
-Internal shared helpers used by both the terminal and web plot backends.
-Not part of the public API.
--}
+-- | Internal helpers shared by the terminal and web plot backends.
 module DataFrame.Display.Internal.Common (
     -- * Aggregation
     Agg (..),
@@ -82,12 +79,7 @@
 aggLabel Min = "min"
 aggLabel Max = "max"
 
-{- | Apply an aggregation across rows grouped by the given category column.
-
-For 'Count', the value column is ignored. For all other aggregations, the
-value column is extracted as numeric and folded per group. Group order is
-the order in which categories first appear.
--}
+-- | Aggregate values across rows grouped by a category column.
 aggregateByGroup ::
     (HasCallStack) =>
     Agg ->
@@ -121,10 +113,7 @@
             reduce = numericReducer agg
          in [(g, reduce (M.findWithDefault [] g byGroup)) | g <- seen]
 
-{- | The numeric reducers, broken out so the overlapping-Count case in
-  'aggregateByGroup' can be dispatched at the head pattern without
-  producing a redundant inner match.
--}
+-- | The numeric reducers, split out from 'aggregateByGroup'.
 numericReducer :: Agg -> [Double] -> Double
 numericReducer Sum = sum
 numericReducer Mean = \vs -> if null vs then 0 else sum vs / fromIntegral (length vs)
@@ -161,7 +150,7 @@
         Nothing -> error $ "Column " ++ T.unpack colName ++ " not found"
         Just idx -> columnToDoubles (columns df V.! idx)
 
--- | Render a column's values as strings (identity for @Text@, @show@ otherwise).
+-- | Render a column's values as strings (@Text@ as is, @show@ otherwise).
 columnToStrings :: (HasCallStack) => Column -> [T.Text]
 columnToStrings col = case col of
     BoxedColumn _ (vec :: V.Vector a) ->
@@ -173,7 +162,7 @@
     PackedText _ _ -> columnToStrings (materializePacked col)
     MergedColumn _ _ -> columnToStrings (materializeMerged col)
 
--- | Coerce a numeric column to @[Double]@; errors if the element type is not numeric.
+-- | Coerce a numeric column to @[Double]@; errors on non-numeric elements.
 columnToDoubles :: (HasCallStack) => Column -> [Double]
 columnToDoubles col = case col of
     BoxedColumn _ vec -> vectorToDoubles vec
@@ -270,9 +259,7 @@
             M.toList $
                 L.foldl' (\acc x -> M.insertWith (+) (show x) (1 :: Int) acc) M.empty xs
 
-{- | Keep the top-N entries by value; fold the rest into a single "Other"
-bucket. Used to limit bar/pie counts to a readable number.
--}
+-- | Keep the top-N entries and fold the rest into an "Other" bucket.
 groupWithOther :: Int -> [(T.Text, Double)] -> [(T.Text, Double)]
 groupWithOther n items =
     let sorted = L.sortOn (negate . snd) items
diff --git a/src/DataFrame/Display/Internal/VegaLite.hs b/src/DataFrame/Display/Internal/VegaLite.hs
--- a/src/DataFrame/Display/Internal/VegaLite.hs
+++ b/src/DataFrame/Display/Internal/VegaLite.hs
@@ -7,11 +7,7 @@
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeApplications #-}
 
-{- |
-Internal Vega-Lite spec model plus aeson encoding, shared by the web plot
-backends. Not public. Channel encodings resolve to a 'ResolvedField' carrying
-the field name, Vega-Lite type, and inlined column values.
--}
+-- | Internal Vega-Lite spec model and aeson encoding for the web backends.
 module DataFrame.Display.Internal.VegaLite (
     -- * Spec model
     Mark (..),
@@ -62,10 +58,6 @@
 
 import DataFrame.Display.Internal.Common (columnToDoubles, columnToStrings)
 
--- ---------------------------------------------------------------------------
--- Spec model
--- ---------------------------------------------------------------------------
-
 data Mark = Bar | Line | Point | Area | Boxplot | Arc | Rule | Tick
     deriving (Eq, Show)
 
@@ -118,9 +110,7 @@
     Ordinal -> "ordinal"
     Temporal -> "temporal"
 
-{- | A single channel encoding. An empty 'ceField' with a 'ceAggregate' of
-@count@ produces a fieldless count aggregation, as Vega-Lite expects.
--}
+-- | A single channel encoding.
 data ChannelEnc = ChannelEnc
     { ceChannel :: Channel
     , ceField :: T.Text
@@ -131,21 +121,15 @@
     , ceSort :: Maybe Sort
     }
 
--- | A bare channel encoding with no aggregation, binning, scale, or sort options.
+-- | A bare channel encoding: no aggregation, binning, scale, or sort options.
 chanEnc :: Channel -> T.Text -> FieldType -> ChannelEnc
 chanEnc ch fld ft = ChannelEnc ch fld ft Nothing False defaultScale Nothing
 
-{- | Domain ordering for a discrete channel, mirroring Vega-Lite's @sort@.
-'DataOrder' pins the axis to the order rows appear in the inlined data, so
-pre-sorted values (e.g. histogram bins) display in that order.
--}
+-- | Domain ordering for a discrete channel, mirroring Vega-Lite's @sort@.
 data Sort = DataOrder
     deriving (Eq, Show)
 
-{- | Per-channel scale configuration, mirroring Vega-Lite's @scale@ object.
-'Nothing' fields leave the Vega-Lite default in place; grow this record
-(domain, nice, clamp, scheme, ...) as the public vocabulary grows.
--}
+-- | Per-channel scale configuration, mirroring Vega-Lite's @scale@ object.
 data ScaleSpec = ScaleSpec
     { scaleType :: Maybe ScaleType
     , scaleZero :: Maybe Bool
@@ -165,10 +149,7 @@
       DensityT T.Text
     deriving (Eq, Show)
 
-{- | A Vega-Lite spec. When 'vlLayers' is non-empty the spec is a layered
-container: the top level carries the shared data and each layer carries its own
-mark/encoding/transform.
--}
+-- | A Vega-Lite spec, layered when 'vlLayers' is non-empty.
 data VLSpec = VLSpec
     { vlMark :: Mark
     , vlEncodings :: [ChannelEnc]
@@ -182,14 +163,7 @@
 emptySpec :: Mark -> VLSpec
 emptySpec m = VLSpec m [] [] Nothing 600 400 []
 
--- ---------------------------------------------------------------------------
--- Field-type inference from the expression's element type
--- ---------------------------------------------------------------------------
-
-{- | Derive the Vega-Lite field type from a Haskell type: numeric →
-'Quantitative', date/time → 'Temporal', everything else → 'Nominal'. @Maybe a@
-classifies as its inner type.
--}
+-- | Derive the Vega-Lite field type from a Haskell type.
 fieldTypeOf :: forall a. (Columnable a) => FieldType
 fieldTypeOf = classify (typeRep @a)
 
@@ -223,20 +197,13 @@
     temporalNames =
         ["Day", "UTCTime", "LocalTime", "ZonedTime", "TimeOfDay"]
 
--- ---------------------------------------------------------------------------
--- Resolving an expression to a field + values
--- ---------------------------------------------------------------------------
-
 data ResolvedField = ResolvedField
     { rfName :: T.Text
     , rfType :: FieldType
     , rfValues :: [Value]
     }
 
-{- | Resolve an expression against a frame. A bare @Col name@ reuses the named
-column; any other expression is materialised with the core interpreter and
-stored under the given fallback name.
--}
+-- | Resolve an expression against a frame, reusing a bare column directly.
 resolveField ::
     forall a. (Columnable a) => DataFrame -> T.Text -> Expr a -> ResolvedField
 resolveField df fallbackName expr =
@@ -264,14 +231,10 @@
 textField :: T.Text -> [T.Text] -> ResolvedField
 textField name vals = ResolvedField name Nominal (map (toJSON :: T.Text -> Value) vals)
 
--- | A quantitative field built directly from numeric values (for pre-computed data).
+-- | A quantitative field built directly from pre-computed numeric values.
 numField :: T.Text -> [Double] -> ResolvedField
 numField name vals = ResolvedField name Quantitative (map toJSON vals)
 
--- ---------------------------------------------------------------------------
--- Encoding to JSON
--- ---------------------------------------------------------------------------
-
 schemaUrl :: T.Text
 schemaUrl = "https://vega.github.io/schema/vega-lite/v5.json"
 
@@ -293,7 +256,7 @@
             , Just ("data" .= object ["values" .= inlineRows fields])
             ]
 
--- | The mark/encoding/transform pairs of a unit spec (no data — that is shared).
+-- | The mark/encoding/transform pairs of a unit spec (data is shared).
 unitPairs :: VLSpec -> [(K.Key, Value)]
 unitPairs spec =
     catMaybes
@@ -357,14 +320,7 @@
         row i = object [K.fromText nm .= fromMaybe Null (vs V.!? i) | (nm, vs) <- vecs]
      in toJSON [row i | i <- [0 .. n - 1]]
 
--- ---------------------------------------------------------------------------
--- HTML embedding (vega-embed via CDN)
--- ---------------------------------------------------------------------------
-
-{- | Render a spec to a self-contained HTML snippet that loads vega/vega-lite/
-vega-embed from a CDN and embeds the chart. Data is inlined, so the snippet
-renders correctly even from a @file://@ URL.
--}
+-- | Render a spec to a self-contained HTML snippet with inlined data.
 specHtml :: T.Text -> [ResolvedField] -> VLSpec -> T.Text
 specHtml chartId fields spec =
     let specJson = TL.toStrict (encodeToLazyText (specToValue fields spec))
@@ -382,9 +338,7 @@
             , ");</script>\n"
             ]
 
-{- | Warn on stderr when a large number of rows is being inlined, which bloats
-the spec and can slow the browser.
--}
+-- | Warn on stderr when a large number of rows is being inlined.
 rowCountWarning :: [ResolvedField] -> IO ()
 rowCountWarning fields = do
     let n = maximum (0 : map (length . rfValues) fields)
diff --git a/src/DataFrame/Display/Terminal/Plot.hs b/src/DataFrame/Display/Terminal/Plot.hs
--- a/src/DataFrame/Display/Terminal/Plot.hs
+++ b/src/DataFrame/Display/Terminal/Plot.hs
@@ -3,18 +3,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE NoFieldSelectors #-}
 
-{- |
-A plotly-express-style one-shot plotting API for terminal output.
-
-Each chart kind has a small record spec (with sensible defaults) and a
-single render function. Customise plots by record-update on the spec:
-
-@
-bar ((mkBar "Sex") { y = Just "Survived" }) titanic
-bar ((mkBar "ocean_proximity")
-      { y = Just "median_house_value", agg = Mean }) housing
-@
--}
+-- | A plotly-express-style one-shot plotting API for terminal output.
 module DataFrame.Display.Terminal.Plot (
     -- * Aggregation
     Agg (..),
@@ -55,11 +44,7 @@
 
     -- * Deprecated legacy entry points
 
-    {- |
-    These shims delegate to the parametrized API. They are kept for
-    one release to ease migration; prefer the spec-based functions
-    above for new code.
-    -}
+    -- Deprecated shims that delegate to the parametrized API.
     plotHistogram,
     plotScatter,
     plotBars,
@@ -92,15 +77,7 @@
 import Granite (Plot, defPlot)
 import qualified Granite
 
--- ---------------------------------------------------------------------------
--- Bar
--- ---------------------------------------------------------------------------
-
-{- | Bar-chart specification.
-
-@x@ is required; everything else has a default. When @y@ is @Nothing@ the
-chart counts rows per group; otherwise @y@ is aggregated using @agg@.
--}
+-- | Bar-chart specification; @x@ is required and everything else defaults.
 data Bar = Bar
     { x :: T.Text
     -- ^ Grouping (categorical) column.
@@ -142,10 +119,6 @@
         cfg = spec.plot{Granite.plotTitle = t}
     T.putStrLn $ Granite.bars rows' cfg
 
--- ---------------------------------------------------------------------------
--- Histogram
--- ---------------------------------------------------------------------------
-
 -- | Histogram specification for a numeric column.
 data Histogram = Histogram
     { x :: T.Text
@@ -175,10 +148,6 @@
         cfg = spec.plot{Granite.plotTitle = t}
     T.putStrLn $ Granite.histogram (Granite.bins spec.bins lo hi) values cfg
 
--- ---------------------------------------------------------------------------
--- Scatter
--- ---------------------------------------------------------------------------
-
 -- | Scatter-plot specification.
 data Scatter = Scatter
     { x :: T.Text
@@ -220,10 +189,6 @@
                     ]
             T.putStrLn $ Granite.scatter series cfg
 
--- ---------------------------------------------------------------------------
--- Line
--- ---------------------------------------------------------------------------
-
 -- | Line-chart specification: one x column, one or more y series.
 data Line = Line
     { x :: T.Text
@@ -255,20 +220,12 @@
         return (col, zip xVals values)
     T.putStrLn $ Granite.lineGraph seriesData cfg
 
--- ---------------------------------------------------------------------------
--- Pie
--- ---------------------------------------------------------------------------
-
 -- | Pie-chart specification.
 data Pie = Pie
     { values :: T.Text
-    {- ^ Slice-defining column. For 'Count', categorical; for the
-    numeric aggs, the value column.
-    -}
+    -- ^ Slice-defining column: categorical for 'Count', else the value column.
     , names :: Maybe T.Text
-    {- ^ Optional label column. If @Nothing@, slices are labelled by
-    the value column directly (for 'Count') or by row index.
-    -}
+    -- ^ Optional label column; otherwise slices are labelled by value or index.
     , agg :: Agg
     , topN :: Maybe Int
     , title :: Maybe T.Text
@@ -307,10 +264,6 @@
         rows' = maybe rows (`groupWithOtherForPie` rows) spec.topN
     T.putStrLn $ Granite.pie rows' cfg
 
--- ---------------------------------------------------------------------------
--- Box
--- ---------------------------------------------------------------------------
-
 -- | Box-plot specification across one or more numeric columns.
 data Box = Box
     { y :: [T.Text]
@@ -335,13 +288,7 @@
     boxData <- forM spec.y $ \col -> return (col, extractNumericColumn col df)
     T.putStrLn $ Granite.boxPlot boxData cfg
 
--- ---------------------------------------------------------------------------
--- Whole-frame plots
--- ---------------------------------------------------------------------------
-
-{- | Heatmap of all numeric columns in the dataframe; each numeric column
-becomes a column of the matrix.
--}
+-- | Heatmap of all numeric columns in the dataframe.
 heatmap :: (HasCallStack) => DataFrame -> IO ()
 heatmap df = do
     let numericCols = filter (isNumericColumn df) (columnNames df)
@@ -370,19 +317,11 @@
             stdY = sqrt $ sum [(b - meanY) ^ (2 :: Int) | b <- ys] / n
          in covXY / (stdX * stdY)
 
--- ---------------------------------------------------------------------------
--- Title helper
--- ---------------------------------------------------------------------------
-
 autoTitle :: Agg -> Maybe T.Text -> T.Text -> T.Text
 autoTitle Count _ groupCol = "count by " <> groupCol
 autoTitle a (Just yCol) groupCol = aggLabel a <> "(" <> yCol <> ") by " <> groupCol
 autoTitle a Nothing groupCol = aggLabel a <> " by " <> groupCol
 
--- ---------------------------------------------------------------------------
--- Deprecated legacy entry points
--- ---------------------------------------------------------------------------
-
 -- | Use 'histogram' with 'mkHistogram' instead.
 plotHistogram :: (HasCallStack) => T.Text -> DataFrame -> IO ()
 plotHistogram c = histogram (mkHistogram c)
@@ -415,10 +354,7 @@
 plotBoxPlots ys = box (mkBox ys)
 {-# DEPRECATED plotBoxPlots "use 'box (mkBox cols)' instead" #-}
 
-{- | Stacked-bar plot: one bar per category, segments from the listed value
-columns. Kept as a direct passthrough to granite — has no spec record
-because no aggregation is involved (segments are summed per category).
--}
+-- | Stacked-bar plot with one bar per category, passed straight to granite.
 plotStackedBars :: (HasCallStack) => T.Text -> [T.Text] -> DataFrame -> IO ()
 plotStackedBars categoryCol valueColumns df = do
     let categories = extractStringColumn categoryCol df
diff --git a/src/DataFrame/Display/Web/Chart.hs b/src/DataFrame/Display/Web/Chart.hs
--- a/src/DataFrame/Display/Web/Chart.hs
+++ b/src/DataFrame/Display/Web/Chart.hs
@@ -1,30 +1,7 @@
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE OverloadedStrings #-}
 
-{- |
-A composable, expression-based grammar of graphics over the untyped 'Expr'.
-Charts are built by piping combinators and rendered to interactive Vega-Lite.
-
-@
-import qualified DataFrame.Display.Web.Chart as Plt
-import DataFrame.Functions (col)
-
-Plt.showChart
-  ( Plt.chart df
-      |> Plt.mark Plt.Point
-      |> Plt.enc Plt.X (col \@Double "age")
-      |> Plt.enc Plt.Y (col \@Double "fare")
-      |> Plt.enc Plt.Color (col \@Text "class")
-  )
-@
-
-The element type of each encoded expression determines the Vega-Lite field
-type (numeric → quantitative, date/time → temporal, otherwise nominal). A bare
-column expression encodes that column; any other expression is materialised
-with the core interpreter and inlined under the channel's name.
-
-For the schema-checked typed variant see "DataFrame.Display.Web.Chart.Typed".
--}
+-- | Composable expression-based grammar of graphics rendered to Vega-Lite.
 module DataFrame.Display.Web.Chart (
     -- * Chart
     Chart,
@@ -96,13 +73,7 @@
 import DataFrame.Internal.DataFrame (DataFrame)
 import DataFrame.Internal.Expression (Expr)
 
--- ---------------------------------------------------------------------------
--- Chart builder
--- ---------------------------------------------------------------------------
-
-{- | An in-progress chart: the source frame, a mark, channel encodings, the
-data fields to inline, transforms, and (for layered charts) sub-layers.
--}
+-- | An in-progress chart: frame, mark, encodings, transforms, and layers.
 data Chart = Chart
     { chDf :: DataFrame
     , chMark :: Mark
@@ -123,9 +94,7 @@
 mark :: Mark -> Chart -> Chart
 mark m c = c{chMark = m}
 
-{- | Encode an expression on a channel. The field type is inferred from the
-expression's element type; use 'encAs' to override.
--}
+-- | Encode an expression on a channel, inferring the field type.
 enc :: (Columnable a) => Channel -> Expr a -> Chart -> Chart
 enc ch e c =
     let rf = resolveField (chDf c) (channelName ch) e
@@ -139,9 +108,7 @@
         ce = chanEnc ch (rfName rf) ft
      in c{chEncs = setChannel ce (chEncs c), chFields = chFields c ++ [rf]}
 
-{- | Apply a declarative aggregation to a channel (computed by Vega-Lite). For
-'Count' the field is dropped, matching Vega-Lite's fieldless count.
--}
+-- | Apply a Vega-Lite aggregation to a channel.
 aggregateOn :: Channel -> Agg -> Chart -> Chart
 aggregateOn ch a =
     withChannel
@@ -158,18 +125,11 @@
 binX = withChannel X (\e -> e{ceBin = True})
 binY = withChannel Y (\e -> e{ceBin = True})
 
-{- | Put a channel on a log scale. Like all channel modifiers, apply it
-after 'enc' on the same channel ('enc' replaces the whole encoding).
--}
+-- | Put a channel on a log scale; apply it after 'enc'.
 logScale :: Channel -> Chart -> Chart
 logScale ch = withScale ch (\s -> s{scaleType = Just LogS})
 
-{- | Anchor (@True@) or release (@False@) a channel's scale at zero.
-Vega-Lite includes zero on quantitative scales by default, which squashes
-data far from the origin (e.g. geographic coordinates) against the chart
-edge; @includeZero X False@ lets the axis fit the data instead. Apply it
-after 'enc' on the same channel ('enc' replaces the whole encoding).
--}
+-- | Anchor or release a channel's scale at zero; apply it after 'enc'.
 includeZero :: Channel -> Bool -> Chart -> Chart
 includeZero ch b = withScale ch (\s -> s{scaleZero = Just b})
 
@@ -190,9 +150,7 @@
 size :: Int -> Int -> Chart -> Chart
 size w h c = c{chW = w, chH = h}
 
-{- | Overlay several charts that share data into a single layered chart. The
-title and size of the first layer are used for the container.
--}
+-- | Overlay several charts that share data into one layered chart.
 layer :: [Chart] -> Chart
 layer [] = error "DataFrame.Display.Web.Chart.layer: empty layer list"
 layer cs@(c0 : _) =
@@ -201,9 +159,7 @@
         , chFields = concatMap chFields cs
         }
 
-{- | Add a regression (least-squares) line over the chart, fitting @y@ on @x@.
-Produces a layered chart: the original marks plus a fitted line.
--}
+-- | Add a least-squares regression line fitting @y@ on @x@.
 regression :: (Columnable a, Columnable b) => Expr a -> Expr b -> Chart -> Chart
 regression xE yE c =
     let rfx = resolveField (chDf c) "x" xE
@@ -222,9 +178,7 @@
                 }
      in withData{chLayers = [points, lineLayer]}
 
-{- | Kernel-density estimate of an expression, drawn as an area. Replaces the
-chart's mark and encodings with the density curve (Vega-Lite @density@).
--}
+-- | Kernel-density estimate of an expression, drawn as an area.
 density :: (Columnable a) => Expr a -> Chart -> Chart
 density e c =
     let rf = resolveField (chDf c) "value" e
@@ -238,11 +192,7 @@
                 ]
             }
 
--- ---------------------------------------------------------------------------
--- Rendering
--- ---------------------------------------------------------------------------
-
--- | The Vega-Lite spec as an aeson 'Value' (escape hatch for advanced use / hvega).
+-- | The Vega-Lite spec as an aeson 'Value' (escape hatch, e.g. for hvega).
 toVegaSpec :: Chart -> Value
 toVegaSpec c = specToValue (allFields c) (toVLSpec c)
 
@@ -256,10 +206,6 @@
     rowCountWarning (allFields c)
     showInDefaultBrowser (toHtml c)
 
--- ---------------------------------------------------------------------------
--- One-shot convenience plots (the simple single-line path)
--- ---------------------------------------------------------------------------
-
 -- | Scatter plot of two expressions. Axes fit the data (no zero anchor).
 scatter ::
     (Columnable a, Columnable b) => Expr a -> Expr b -> DataFrame -> IO ()
@@ -301,10 +247,6 @@
 -- | Box-and-whisker plot of a numeric expression.
 box :: (Columnable a) => Expr a -> DataFrame -> IO ()
 box yE df = showChart (chart df & mark Boxplot & enc Y yE)
-
--- ---------------------------------------------------------------------------
--- Internals
--- ---------------------------------------------------------------------------
 
 -- | Replace any existing encoding on the same channel (last write wins).
 setChannel :: ChannelEnc -> [ChannelEnc] -> [ChannelEnc]
diff --git a/src/DataFrame/Display/Web/Chart/Typed.hs b/src/DataFrame/Display/Web/Chart/Typed.hs
--- a/src/DataFrame/Display/Web/Chart/Typed.hs
+++ b/src/DataFrame/Display/Web/Chart/Typed.hs
@@ -2,34 +2,7 @@
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE KindSignatures #-}
 
-{- |
-The schema-checked, typed-expression analogue of "DataFrame.Display.Web.Chart".
-Charts are built over a 'TypedDataFrame' using typed expressions ('TExpr',
-@#col@), so column references are checked against the schema at compile time
-and the field type follows from the column's Haskell type.
-
-@
-{\-\# LANGUAGE OverloadedLabels \#-\}
-import qualified DataFrame.Display.Web.Chart.Typed as Plt
-
--- one-liner
-Plt.scatter #age #fare tdf
-
--- composable grammar
-Plt.showChart
-  ( Plt.chart tdf
-      |> Plt.mark Plt.Boxplot
-      |> Plt.enc Plt.X #region
-      |> Plt.enc Plt.Y #value
-      |> Plt.enc Plt.Color #grp
-      |> Plt.facet #grp
-  )
-@
-
-Every combinator delegates to "DataFrame.Display.Web.Chart" by unwrapping the
-typed expression ('unTExpr') and frame ('unTDF'); the @cols@ phantom is erased
-at the boundary.
--}
+-- | Schema-checked typed-expression analogue of "DataFrame.Display.Web.Chart".
 module DataFrame.Display.Web.Chart.Typed (
     -- * Chart
     Chart,
@@ -157,7 +130,7 @@
 density :: (Columnable a) => TExpr cols a -> Chart cols -> Chart cols
 density e (Chart c) = Chart (C.density (unTExpr e) c)
 
--- | The Vega-Lite spec as an aeson 'Value' (escape hatch for advanced use / hvega).
+-- | The Vega-Lite spec as an aeson 'Value' (escape hatch, e.g. for hvega).
 toVegaSpec :: Chart cols -> Value
 toVegaSpec (Chart c) = C.toVegaSpec c
 
diff --git a/src/DataFrame/Display/Web/Plot.hs b/src/DataFrame/Display/Web/Plot.hs
--- a/src/DataFrame/Display/Web/Plot.hs
+++ b/src/DataFrame/Display/Web/Plot.hs
@@ -3,11 +3,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE NoFieldSelectors #-}
 
-{- |
-A plotly-express-style one-shot plotting API for HTML output: the string-keyed
-convenience tier emitting interactive Vega-Lite charts. For the composable
-expression-based grammar see "DataFrame.Display.Web.Chart" and its @.Typed@.
--}
+-- | Plotly-express-style one-shot plotting API emitting Vega-Lite HTML.
 module DataFrame.Display.Web.Plot (
     -- * Aggregation
     Agg (..),
@@ -108,10 +104,6 @@
 import qualified DataFrame.Display.Internal.VegaLite as VL
 import DataFrame.Internal.DataFrame (DataFrame, columnNames)
 
--- ---------------------------------------------------------------------------
--- Layout
--- ---------------------------------------------------------------------------
-
 -- | Display dimensions for the chart, in pixels.
 data Size = Size {width :: Int, height :: Int}
 
@@ -138,10 +130,6 @@
 anchorZero :: Bool -> VL.ChannelEnc -> VL.ChannelEnc
 anchorZero b e = e{VL.ceScale = VL.defaultScale{VL.scaleZero = Just b}}
 
--- ---------------------------------------------------------------------------
--- Bar
--- ---------------------------------------------------------------------------
-
 data Bar = Bar
     { x :: T.Text
     , y :: Maybe T.Text
@@ -182,10 +170,6 @@
                 }
     renderSpec spec.size fields vlSpec
 
--- ---------------------------------------------------------------------------
--- Histogram
--- ---------------------------------------------------------------------------
-
 data Histogram = Histogram
     { x :: T.Text
     , bins :: Int
@@ -224,14 +208,7 @@
                 }
     renderSpec spec.size fields vlSpec
 
--- ---------------------------------------------------------------------------
--- Scatter
--- ---------------------------------------------------------------------------
-
-{- | 'includeZero' anchors both axes at zero. Off by default so the axes fit
-the data — otherwise points far from the origin (e.g. geographic coordinates)
-get squashed against the chart edge.
--}
+-- | Scatter spec; 'includeZero' anchors both axes at zero (off by default).
 data Scatter = Scatter
     { x :: T.Text
     , y :: T.Text
@@ -269,14 +246,7 @@
                 }
     renderSpec spec.size fields vlSpec
 
--- ---------------------------------------------------------------------------
--- Line
--- ---------------------------------------------------------------------------
-
-{- | 'includeZero' anchors the axes at zero (Vega-Lite's own default). Off
-by default so the axes fit the data — a line is position-encoded, so a
-forced zero baseline only squashes series far from the origin.
--}
+-- | Line spec; 'includeZero' anchors the axes at zero and is off by default.
 data Line = Line
     { x :: T.Text
     , y :: [T.Text]
@@ -319,10 +289,6 @@
                 }
     renderSpec spec.size fields vlSpec
 
--- ---------------------------------------------------------------------------
--- Pie
--- ---------------------------------------------------------------------------
-
 data Pie = Pie
     { values :: T.Text
     , names :: Maybe T.Text
@@ -369,10 +335,6 @@
                 }
     renderSpec spec.size fields vlSpec
 
--- ---------------------------------------------------------------------------
--- Box
--- ---------------------------------------------------------------------------
-
 data Box = Box
     { y :: [T.Text]
     , title :: Maybe T.Text
@@ -404,32 +366,18 @@
                 }
     renderSpec spec.size fields vlSpec
 
--- ---------------------------------------------------------------------------
--- Whole-frame helpers
--- ---------------------------------------------------------------------------
-
-{- | Concatenate a histogram for every numeric column in the frame. Useful
-as a one-shot exploratory summary.
--}
+-- | Concatenate a histogram for every numeric column in the frame.
 allHistograms :: (HasCallStack) => DataFrame -> IO String
 allHistograms df = do
     let cols = filter (isNumericColumn df) (columnNames df)
     xs <- forM cols $ \c -> histogram (mkHistogram c) df
     return $ L.intercalate "\n" xs
 
--- ---------------------------------------------------------------------------
--- Title helper
--- ---------------------------------------------------------------------------
-
 autoTitle :: Agg -> Maybe T.Text -> T.Text -> T.Text
 autoTitle Count _ groupCol = "count by " <> groupCol
 autoTitle a (Just yCol) groupCol = aggLabel a <> "(" <> yCol <> ") by " <> groupCol
 autoTitle a Nothing groupCol = aggLabel a <> " by " <> groupCol
 
--- ---------------------------------------------------------------------------
--- Deprecated legacy entry points
--- ---------------------------------------------------------------------------
-
 plotHistogram :: (HasCallStack) => T.Text -> DataFrame -> IO String
 plotHistogram c = histogram (mkHistogram c)
 {-# DEPRECATED plotHistogram "use 'histogram (mkHistogram col)' instead" #-}
@@ -455,10 +403,6 @@
 plotBoxPlots :: (HasCallStack) => [T.Text] -> DataFrame -> IO String
 plotBoxPlots ys = box (mkBox ys)
 {-# DEPRECATED plotBoxPlots "use 'box (mkBox cols)' instead" #-}
-
--- ---------------------------------------------------------------------------
--- Browser launcher
--- ---------------------------------------------------------------------------
 
 -- | Windows launches via a shell: 'start' is a cmd builtin.
 showInDefaultBrowser :: String -> IO ()
