517517 ; ; Statistics
518518 :bin-method :sturges ; ; Sturges' rule: bin count = ceil(log2(n) + 1)
519519 :domain-padding 0.05
520+ ; ; Labels and titles
521+ :label-font-size 11 :title-font-size 13
522+ :label-offset 18 :title-offset 18
520523 ; ; Fallback
521524 :default-color " #333" })
522525
@@ -1041,12 +1044,14 @@ mpg
10411044 stat-y-domains (keep #(get-in % [:stat :y-domain ]) view-stats)
10421045
10431046 merged-x-dom (or x-domain
1047+ (:domain x-scale-spec) ; ; user-specified domain
10441048 (if (categorical-domain? (first stat-x-domains))
10451049 (distinct (mapcat identity stat-x-domains))
10461050 (let [lo (reduce min (map first stat-x-domains))
10471051 hi (reduce max (map second stat-x-domains))]
10481052 (pad-domain [lo hi] x-scale-spec))))
10491053 merged-y-dom (or y-domain
1054+ (:domain y-scale-spec) ; ; user-specified domain
10501055 (if (categorical-domain? (first stat-y-domains))
10511056 (distinct (mapcat identity stat-y-domains))
10521057 (let [lo (reduce min (map first stat-y-domains))
@@ -1255,9 +1260,11 @@ mpg
12551260; ; `aes` + `geom` from `theme` and rendering options.
12561261
12571262(defn plot
1258- " Render views as SVG. Options: :width :height :scales :coord :tooltip :brush :config"
1263+ " Render views as SVG. Options: :width :height :scales :coord :tooltip :brush :config
1264+ :x-label :y-label :title — axis labels auto-infer from column names, override here."
12591265 ([views] (plot views {}))
1260- ([views {:keys [width height scales coord tooltip brush config]}]
1266+ ([views {:keys [width height scales coord tooltip brush config
1267+ x-label y-label title] :as opts}]
12611268 (let [cfg (merge defaults config)
12621269 width (or width (:width cfg))
12631270 height (or height (:height cfg))
@@ -1303,13 +1310,30 @@ mpg
13031310 scale-mode (or scales :shared )
13041311 x-scale-spec (or (:x-scale (first non-ann-views)) {:type :linear })
13051312 y-scale-spec (or (:y-scale (first non-ann-views)) {:type :linear })
1306- global-x-doms (when (#{:shared :free-y } scale-mode)
1307- (collect-domain stat-results :x-domain x-scale-spec))
1308- global-y-doms (when (#{:shared :free-x } scale-mode)
1309- (compute-global-y-domain stat-results views y-scale-spec))
1313+ global-x-doms (or (:domain x-scale-spec)
1314+ (when (#{:shared :free-y } scale-mode)
1315+ (collect-domain stat-results :x-domain x-scale-spec)))
1316+ global-y-doms (or (:domain y-scale-spec)
1317+ (when (#{:shared :free-x } scale-mode)
1318+ (compute-global-y-domain stat-results views y-scale-spec)))
1319+ ; ; Axis labels: auto-infer unless multi-variable (SPLOM), allow override
1320+ auto-label? (not multi?)
1321+ eff-x-label (or x-label
1322+ (:label x-scale-spec)
1323+ (when auto-label?
1324+ (when-let [x (first x-vars)] (fmt-name x))))
1325+ eff-y-label (or y-label
1326+ (:label y-scale-spec)
1327+ (when auto-label?
1328+ (when-let [y (first y-vars)] (fmt-name y))))
1329+ eff-title title
1330+ ; ; Extra space for labels
1331+ x-label-pad (if eff-x-label (:label-offset cfg) 0 )
1332+ y-label-pad (if eff-y-label (:label-offset cfg) 0 )
1333+ title-pad (if eff-title (:title-offset cfg) 0 )
13101334 legend-w (if (or all-colors shape-categories) (:legend-width cfg) 0 )
1311- total-w (+ (* cols pw) legend-w)
1312- total-h (* rows ph)
1335+ total-w (+ y-label-pad (* cols pw) legend-w)
1336+ total-h (+ title-pad ( * rows ph) x-label-pad )
13131337 ctx {:non-ann-views non-ann-views :ann-views ann-views
13141338 :pw pw :ph ph :m m :rows rows :cols cols
13151339 :all-colors all-colors :tooltip-fn tooltip-fn
@@ -1324,13 +1348,36 @@ mpg
13241348 " xmlns" " http://www.w3.org/2000/svg"
13251349 " xmlns:xlink" " http://www.w3.org/1999/xlink"
13261350 " version" " 1.1" }
1351+ ; ; Plot title
1352+ (when eff-title
1353+ [:text {:x (+ y-label-pad (/ (* cols pw) 2 ))
1354+ :y 14
1355+ :text-anchor " middle" :font-size (:title-font-size cfg)
1356+ :fill " #333" :font-weight " bold" :font-family " sans-serif" }
1357+ eff-title])
1358+ ; ; Y-axis label (rotated)
1359+ (when eff-y-label
1360+ (let [cy (+ title-pad (/ (* rows ph) 2 ))]
1361+ [:text {:x 12 :y cy
1362+ :text-anchor " middle" :font-size (:label-font-size cfg)
1363+ :fill " #333" :font-family " sans-serif"
1364+ :transform (str " rotate(-90,12," cy " )" )}
1365+ eff-y-label]))
1366+ ; ; X-axis label
1367+ (when eff-x-label
1368+ [:text {:x (+ y-label-pad (/ (* cols pw) 2 ))
1369+ :y (- total-h 3 )
1370+ :text-anchor " middle" :font-size (:label-font-size cfg)
1371+ :fill " #333" :font-family " sans-serif" }
1372+ eff-x-label])
1373+ ; ; Legend (offset by label padding)
13271374 (when all-colors
13281375 (render-legend all-colors #(color-for all-colors %)
1329- :x (+ (* cols pw) 10 ) :y 20
1376+ :x (+ y-label-pad (* cols pw) 10 ) :y ( + title-pad 20 )
13301377 :title (first color-cols)))
13311378 (when shape-categories
1332- (let [y-off (if all-colors (+ 20 (* (count all-colors) 16 ) 10 ) 20 )
1333- x-off (+ (* cols pw) 10 )]
1379+ (let [y-off (+ title-pad ( if all-colors (+ 20 (* (count all-colors) 16 ) 10 ) 20 ) )
1380+ x-off (+ y-label-pad (* cols pw) 10 )]
13341381 (into [:g {:font-family " sans-serif" :font-size 10 }
13351382 (when shape-col [:text {:x x-off :y (- y-off 5 ) :fill " #333" :font-size 9 }
13361383 (fmt-name shape-col)])]
@@ -1340,7 +1387,9 @@ mpg
13401387 (if all-colors (color-for all-colors cat) " #333" ) {})
13411388 [:text {:x (+ x-off 15 ) :y (+ y-off (* i 16 ) 4 ) :fill " #333" }
13421389 (str cat)]]))))
1343- (into [:g ] (remove nil? (arrange-panels layout-type ctx)))]]
1390+ ; ; Panels (offset by label padding)
1391+ [:g {:transform (str " translate(" y-label-pad " ," title-pad " )" )}
1392+ (into [:g ] (remove nil? (arrange-panels layout-type ctx)))]]]
13441393 (wrap-plot (cond-> #{} tooltip (conj :tooltip ) brush (conj :brush )) svg-content))))
13451394
13461395; ; ---
@@ -2325,8 +2374,13 @@ mpg
23252374; ; ### ⚙️ Scale and Coord Setters
23262375
23272376(defn scale
2328- " Set a scale type for :x or :y across all views."
2329- ([views channel type] (scale views channel type {}))
2377+ " Set scale options for :x or :y across all views.
2378+ Accepts (views channel type), (views channel type opts), or (views channel opts-map).
2379+ opts-map may include :type, :domain, and :label."
2380+ ([views channel type-or-opts]
2381+ (if (map? type-or-opts)
2382+ (scale views channel (or (:type type-or-opts) :linear ) (dissoc type-or-opts :type ))
2383+ (scale views channel type-or-opts {})))
23302384 ([views channel type opts]
23312385 (let [k (case channel :x :x-scale :y :y-scale )]
23322386 (mapv #(assoc % k (merge {:type type} opts)) views))))
@@ -2364,6 +2418,37 @@ mpg
23642418 (scale :y :log )
23652419 plot)
23662420
2421+ ; ; ### 🧪 Custom Domain
2422+ ; ;
2423+ ; ; The `scale` function also accepts an options map with `:domain`
2424+ ; ; to clip or expand the axis range:
2425+
2426+ (-> iris
2427+ (view [[:sepal-length :sepal-width ]])
2428+ (lay (point {:color :species }))
2429+ (scale :x {:domain [4 8 ]})
2430+ plot)
2431+
2432+ ; ; ### 🧪 Axis Titles
2433+ ; ;
2434+ ; ; Axis labels are auto-inferred from column names.
2435+ ; ; Override with plot options:
2436+
2437+ (-> iris
2438+ (view [[:sepal-length :sepal-width ]])
2439+ (lay (point {:color :species }))
2440+ (plot {:x-label " Sepal Length (cm)"
2441+ :y-label " Sepal Width (cm)"
2442+ :title " Iris Measurements" }))
2443+
2444+ ; ; Or via the scale constructor — the label travels with the scale:
2445+
2446+ (-> iris
2447+ (view [[:sepal-length :sepal-width ]])
2448+ (lay (point {:color :species }))
2449+ (scale :x {:label " Length (cm)" })
2450+ plot)
2451+
23672452; ; ### ⚙️ Polar
23682453; ;
23692454; ; [Polar coordinates](https://en.wikipedia.org/wiki/Polar_coordinate_system)
@@ -2856,12 +2941,11 @@ mpg
28562941; ; into smaller steps -- a preparation phase, a rendering phase, and an
28572942; ; assembly phase -- would make it easier to understand and modify.
28582943; ;
2859- ; ; **No axis labels on standalone plots.**
2860- ; ; Single-panel plots show tick values but not axis titles like
2861- ; ; "sepal length" or "count." Multi-panel layouts use column headers above
2862- ; ; and beside the grid, which works well, but a standalone scatter plot
2863- ; ; gives no indication of what the axes represent beyond the tick values
2864- ; ; themselves.
2944+ ; ; **Axis labels are auto-inferred** from column names and shown on
2945+ ; ; standalone and faceted plots (but not SPLOM grids, which use column
2946+ ; ; headers). Override with `(plot views {:x-label "..." :title "..."})`
2947+ ; ; or `(scale :x {:label "..."})`. Custom domains also work:
2948+ ; ; `(scale :x {:domain [4 8]})`.
28652949; ;
28662950; ; **Partial validation.**
28672951; ; `view` checks that the specified columns actually exist in the
0 commit comments