diff --git a/dataframe-viz.cabal b/dataframe-viz.cabal
--- a/dataframe-viz.cabal
+++ b/dataframe-viz.cabal
@@ -1,6 +1,6 @@
-cabal-version:      2.4
+cabal-version:      3.4
 name:               dataframe-viz
-version:            1.0.3.0
+version:            1.1.0.0
 synopsis:           Visualisation/plotting helpers for the dataframe ecosystem.
 description:
     Display harness plus terminal and web plotters. Built on top of
@@ -36,13 +36,14 @@
                         DataFrame.Display.Internal.VegaLite
     build-depends:      base >= 4 && < 5,
                         aeson >= 0.11.0.0 && < 3,
-                        containers >= 0.6.7 && < 0.9,
-                        dataframe-core ^>= 1.1,
+                        containers >= 0.6.7 && < 0.10,
+                        dataframe-core >= 2.0 && < 2.1,
+                        dataframe-core:internal >= 2.0 && < 2.1,
                         directory >= 1.3.0.0 && < 2,
                         granite >= 0.6 && < 1,
                         process ^>= 1.6,
                         random >= 1 && < 2,
-                        text >= 2.0 && < 3,
-                        vector ^>= 0.13
+                        text >= 2.1 && < 3,
+                        vector >= 0.13 && < 0.15
     hs-source-dirs:     src
     default-language:   Haskell2010
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
@@ -9,11 +9,8 @@
 
 {- |
 Internal Vega-Lite spec model plus aeson encoding, shared by the web plot
-backends. Not part of the public API.
-
-The encoding medium is the untyped 'Expr'. A channel encoding resolves to a
-'ResolvedField' carrying the field name, the Vega-Lite field type (derived from
-the expression's Haskell element type) and the column values to inline.
+backends. Not public. Channel encodings resolve to a 'ResolvedField' carrying
+the field name, Vega-Lite type, and inlined column values.
 -}
 module DataFrame.Display.Internal.VegaLite (
     -- * Spec model
@@ -21,6 +18,7 @@
     Channel (..),
     FieldType (..),
     ChannelEnc (..),
+    Sort (..),
     ScaleSpec (..),
     ScaleType (..),
     defaultScale,
@@ -129,12 +127,20 @@
     , ceAggregate :: Maybe T.Text
     , ceBin :: Bool
     , ceScale :: ScaleSpec
+    , ceSort :: Maybe Sort
     }
 
--- | A bare channel encoding with no aggregation, binning, or scale options.
+-- | A bare channel encoding with no aggregation, binning, scale, or sort options.
 chanEnc :: Channel -> T.Text -> FieldType -> ChannelEnc
-chanEnc ch fld ft = ChannelEnc ch fld ft Nothing False defaultScale
+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.
+-}
+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.
@@ -314,7 +320,12 @@
             , fmap ("aggregate" .=) (ceAggregate e)
             , if ceBin e then Just ("bin" .= True) else Nothing
             , fmap ("scale" .=) (scaleValue (ceScale e))
+            , ("sort" .=) . sortValue <$> ceSort e
             ]
+
+-- | Render a sort spec to its Vega-Lite JSON value.
+sortValue :: Sort -> Value
+sortValue DataOrder = Null
 
 -- | Render a scale spec, or 'Nothing' when every field is at its default.
 scaleValue :: ScaleSpec -> Maybe Value
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
@@ -4,13 +4,9 @@
 {-# LANGUAGE NoFieldSelectors #-}
 
 {- |
-A plotly-express-style one-shot plotting API for HTML output. Mirrors
-'DataFrame.Display.Terminal.Plot' but emits embeddable, interactive Vega-Lite
-charts (rendered in the browser via vega-embed loaded from a CDN).
-
-This module is the string-keyed convenience tier. For an expression-based,
-composable grammar of graphics see "DataFrame.Display.Web.Chart" (untyped
-'Expr') and "DataFrame.Display.Web.Chart.Typed" (typed 'TExpr').
+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@.
 -}
 module DataFrame.Display.Web.Plot (
     -- * Aggregation
@@ -219,7 +215,7 @@
         vlSpec =
             (emptySpec VL.Bar)
                 { vlEncodings =
-                    [ chanEnc X spec.x Ordinal
+                    [ (chanEnc X spec.x Ordinal){VL.ceSort = Just VL.DataOrder}
                     , chanEnc Y "count" Quantitative
                     ]
                 , vlTitle = Just chartTitle
@@ -230,10 +226,9 @@
 -- 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.
+{- | '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.
 -}
 data Scatter = Scatter
     { x :: T.Text
@@ -299,7 +294,6 @@
                 [single] -> single <> " over " <> spec.x
                 _ -> T.intercalate ", " spec.y <> " over " <> spec.x
         xVals = extractNumericColumn spec.x df
-        -- Long-form melt: (x, value, series) so each y column becomes a line.
         perSeries =
             [ (col, zip xVals (extractNumericColumn col df))
             | col <- spec.y
