Skip to content

Commit ca18264

Browse files
authored
Merge pull request #16 from humanpred/feature/gt-connector
Feature/gt connector
2 parents 19800cb + 1eeedc9 commit ca18264

File tree

6 files changed

+254
-8
lines changed

6 files changed

+254
-8
lines changed

R/gt.R

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,35 @@ gt_to_pagelist <- function(gt_obj, pg_width = 11, pg_height = 8.5,
336336
sub_gt[["_styles"]] <- sub_styles
337337
}
338338

339-
# Copy transforms and substitutions (declarative, no row re-indexing needed)
340-
sub_gt[["_transforms"]] <- gt_obj[["_transforms"]]
341-
sub_gt[["_substitutions"]] <- gt_obj[["_substitutions"]]
339+
# Re-index transforms (have $resolved$rows)
340+
orig_transforms <- gt_obj[["_transforms"]]
341+
if (length(orig_transforms) > 0L) {
342+
sub_gt[["_transforms"]] <- lapply(orig_transforms, function(tr) {
343+
old_rows <- tr$resolved$rows
344+
keep <- old_rows %in% row_indices
345+
if (!any(keep)) return(NULL)
346+
tr$resolved$rows <- as.integer(idx_map[as.character(old_rows[keep])])
347+
tr
348+
})
349+
sub_gt[["_transforms"]] <- Filter(Negate(is.null),
350+
sub_gt[["_transforms"]])
351+
}
352+
353+
sub_gt[["_locale"]] <- gt_obj[["_locale"]]
354+
355+
# Re-index substitutions (have $rows like formats)
356+
orig_subs <- gt_obj[["_substitutions"]]
357+
if (length(orig_subs) > 0L) {
358+
sub_gt[["_substitutions"]] <- lapply(orig_subs, function(s) {
359+
old_rows <- s$rows
360+
keep <- old_rows %in% row_indices
361+
if (!any(keep)) return(NULL) # nocov
362+
s$rows <- as.integer(idx_map[as.character(old_rows[keep])])
363+
s
364+
})
365+
sub_gt[["_substitutions"]] <- Filter(Negate(is.null),
366+
sub_gt[["_substitutions"]])
367+
}
342368

343369
# Copy summary definitions, filtering to groups present in subset
344370
orig_summary <- gt_obj[["_summary"]]
@@ -356,5 +382,10 @@ gt_to_pagelist <- function(gt_obj, pg_width = 11, pg_height = 8.5,
356382
sub_gt[["_summary"]] <- orig_summary
357383
}
358384

385+
# Copy summary column config (if any; empty in gt <= 1.2.0)
386+
if (length(gt_obj[["_summary_cols"]]) > 0L) {
387+
sub_gt[["_summary_cols"]] <- gt_obj[["_summary_cols"]] # nocov
388+
}
389+
359390
sub_gt
360391
}

design/ARCHITECTURE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ export_tfl(x = gt_tbl_obj, ...) [exported]
179179
└── greedy assignment:
180180
for each group:
181181
.rebuild_gt_subset(cleaned, rows) — gt.R
182+
re-indexes: _formats, _styles,
183+
_substitutions, _transforms
184+
copies: _boxhead, _options, _spanners,
185+
_stubhead, _locale, _summary_cols
186+
filters: _summary (by present groups)
182187
gt::as_gtable(sub_gt)
183188
.gt_grob_height(sub_grob, ...)
184189
→ list of row index vectors per page

design/DECISIONS.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -518,9 +518,18 @@ converts each independently via `gt_to_pagelist()`.
518518
content area, and greedily splits rows at group boundaries. Sub-gt objects
519519
are rebuilt with `.rebuild_gt_subset()` preserving column labels, options,
520520
`_formats` (re-indexed), and `_styles` (re-indexed).
521-
**Phase 3/4:** `.rebuild_gt_subset()` also copies `_transforms`,
522-
`_substitutions`, and `_summary` (filtered to groups present in subset).
523-
Spanners, `cols_merge()`, and `summary_rows()` all survive pagination.
521+
**Phase 3/4:** `.rebuild_gt_subset()` preserves all gt metadata through
522+
pagination. Row-indexed slots (`_formats`, `_styles`, `_substitutions`,
523+
`_transforms`) are re-indexed to the subset's row positions. Structural
524+
slots (`_boxhead`, `_options`, `_spanners`, `_stubhead`, `_locale`,
525+
`_summary_cols`) are copied as-is. `_summary` is filtered to groups
526+
present in the subset. Spanners, `cols_merge()`, `summary_rows()`,
527+
`sub_*()`, `text_transform()`, `tab_options()`, and locale all survive
528+
pagination.
529+
530+
Note: `_substitutions` and `_transforms` were initially copied without
531+
re-indexing but this caused row-count mismatches — both have `$rows` /
532+
`$resolved$rows` fields that reference original row indices.
524533

