diff --git a/dataframe-viz.cabal b/dataframe-viz.cabal
--- a/dataframe-viz.cabal
+++ b/dataframe-viz.cabal
@@ -1,7 +1,6 @@
 cabal-version:      2.4
 name:               dataframe-viz
-version:            1.0.2.0
-
+version:            1.0.3.0
 synopsis:           Visualisation/plotting helpers for the dataframe ecosystem.
 description:
     Display harness plus terminal and web plotters. Built on top of
@@ -38,7 +37,7 @@
     build-depends:      base >= 4 && < 5,
                         aeson >= 0.11.0.0 && < 3,
                         containers >= 0.6.7 && < 0.9,
-                        dataframe-core ^>= 1.0,
+                        dataframe-core ^>= 1.1,
                         directory >= 1.3.0.0 && < 2,
                         granite >= 0.6 && < 1,
                         process ^>= 1.6,
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
@@ -47,7 +47,12 @@
 import GHC.Stack (HasCallStack)
 import Type.Reflection (TypeRep, typeRep)
 
-import DataFrame.Internal.Column (Column (..), Columnable, isNumeric)
+import DataFrame.Internal.Column (
+    Column (..),
+    Columnable,
+    isNumeric,
+    materializePacked,
+ )
 import DataFrame.Internal.DataFrame (DataFrame (..), getColumn)
 import DataFrame.Internal.Types
 
@@ -164,12 +169,14 @@
             Nothing -> V.toList $ V.map (T.pack . show) vec
     UnboxedColumn _ vec ->
         V.toList $ VG.map (T.pack . show) (VG.convert vec)
+    PackedText _ _ -> columnToStrings (materializePacked col)
 
 -- | Coerce a numeric column to @[Double]@; errors if the element type is not numeric.
 columnToDoubles :: (HasCallStack) => Column -> [Double]
 columnToDoubles col = case col of
     BoxedColumn _ vec -> vectorToDoubles vec
     UnboxedColumn _ vec -> unboxedVectorToDoubles vec
+    PackedText _ _ -> columnToDoubles (materializePacked col)
 
 vectorToDoubles :: forall a. (Columnable a, Show a) => V.Vector a -> [Double]
 vectorToDoubles vec =
@@ -206,6 +213,9 @@
                         Just (countBoxed (typeRep @a) vec)
                     UnboxedColumn _ (vec :: VU.Vector a) ->
                         Just (countUnboxed (typeRep @a) vec)
+                    PackedText _ _ -> case materializePacked col of
+                        BoxedColumn _ (vec :: V.Vector a) -> Just (countBoxed (typeRep @a) vec)
+                        _ -> Nothing
   where
     countBoxed ::
         forall a. (Show a) => TypeRep a -> V.Vector a -> [(T.Text, Double)]
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
@@ -21,6 +21,9 @@
     Channel (..),
     FieldType (..),
     ChannelEnc (..),
+    ScaleSpec (..),
+    ScaleType (..),
+    defaultScale,
     Transform (..),
     VLSpec (..),
     emptySpec,
@@ -125,13 +128,29 @@
     , ceType :: FieldType
     , ceAggregate :: Maybe T.Text
     , ceBin :: Bool
-    , ceLogScale :: Bool
+    , ceScale :: ScaleSpec
     }
 
--- | A bare channel encoding with no aggregation, binning, or log scale.
+-- | A bare channel encoding with no aggregation, binning, or scale options.
 chanEnc :: Channel -> T.Text -> FieldType -> ChannelEnc
-chanEnc ch fld ft = ChannelEnc ch fld ft Nothing False False
+chanEnc ch fld ft = ChannelEnc ch fld ft Nothing False defaultScale
 
+{- | 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.
+-}
+data ScaleSpec = ScaleSpec
+    { scaleType :: Maybe ScaleType
+    , scaleZero :: Maybe Bool
+    }
+    deriving (Eq, Show)
+
+data ScaleType = LogS
+    deriving (Eq, Show)
+
+defaultScale :: ScaleSpec
+defaultScale = ScaleSpec Nothing Nothing
+
 data Transform
     = -- | Fit @regression(yField) on xField@ (used inside a layer).
       RegressionT T.Text T.Text
