Skip to content

Commit 17bc079

Browse files
committed
Merge branch 'main' into fix/text-angle-wiring
# Conflicts: # CHANGELOG.md
2 parents c05f88a + 4c014a5 commit 17bc079

7 files changed

Lines changed: 101 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
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)
1515
- fix: `element-text(angle:)`/`element-typst(angle:)` rotate axis tick labels (seeding the `guide-axis(angle:)` default) and the plot title, subtitle, and caption, instead of being ignored. (#59)
16+
- fix: `labs(tag:)` draws the figure tag above the title on a standalone plot, styled by the `plot-tag` theme element, instead of being ignored. (#58)
17+
- fix: `labs(alt:)` fills in the figure's accessibility alt text when `plot(alt:)` is unset, instead of being stored and ignored. (#57)
1618
- fix: `geom-qq()`/`geom-qq-line()` honour the `distribution` argument (`uniform`/`exponential`) instead of always plotting against the normal reference. (#56)
1719
- fix: the `font` set on `element-text`/`element-typst`/`element-geom` is applied to every text surface and to the text-drawing geoms, inheriting the base `text` font when unset. (#54)
1820
- 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)

docs/examples/gallery.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
description: 'Title, subtitle, caption, and axis labels set in a single [`labs`](../reference/labs/labs.qmd) call.'
2424
alt: "Scatter and line chart of count against month showing a rising trend, framed by a title, subtitle, caption, and axis labels."
2525

26+
- slug: labs-tag
27+
section: basics
28+
title: "Figure tag"
29+
description: 'A figure tag drawn above the title via [`labs`](../reference/labs/labs.qmd) `tag`, styled by the `plot-tag` theme element.'
30+
alt: "Scatter plot of highway fuel economy against engine displacement, marked with a bold 'A' tag above the title and subtitle."
31+
2632
- slug: factor-inline
2733
section: basics
2834
title: "Inline factor coercion"

examples/labs-tag.typ

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// A figure tag drawn above the title, styled by the plot-tag theme element.
2+
3+
#import "../lib.typ": *
4+
5+
#set page(width: auto, height: auto, margin: 0cm)
6+
7+
#plot(
8+
data: mpg,
9+
mapping: aes(x: "displ", y: "hwy"),
10+
layers: (geom-point(size: 2.5pt, alpha: 0.75),),
11+
labs: labs(
12+
tag: "A",
13+
title: "Engine Displacement Versus Highway Fuel Economy",
14+
subtitle: "One panel of a larger figure",
15+
x: "Displacement (L)",
16+
y: "Highway mpg",
17+
),
18+
theme: theme-minimal(),
19+
width: 12cm,
20+
height: 9cm,
21+
)

src/plot.typ

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
#import "render.typ": render-plot
22
#import "data.typ": _normalise-data
33

4+
// Effective alt text: an explicit `plot(alt:)` wins; otherwise a `labs(alt:)`
5+
// fills in (its `auto` default and `none` both count as unset), so the labs
6+
// field reaches `get-alt-text` instead of being dropped.
7+
#let _resolve-alt(alt, labs) = {
8+
if alt != none { return alt }
9+
if labs != none {
10+
let labs-alt = labs.at("alt", default: auto)
11+
if labs-alt != auto and labs-alt != none { return labs-alt }
12+
}
13+
none
14+
}
15+
416
/// Compose a layered plot from data, aesthetics, and geom layers.
517
///
618
/// `plot` is the entry point of the grammar: it resolves the dataset, wires up
@@ -104,6 +116,7 @@
104116
strict: false,
105117
defer: false,
106118
) = {
119+
let alt = _resolve-alt(alt, labs)
107120
// Deferred plots skip the context block because `context` returns
108121
// content; compose() resolves the active theme from its own context
109122
// before handing the spec to the renderer.

src/render.typ

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3093,6 +3093,7 @@
30933093
- outer-pad.right
30943094
- inner-inset.right
30953095
)
3096+
let tag = _text-style(theme, "plot-tag")
30963097
let title = _text-style(theme, "plot-title")
30973098
let subtitle = _text-style(theme, "plot-subtitle")
30983099
let caption = _text-style(theme, "plot-caption")
@@ -3113,6 +3114,9 @@
31133114
if style.angle != none { body = rotate(style.angle, reflow: true, body) }
31143115
box(width: inner-w, align(a, body))
31153116
} else { none }
3117+
let tag-block = if labs != none {
3118+
_chrome-block(labs.tag, tag, left, weight: tag.weight)
3119+
} else { none }
31163120
let title-block = if labs != none {
31173121
_chrome-block(labs.title, title, left, weight: title.weight)
31183122
} else { none }
@@ -3129,17 +3133,22 @@
31293133
_text-margin-cm(style, side, 0.6em) * 1cm
31303134
}
31313135