525534
---
526535

design/TESTING.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ One test file per source file — `tests/testthat/test-<name>.R` covers
2727
| `test-table_draw.R` | `build_table_grob()`, `drawDetails.tfl_table_grob()` (uncached fallback, wrap branch, rotated col_cont_msg labels, first_data fallback) |
2828
| `test-tfl_table.R` | `tfl_colspec()`, `tfl_table()`, column/row pagination, column width calculation, col_cont_msg flags, `tfl_table_to_pagelist()` |
2929
| `test-ggtibble.R` | `ggtibble_to_pagelist()`, `export_tfl.ggtibble()` — conversion, S3 dispatch, end-to-end (requires ggtibble, skipped if absent) |
30-
| `test-gt.R` | `.extract_gt_annotations()`, `.clean_gt()`, `gt_to_pagelist()`, `export_tfl.gt_tbl()`, `export_tfl.list()` with gt_tbl objects, S3 dispatch |
30+
| `test-gt.R` | `.extract_gt_annotations()`, `.clean_gt()`, `gt_to_pagelist()`, `.rebuild_gt_subset()` (row groups, formats, styles, substitutions, transforms, locale, stubhead, options, summary), `export_tfl.gt_tbl()`, `export_tfl.list()` with gt_tbl objects, S3 dispatch |
3131
| `test-integration.R` | Multi-file end-to-end smoke tests spanning the full pipeline |
3232

3333
---
@@ -274,6 +274,19 @@ test_that(".rebuild_gt_subset preserves row groups in subset", ...)
274274
test_that(".rebuild_gt_subset re-indexes formats", ...)
275275
test_that(".rebuild_gt_subset re-indexes styles", ...)
276276
test_that(".rebuild_gt_subset converts to valid grob", ...)
277+
test_that(".rebuild_gt_subset drops formats not in subset", ...)
278+
test_that(".rebuild_gt_subset preserves summary_rows for present groups", ...)
279+
test_that(".rebuild_gt_subset drops summary_rows for absent groups", ...)
280+
test_that(".rebuild_gt_subset copies grand summary for ungrouped tables", ...)
281+
test_that(".rebuild_gt_subset copies transforms and substitutions", ...)
282+
test_that(".rebuild_gt_subset preserves locale", ...)
283+
test_that(".rebuild_gt_subset preserves stubhead label", ...)
284+
test_that(".rebuild_gt_subset preserves sub_missing() substitutions", ...)
285+
test_that(".rebuild_gt_subset drops substitutions for excluded rows", ...)
286+
test_that(".rebuild_gt_subset preserves text_transform()", ...)
287+
test_that(".rebuild_gt_subset drops transforms targeting excluded rows", ...)
288+
test_that(".rebuild_gt_subset preserves tab_options()", ...)
289+
test_that(".rebuild_gt_subset copies _summary_cols when present", ...)
277290

278291
# .gt_content_height() / .gt_grob_height()
279292
test_that(".gt_content_height returns positive numeric", ...)

tests/testthat/test-gt.R

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,113 @@ test_that(".rebuild_gt_subset copies transforms and substitutions", {
338338
expect_true("_substitutions" %in% names(sub))
339339
})
340340