@@ -294,10 +313,23 @@
             , Just ("type" .= fieldTypeName (ceType e))
             , fmap ("aggregate" .=) (ceAggregate e)
             , if ceBin e then Just ("bin" .= True) else Nothing
-            , if ceLogScale e
-                then Just ("scale" .= object ["type" .= ("log" :: T.Text)])
-                else Nothing
+            , fmap ("scale" .=) (scaleValue (ceScale e))
             ]
+
+-- | Render a scale spec, or 'Nothing' when every field is at its default.
+scaleValue :: ScaleSpec -> Maybe Value
+scaleValue s
+    | null pairs = Nothing
+    | otherwise = Just (object pairs)
+  where
+    pairs =
+        catMaybes
+            [ fmap (("type" .=) . scaleTypeName) (scaleType s)
+            , fmap ("zero" .=) (scaleZero s)
+            ]
+
+scaleTypeName :: ScaleType -> T.Text
+scaleTypeName LogS = "log"
 
 transformValue :: Transform -> Value
 transformValue (RegressionT yField xField) =
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
@@ -50,6 +50,7 @@
     regression,
     density,
     logScale,
+    includeZero,
     title,
     size,
 
@@ -78,6 +79,8 @@
     FieldType (..),
     Mark (..),
     ResolvedField (..),
+    ScaleSpec (..),
+    ScaleType (..),
     Transform (DensityT, RegressionT),
     VLSpec (..),
     chanEnc,
@@ -155,10 +158,21 @@
 binX = withChannel X (\e -> e{ceBin = True})
 binY = withChannel Y (\e -> e{ceBin = True})
 
--- | Put a channel on a log scale.
+{- | Put a channel on a log scale. Like all channel modifiers, apply it
+after 'enc' on the same channel ('enc' replaces the whole encoding).
+-}
 logScale :: Channel -> Chart -> Chart
-logScale ch = withChannel ch (\e -> e{ceLogScale = True})
+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).
+-}
+includeZero :: Channel -> Bool -> Chart -> Chart
+includeZero ch b = withScale ch (\s -> s{scaleZero = Just b})
+
 -- | Facet into small multiples by a column (alias for 'column').
 facet :: (Columnable a) => Expr a -> Chart -> Chart
 facet = column
@@ -246,10 +260,18 @@
 -- One-shot convenience plots (the simple single-line path)
 -- ---------------------------------------------------------------------------
 
--- | Scatter plot of two expressions.
+-- | Scatter plot of two expressions. Axes fit the data (no zero anchor).
 scatter ::
     (Columnable a, Columnable b) => Expr a -> Expr b -> DataFrame -> IO ()
-scatter xE yE df = showChart (chart df & mark Point & enc X xE & enc Y yE)
+scatter xE yE df =
+    showChart
+        ( chart df
+            & mark Point
+            & enc X xE
+            & enc Y yE
+            & includeZero X False
+            & includeZero Y False
+        )
 
 -- | Count of rows per category, as bars.
 bar :: (Columnable a) => Expr a -> DataFrame -> IO ()
@@ -260,9 +282,17 @@
 histogram xE df =
     showChart (chart df & mark Bar & enc X xE & binX & aggregateOn Y Count)
 
--- | Line chart of @y@ over @x@.
+-- | Line chart of @y@ over @x@. Axes fit the data (no zero anchor).
 line :: (Columnable a, Columnable b) => Expr a -> Expr b -> DataFrame -> IO ()
-line xE yE df = showChart (chart df & mark Line & enc X xE & enc Y yE)
+line xE yE df =
+    showChart
+        ( chart df
+            & mark Line
+            & enc X xE
+            & enc Y yE
+            & includeZero X False
+            & includeZero Y False
+        )
 
 -- | Pie chart counting rows per category.
 pie :: (Columnable a) => Expr a -> DataFrame -> IO ()
@@ -287,6 +317,10 @@
      in if any ((== ch) . ceChannel) encs
             then c{chEncs = map (\e -> if ceChannel e == ch then f e else e) encs}
             else c{chEncs = encs ++ [f (chanEnc ch "" Quantitative)]}
