Skip to content

Commit 50c09bc

Browse files
yihuiclaude
andcommitted
Reorder spanner columns to body order before rendering
lt matches a spanner to the visual position of its first column, then spans the next columns.length cells. When a spanner's columns were listed in an order other than the final left-to-right body order (e.g. after lt_move(), or when selected by a predicate formula that returns data-frame order), the mismatched first column shifted the colspan and could silently drop later spanners. Reorder each explicit spanner's columns to match `visible` (the post-move body order) in resolveSpec() before the header row is built. This makes spanners order-insensitive and lets predicate selectors like `columns = ~ endsWith(., "_time")` be used for spanners safely. Columns must still be contiguous in the body. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d9f4f38 commit 50c09bc

3 files changed

Lines changed: 31 additions & 2 deletions

File tree

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- `lt_label()` now also accepts a single named list or named character vector mapping column names to labels (e.g., `lt_label(x, c(mpg = "Miles/Gallon", cyl = "Cylinders"))`), which is convenient when labels are computed programmatically. Named arguments (e.g., `lt_label(x, mpg = "Miles/Gallon")`) continue to work.
66

7+
- `lt_spanner()` no longer requires its `columns` to be listed in the table's visual (left-to-right) order. The columns of an explicit spanner are now reordered to match the final body order (after any `lt_move()`) before rendering, so a spanner is drawn correctly regardless of the order in which its columns were listed. This also makes predicate selectors (e.g., `columns = ~ endsWith(., "_time")`) safe to use for spanners. The columns must still be contiguous in the table body.
8+
79
# CHANGES IN lt VERSION 0.4
810

911
- `lt_format()` gained a `sig_digits` argument to format numbers to a fixed number of significant digits (e.g., `lt_format(x, ~col, sig_digits = 3)`), which is useful for columns spanning several orders of magnitude. It is mutually exclusive with `decimals`.

inst/www/lt.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,16 @@
352352
// Styles: the style ops are consumed directly (css/class/columns/rows/test).
353353
const styles = ops.filter(op => op.type === "style");
354354

355-
// Auto-spanners: split column names on separator, group contiguous prefixes
356-
let spanners = spec.spanners || [];
355+
// Explicit spanners: reorder each spanner's columns to match the final
356+
// (post-move) body order, so a spanner is recorded/rendered correctly
357+
// regardless of the order in which its columns were listed. A spanner is
358+
// matched to the visual position of its first column and then spans the
359+
// next columns.length cells, so a first column that is not the leftmost in
360+
// `visible` would otherwise misalign the header row.
361+
let spanners = (spec.spanners || []).map(sp => {
362+
const cols = sp.columns.slice().sort((a, b) => visible.indexOf(a) - visible.indexOf(b));
363+
return { ...sp, columns: cols };
364+
});
357365
if (spec.auto_span) {
358366
const sep = typeof spec.auto_span === "string" ? new RegExp(spec.auto_span) : /[._]/;
359367
spanners = [...spanners];

tests/test-ci/test-js.R

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,25 @@ assert("column spanner renders colspan", {
202202
(matches(html, '.*colspan="2".*>AB</th>.*') %==% "")
203203
})
204204

205+
assert("spanner columns are reordered to body order before rendering", {
206+
# columns listed out of order (b before a) still render as one spanner
207+
html = build(list(
208+
data = list(a = 1, b = 2, c = 3),
209+
spanners = list(list(label = "AB", columns = list("b", "a")))
210+
))
211+
(matches(html, '.*colspan="2".*>AB</th>.*') %==% "")
212+
213+
# after a move, a spanner whose columns are listed in the original (pre-move)
214+
# order must anchor to the moved first column, not be dropped
215+
html = build(list(
216+
data = list(x = 1, a = 2, b = 3),
217+
ops = list(list(type = "move", columns = list("b", "a"), after = "x")),
218+
# body order becomes x, b, a; list spanner cols in the pre-move order
219+
spanners = list(list(label = "AB", columns = list("a", "b")))
220+
))
221+
(matches(html, '.*colspan="2".*>AB</th>.*') %==% "")
222+
})
223+
205224
assert("footnotes render in tfoot", {
206225
html = build(list(
207226
data = list(x = 1),

0 commit comments

Comments
 (0)