341+
test_that(".rebuild_gt_subset preserves locale", {
342+
tbl <- gt::gt(mtcars[1:6, 1:4], locale = "de")
343+
cleaned <- writetfl:::.clean_gt(tbl)
344+
sub <- writetfl:::.rebuild_gt_subset(cleaned, 1:3)
345+
346+
expect_equal(sub[["_locale"]], cleaned[["_locale"]])
347+
})
348+
349+
test_that(".rebuild_gt_subset preserves stubhead label", {
350+
tbl <- gt::gt(mtcars[1:6, 1:4], rownames_to_stub = TRUE) |>
351+
gt::tab_stubhead(label = "Car")
352+
cleaned <- writetfl:::.clean_gt(tbl)
353+
sub <- writetfl:::.rebuild_gt_subset(cleaned, 1:3)
354+
355+
expect_equal(sub[["_stubhead"]]$label, "Car")
356+
grob <- gt::as_gtable(sub)
357+
expect_true(inherits(grob, "grob"))
358+
})
359+
360+
test_that(".rebuild_gt_subset preserves sub_missing() substitutions", {
361+
df <- data.frame(a = c(1, NA, 3, NA, 5, 6), b = c(10, 20, 30, 40, 50, 60))
362+
tbl <- gt::gt(df) |>
363+
gt::sub_missing(columns = a, missing_text = "N/A")
364+
cleaned <- writetfl:::.clean_gt(tbl)
365+
sub <- writetfl:::.rebuild_gt_subset(cleaned, c(1, 2, 3))
366+
367+
expect_true(length(sub[["_substitutions"]]) > 0L)
368+
grob <- gt::as_gtable(sub)
369+
expect_true(inherits(grob, "grob"))
370+
})
371+
372+
test_that(".rebuild_gt_subset drops substitutions for excluded rows", {
373+
df <- data.frame(a = c(1, NA, 3, 4, 5, 6), b = 1:6)
374+
tbl <- gt::gt(df) |>
375+
gt::sub_missing(columns = a, missing_text = "N/A")
376+
cleaned <- writetfl:::.clean_gt(tbl)
377+
# Row 2 has NA; subset rows 3:6 excludes it
378+
sub <- writetfl:::.rebuild_gt_subset(cleaned, c(3, 4, 5, 6))
379+
# Substitution should still exist (applied to all rows) but re-indexed
380+
grob <- gt::as_gtable(sub)
381+
expect_true(inherits(grob, "grob"))
382+
})
383+
384+
test_that(".rebuild_gt_subset preserves text_transform()", {
385+
tbl <- gt::gt(mtcars[1:6, 1:4]) |>
386+
gt::text_transform(
387+
locations = gt::cells_body(columns = mpg),
388+
fn = function(x) paste0(x, " mpg")
389+
)
390+
cleaned <- writetfl:::.clean_gt(tbl)
391+
sub <- writetfl:::.rebuild_gt_subset(cleaned, 1:3)
392+
393+
expect_true(length(sub[["_transforms"]]) > 0L)
394+
grob <- gt::as_gtable(sub)
395+
expect_true(inherits(grob, "grob"))
396+
})
397+
398+
test_that(".rebuild_gt_subset drops transforms targeting excluded rows", {
399+
tbl <- gt::gt(mtcars[1:6, 1:4]) |>
400+
gt::text_transform(
401+
locations = gt::cells_body(columns = mpg, rows = 1:2),
402+
fn = function(x) paste0(x, " mpg")
403+
)
404+
cleaned <- writetfl:::.clean_gt(tbl)
405+
# Subset rows 4:6 — transform targets rows 1:2 only, should be dropped
406+
sub <- writetfl:::.rebuild_gt_subset(cleaned, 4:6)
407+
expect_length(sub[["_transforms"]], 0L)
408+
})
409+
410+
test_that(".rebuild_gt_subset preserves tab_options()", {
411+
tbl <- gt::gt(mtcars[1:6, 1:4]) |>
412+
gt::tab_options(
413+
table.font.size = gt::px(10),
414+
row.striping.include_table_body = TRUE
415+
)
416+
cleaned <- writetfl:::.clean_gt(tbl)
417+
sub <- writetfl:::.rebuild_gt_subset(cleaned, 1:3)
418+
419+
expect_equal(sub[["_options"]], cleaned[["_options"]])
420+
grob <- gt::as_gtable(sub)
421+
expect_true(inherits(grob, "grob"))
422+
})
423+
424+
test_that(".rebuild_gt_subset copies _summary_cols when present", {
425+
df <- data.frame(
426+
group = rep(c("A", "B"), each = 3),
427+
val = c(10, 20, 30, 40, 50, 60)
428+
)
429+
tbl <- gt::gt(df, groupname_col = "group") |>
430+
gt::summary_rows(
431+
groups = gt::everything(),
432+
columns = val,
433+
fns = list(Total = ~ sum(.))
434+
)
435+
cleaned <- writetfl:::.clean_gt(tbl)
436+
sub <- writetfl:::.rebuild_gt_subset(cleaned, 1:3)
437+
438+
# _summary_cols may or may not be populated depending on gt version,
439+
440+
# but the slot should be carried through if present
441+
if (length(cleaned[["_summary_cols"]]) > 0L) {
442+
expect_equal(sub[["_summary_cols"]], cleaned[["_summary_cols"]])
443+
} else {
444+
expect_true(TRUE) # slot empty in this gt version
445+
}
446+
})
447+
341448
# .gt_content_height() / .gt_grob_height() --------------------------------
342449