3136+
// Bottom margin of the lowest header block sets the gap above the canvas.
31323137
let above-canvas = if subtitle-block != none {
31333138
subtitle
3134-
} else if title-block != none { title } else { none }
3139+
} else if title-block != none {
3140+
title
3141+
} else if tag-block != none { tag } else { none }
31353142

31363143
(
31373144
plot-bg: plot-bg,
31383145
outer-pad: outer-pad,
31393146
inner-inset: inner-inset,
3147+
tag-block: tag-block,
31403148
title-block: title-block,
31413149
subtitle-block: subtitle-block,
31423150
caption-block: caption-block,
3151+
tag-gap: _gap-length(tag, "bottom"),
31433152
inter-title-gap: _gap-length(title, "bottom"),
31443153
above-gap: if above-canvas != none {
31453154
_gap-length(above-canvas, "bottom")
@@ -3154,14 +3163,26 @@
31543163
#let _decorate-extents(parts) = {
31553164
let _h(b) = if b == none { 0cm } else { measure(b).height }
31563165
let top = parts.outer-pad.top + parts.inner-inset.top
3157-
if parts.title-block != none { top += _h(parts.title-block) }
3166+
let headers = 0
3167+
if parts.tag-block != none {
3168+
top += _h(parts.tag-block)
3169+
headers += 1
3170+
}
3171+
if parts.title-block != none {
3172+
if headers > 0 { top += parts.tag-gap }
3173+
top += _h(parts.title-block)
3174+
headers += 1
3175+
}
31583176
if parts.subtitle-block != none {
3159-
if parts.title-block != none { top += parts.inter-title-gap }
3177+
if headers > 0 {
3178+
top += if parts.title-block != none { parts.inter-title-gap } else {
3179+
parts.tag-gap
3180+
}
3181+
}
31603182
top += _h(parts.subtitle-block)
3183+
headers += 1
31613184
}
3162-
if parts.title-block != none or parts.subtitle-block != none {
3163-
top += parts.above-gap
3164-
}
3185+
if headers > 0 { top += parts.above-gap }
31653186
let bottom = parts.outer-pad.bottom + parts.inner-inset.bottom
31663187
if parts.caption-block != none {
31673188
bottom += parts.caption-gap + _h(parts.caption-block)
@@ -3201,19 +3222,26 @@
32013222
),
32023223
)
32033224
} else { content }
3225+
let tag-block = parts.tag-block
32043226
let title-block = parts.title-block
32053227
let subtitle-block = parts.subtitle-block
32063228
let caption-block = parts.caption-block
32073229

32083230
let items = ()
3209-
if title-block != none { items.push(title-block) }
3231+
if tag-block != none { items.push(tag-block) }
3232+
if title-block != none {
3233+
if items.len() > 0 { items.push(v(parts.tag-gap)) }
3234+
items.push(title-block)
3235+
}
32103236
if subtitle-block != none {
3211-
if items.len() > 0 { items.push(v(parts.inter-title-gap)) }
3237+
if items.len() > 0 {
3238+
items.push(v(if title-block != none { parts.inter-title-gap } else {
3239+
parts.tag-gap
3240+
}))
3241+
}
32123242
items.push(subtitle-block)
32133243
}
3214-
if title-block != none or subtitle-block != none {
3215-
items.push(v(parts.above-gap))
3216-
}
3244+
if items.len() > 0 { items.push(v(parts.above-gap)) }
32173245
items.push(canvas)
32183246
if caption-block != none {
32193247
items.push(v(parts.caption-gap))

tests/unit/test-labs-alt.typ

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// `_resolve-alt`: plot(alt:) wins, else labs(alt:) fills in.
2+
3+
#import "../../src/plot.typ": _resolve-alt
4+
#import "../../src/labs.typ": labs
5+
6+
// --- explicit plot alt wins over a labs alt -------------------------------
7+
8+
#assert.eq(_resolve-alt("explicit", labs(alt: "from labs")), "explicit")
9+
10+
// --- labs alt fills in when plot alt is unset -----------------------------
11+
12+
#assert.eq(_resolve-alt(none, labs(alt: "from labs")), "from labs")
13+
14+
// --- both unset resolves to none ------------------------------------------
15+
// labs(alt:) defaults to `auto`, which counts as unset.
16+
17+
#assert.eq(_resolve-alt(none, labs(title: "t")), none)
18+
#assert.eq(_resolve-alt(none, none), none)
19+
20+
Labs alt tests passed.
71.2 KB
Loading

0 commit comments

Comments
 (0)