Skip to content

Commit fb062e4

Browse files
authored
fix: train late-binding channel on its source column (#52)
2 parents e9417f4 + 19b68ed commit fb062e4

6 files changed

Lines changed: 63 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- feat: `labs()` fields default to `auto`; pass `none` to suppress an axis or legend title and reclaim the space it reserved. (#12)
1313
- feat: `element-blank()` on a text surface (axis, plot, or legend title) collapses the space the text would reserve. (#12)
1414
- feat: `width`/`height` accept `auto` to fill the available space of a bounded container. (#10)
15+
- fix: a `stage`/`after-scale` channel now trains its scale on the marker's source column, so the closure receives the channel's scale-resolved value as documented. (#52)
1516
- fix: grouped geoms (e.g., `geom-smooth`) no longer panic when a grouping aesthetic is mapped via `after-scale` or `stage`. (#51)
1617
- fix: facet-grid legends reserve space for a secondary x-axis so a top legend no longer overlaps its ticks. (#46)
1718
- fix: `geom-errorbar()`/`geom-errorbarh()`/`geom-rug()` resolve an `after-scale`/`stage` colour mapping instead of panicking. (#45)

src/scale/train.typ

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
#import "../data.typ": column
1010
#import "../utils/types.typ": infer-column-type, parse-number
1111
#import "../utils/typst-markup.typ": is-typst-markup
12-
#import "../utils/late-binding.typ": is-late-binding, late-binding-name
12+
#import "../utils/late-binding.typ": (
13+
after-scale-source, is-late-binding, late-binding-name,
14+
)
1315

1416
#let _resolve-mapping(layer, plot-mapping) = {
1517
if layer.at("inherit-aes", default: true) and plot-mapping != none {
@@ -260,19 +262,25 @@
260262
for a in aes-list {
261263
let raw = layer-mapping.at(a, default: none)
262264
if raw == none { continue }
263-
// Late-binding markers (`after-stat`, `from-theme`, ...) carry no
264-
// column to train against; evaluation happens in `_prepare-layer`
265-
// for `from-theme` and post-stat for `after-stat`.
266-
if is-late-binding(raw) { continue }
267-
let col-name = mapping-ref-col(raw)
265+
// Late-binding markers that carry a `source` column (an `after-scale`
266+
// derived from `stage(start: ...)`) train on that column so the per-row
267+
// resolver hands the closure the channel's scale-resolved value, as
268+
// documented on `@after-scale`. Source-less markers (`after-stat`,
269+
// `from-theme`, and a pure `after-scale` closure) are evaluated
270+
// elsewhere; skip them.
271+
let train-ref = if is-late-binding(raw) { after-scale-source(raw) } else {
272+
raw
273+
}
274+
if train-ref == none or is-late-binding(train-ref) { continue }
275+
let col-name = mapping-ref-col(train-ref)
268276
let entry = cache.at(a)
269277
entry.cols.push((
270278
name: col-name,
271279
values: column(layer-data, col-name),
272-
forced-type: _resolve-forced-type(raw, layer-data, col-name),
280+
forced-type: _resolve-forced-type(train-ref, layer-data, col-name),
273281
levels: layer-factor-levels.at(col-name, default: none),
274282
))
275-
if is-typst-markup(raw) { entry.typst-mark = true }
283+
if is-typst-markup(train-ref) { entry.typst-mark = true }
276284
cache.insert(a, entry)
277285
}
278286
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// A `stage`/`after-scale` marker that carries a `source` column trains the
2+
// channel scale on that column (post `apply-stages`), so the per-row resolver
3+
// hands the closure the scale-resolved value as `@after-scale` documents.
4+
// Regression for #50: previously `train` skipped every late-binding marker,
5+
// leaving the channel's domain empty and the closure receiving ink.
6+
7+
#import "../../src/render.typ": _prepare-layer
8+
#import "../../src/scale/train.typ": train
9+
#import "../../src/aes.typ": aes
10+
#import "../../src/utils/late-binding.typ": after-scale, stage
11+
#import "../../src/geom/point.typ": geom-point
12+
13+
#let raw = (
14+
(x: 1, y: 1, g: "a"),
15+
(x: 2, y: 2, g: "a"),
16+
(x: 3, y: 3, g: "b"),
17+
(x: 4, y: 4, g: "b"),
18+
)
19+
20+
// A stage-derived after-scale marker trains its channel on the source column.
21+
#let mapping-stage = aes(
22+
x: "x",
23+
y: "y",
24+
colour: stage(start: "g", after-scale: (c, _) => c),
25+
)
26+
#let prepared-stage = (_prepare-layer(geom-point(), mapping-stage, raw),)
27+
#let trained-stage = train(
28+
layers: prepared-stage,
29+
mapping: mapping-stage,
30+
data: raw,
31+
)
32+
#assert.eq(trained-stage.colour.type, "discrete")
33+
#assert.eq(trained-stage.colour.domain, ("a", "b"))
34+
35+
// A pure `after-scale` carries no source, so the channel scale stays untrained
36+
// (the closure is expected to compute the value from `ctx`).
37+
#let mapping-pure = aes(x: "x", y: "y", colour: after-scale((v, _) => v))
38+
#let prepared-pure = (_prepare-layer(geom-point(), mapping-pure, raw),)
39+
#let trained-pure = train(
40+
layers: prepared-pure,
41+
mapping: mapping-pure,
42+
data: raw,
43+
)
44+
#assert.eq(trained-pure.at("colour", default: none), none)
45+
46+
after-scale source training tests passed.
-47 Bytes
Loading
-88 Bytes
Loading
2.63 KB
Loading

0 commit comments

Comments
 (0)