343450
test_that(".gt_content_height returns positive numeric", {

vignettes/v05-gt_tables.Rmd

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,13 @@ The following gt features are preserved through pagination:
200200
| `fmt_*()` functions | Yes | Re-indexed per page subset |
201201
| `tab_style()` | Yes | Re-indexed per page subset |
202202
| `cols_merge()` | Yes | Carried through boxhead |
203-
| `summary_rows()` | Yes | Filtered to groups present on each page |
204203
| `cols_label()` | Yes | Carried through boxhead |
204+
| `summary_rows()` | Yes | Filtered to groups present on each page |
205+
| `sub_*()` functions | Yes | Re-indexed per page subset |
206+
| `text_transform()` | Yes | Re-indexed per page subset |
207+
| `tab_options()` | Yes | Copied to every page |
208+
| `tab_stubhead()` | Yes | Copied to every page |
209+
| `gt(locale = ...)` | Yes | Locale preserved through pagination |
205210

206211
```{r spanner-example, eval = FALSE}
207212
tbl <- gt(mtcars[, 1:6]) |>
@@ -275,3 +280,79 @@ tbl <- gt(df, groupname_col = "group") |>
275280
276281
export_tfl(tbl, preview = TRUE)
277282
```
283+
284+
### Missing value substitutions
285+
286+
`sub_missing()` and other `sub_*()` functions replace cell values with
287+
display text. These are re-indexed per page subset during pagination.
288+
289+
```{r sub-missing, fig.width = 11, fig.height = 8.5, out.width = "100%"}
290+
df <- data.frame(
291+
name = c("Alice", "Bob", "Carol", "Dave"),
292+
score = c(95, NA, 87, NA)
293+
)
294+
295+
tbl <- gt(df) |>
296+
tab_header(title = "Scores with Missing Values") |>
297+
sub_missing(columns = score, missing_text = "N/A")
298+
299+
export_tfl(tbl, preview = TRUE)
300+
```
301+
302+
### Text transforms
303+
304+
`text_transform()` applies arbitrary functions to cell text. Transforms
305+
are re-indexed so only rows present on each page are processed.
306+
307+
```{r text-transform, fig.width = 11, fig.height = 8.5, out.width = "100%"}
308+
tbl <- gt(head(mtcars[, 1:4], 6)) |>
309+
tab_header(title = "Transformed Text") |>
310+
text_transform(
311+
locations = cells_body(columns = mpg),
312+
fn = function(x) paste0(x, " mpg")
313+
)
314+
315+
export_tfl(tbl, preview = TRUE)
316+
```
317+
318+
### Table options
319+
320+
`tab_options()` settings (font size, row striping, etc.) are preserved
321+
on every paginated page.
322+
323+
```{r tab-options, fig.width = 11, fig.height = 8.5, out.width = "100%"}
324+
tbl <- gt(head(mtcars[, 1:4], 8)) |>
325+
tab_header(title = "Custom Table Options") |>
326+
tab_options(
327+
table.font.size = px(10),
328+
row.striping.include_table_body = TRUE
329+
)
330+
331+
export_tfl(tbl, preview = TRUE)
332+
```
333+
334+
### Stub head labels
335+
336+
When row names are used as a stub column, `tab_stubhead()` sets a label for
337+
that column. The label is preserved on every paginated page.
338+
339+
```{r stubhead, fig.width = 11, fig.height = 8.5, out.width = "100%"}
340+
tbl <- gt(head(mtcars[, 1:4], 8), rownames_to_stub = TRUE) |>
341+
tab_header(title = "Stubhead Label Example") |>
342+
tab_stubhead(label = "Car")
343+
344+
export_tfl(tbl, preview = TRUE)
345+
```
346+
347+
### Locale support
348+
349+
When a locale is set via `gt(locale = ...)`, it is preserved through
350+
pagination so that number formatting respects locale conventions.
351+
352+
```{r locale, eval = FALSE}
353+
tbl <- gt(head(mtcars[, 1:4], 8), locale = "de") |>
354+
tab_header(title = "German Locale Formatting") |>
355+
fmt_number(columns = mpg, decimals = 1)
356+
357+
export_tfl(tbl, file = "locale.pdf")
358+
```

0 commit comments

Comments
 (0)