+
+-- | Modify a channel's scale spec (creating the encoding if absent).
+withScale :: Channel -> (ScaleSpec -> ScaleSpec) -> Chart -> Chart
+withScale ch f = withChannel ch (\e -> e{ceScale = f (ceScale e)})
 
 aggVegaName :: Agg -> T.Text
 aggVegaName a = case a of
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
@@ -55,6 +55,7 @@
     regression,
     density,
     logScale,
+    includeZero,
     title,
     size,
 
@@ -119,6 +120,10 @@
 -- | Put a channel on a log scale.
 logScale :: Channel -> Chart cols -> Chart cols
 logScale ch (Chart c) = Chart (C.logScale ch c)
+
+-- | Anchor (@True@) or release (@False@) a channel's scale at zero.
+includeZero :: Channel -> Bool -> Chart cols -> Chart cols
+includeZero ch b (Chart c) = Chart (C.includeZero ch b c)
 
 -- | Facet into small multiples by a column (alias for 'column').
 facet :: (Columnable a) => TExpr cols a -> Chart cols -> Chart cols
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
@@ -136,6 +136,10 @@
     let spec' = spec{vlWidth = sz.width, vlHeight = sz.height}
     return $ T.unpack $ specHtml chartId fields spec'
 
+-- | Pin a channel's zero anchor explicitly, on or off.
+anchorZero :: Bool -> VL.ChannelEnc -> VL.ChannelEnc
+anchorZero b e = e{VL.ceScale = VL.defaultScale{VL.scaleZero = Just b}}
+
 -- ---------------------------------------------------------------------------
 -- Bar
 -- ---------------------------------------------------------------------------
@@ -226,16 +230,22 @@
 -- Scatter
 -- ---------------------------------------------------------------------------
 
+{- | 'includeZero' anchors both axes at zero (Vega-Lite's own default).
+It is off by default so the axes fit the data, as expected of a scatter —
+data far from the origin (e.g. geographic coordinates) would otherwise be
+squashed against the chart edge.
+-}
 data Scatter = Scatter
     { x :: T.Text
     , y :: T.Text
     , color :: Maybe T.Text
     , title :: Maybe T.Text
     , size :: Size
+    , includeZero :: Bool
     }
 
 mkScatter :: T.Text -> T.Text -> Scatter
-mkScatter xc yc = Scatter xc yc Nothing Nothing defaultSize
+mkScatter xc yc = Scatter xc yc Nothing Nothing defaultSize False
 
 scatter :: (HasCallStack) => Scatter -> DataFrame -> IO String
 scatter spec df = do
@@ -245,7 +255,10 @@
         xVals = extractNumericColumn spec.x df
         yVals = extractNumericColumn spec.y df
         baseFields = [numField spec.x xVals, numField spec.y yVals]
-        baseEncs = [chanEnc X spec.x Quantitative, chanEnc Y spec.y Quantitative]
+        baseEncs =
+            map
+                (anchorZero spec.includeZero)
+                [chanEnc X spec.x Quantitative, chanEnc Y spec.y Quantitative]
         (fields, encs) = case spec.color of
             Nothing -> (baseFields, baseEncs)
             Just grp ->
@@ -263,15 +276,20 @@
 -- 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.
+-}
 data Line = Line
     { x :: T.Text
     , y :: [T.Text]
     , title :: Maybe T.Text
     , size :: Size
+    , includeZero :: Bool
     }
 
 mkLine :: T.Text -> [T.Text] -> Line
-mkLine xc ys = Line xc ys Nothing defaultSize
+mkLine xc ys = Line xc ys Nothing defaultSize False
 
 line :: (HasCallStack) => Line -> DataFrame -> IO String
 line spec df = do
@@ -297,8 +315,8 @@
         vlSpec =
             (emptySpec VL.Line)
                 { vlEncodings =
-                    [ chanEnc X spec.x Quantitative
-                    , chanEnc Y "value" Quantitative
+                    [ anchorZero spec.includeZero (chanEnc X spec.x Quantitative)
+                    , anchorZero spec.includeZero (chanEnc Y "value" Quantitative)
                     , chanEnc Color "series" Nominal
                     ]
                 , vlTitle = Just chartTitle
