Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions R/gt.R
Original file line number Diff line number Diff line change
Expand Up @@ -336,9 +336,35 @@ gt_to_pagelist <- function(gt_obj, pg_width = 11, pg_height = 8.5,
sub_gt[["_styles"]] <- sub_styles
}

# Copy transforms and substitutions (declarative, no row re-indexing needed)
sub_gt[["_transforms"]] <- gt_obj[["_transforms"]]
sub_gt[["_substitutions"]] <- gt_obj[["_substitutions"]]
# Re-index transforms (have $resolved$rows)
orig_transforms <- gt_obj[["_transforms"]]
if (length(orig_transforms) > 0L) {
sub_gt[["_transforms"]] <- lapply(orig_transforms, function(tr) {
old_rows <- tr$resolved$rows
keep <- old_rows %in% row_indices
if (!any(keep)) return(NULL)
tr$resolved$rows <- as.integer(idx_map[as.character(old_rows[keep])])
tr
})
sub_gt[["_transforms"]] <- Filter(Negate(is.null),
sub_gt[["_transforms"]])
}

sub_gt[["_locale"]] <- gt_obj[["_locale"]]

# Re-index substitutions (have $rows like formats)
orig_subs <- gt_obj[["_substitutions"]]
if (length(orig_subs) > 0L) {
sub_gt[["_substitutions"]] <- lapply(orig_subs, function(s) {
old_rows <- s$rows
keep <- old_rows %in% row_indices
if (!any(keep)) return(NULL) # nocov
s$rows <- as.integer(idx_map[as.character(old_rows[keep])])
s
})
sub_gt[["_substitutions"]] <- Filter(Negate(is.null),
sub_gt[["_substitutions"]])
}

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

# Copy summary column config (if any; empty in gt <= 1.2.0)
if (length(gt_obj[["_summary_cols"]]) > 0L) {
sub_gt[["_summary_cols"]] <- gt_obj[["_summary_cols"]] # nocov
}

sub_gt
}
5 changes: 5 additions & 0 deletions design/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ export_tfl(x = gt_tbl_obj, ...) [exported]
└── greedy assignment:
for each group:
.rebuild_gt_subset(cleaned, rows) — gt.R
re-indexes: _formats, _styles,
_substitutions, _transforms
copies: _boxhead, _options, _spanners,
_stubhead, _locale, _summary_cols
filters: _summary (by present groups)
gt::as_gtable(sub_gt)
.gt_grob_height(sub_grob, ...)
→ list of row index vectors per page
Expand Down
15 changes: 12 additions & 3 deletions design/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,9 +518,18 @@ converts each independently via `gt_to_pagelist()`.
content area, and greedily splits rows at group boundaries. Sub-gt objects
are rebuilt with `.rebuild_gt_subset()` preserving column labels, options,
`_formats` (re-indexed), and `_styles` (re-indexed).
**Phase 3/4:** `.rebuild_gt_subset()` also copies `_transforms`,
`_substitutions`, and `_summary` (filtered to groups present in subset).
Spanners, `cols_merge()`, and `summary_rows()` all survive pagination.
**Phase 3/4:** `.rebuild_gt_subset()` preserves all gt metadata through
pagination. Row-indexed slots (`_formats`, `_styles`, `_substitutions`,
`_transforms`) are re-indexed to the subset's row positions. Structural
slots (`_boxhead`, `_options`, `_spanners`, `_stubhead`, `_locale`,
`_summary_cols`) are copied as-is. `_summary` is filtered to groups
present in the subset. Spanners, `cols_merge()`, `summary_rows()`,
`sub_*()`, `text_transform()`, `tab_options()`, and locale all survive
pagination.

Note: `_substitutions` and `_transforms` were initially copied without
re-indexing but this caused row-count mismatches — both have `$rows` /
`$resolved$rows` fields that reference original row indices.

---

Expand Down
15 changes: 14 additions & 1 deletion design/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ One test file per source file — `tests/testthat/test-<name>.R` covers
| `test-table_draw.R` | `build_table_grob()`, `drawDetails.tfl_table_grob()` (uncached fallback, wrap branch, rotated col_cont_msg labels, first_data fallback) |
| `test-tfl_table.R` | `tfl_colspec()`, `tfl_table()`, column/row pagination, column width calculation, col_cont_msg flags, `tfl_table_to_pagelist()` |
| `test-ggtibble.R` | `ggtibble_to_pagelist()`, `export_tfl.ggtibble()` — conversion, S3 dispatch, end-to-end (requires ggtibble, skipped if absent) |
| `test-gt.R` | `.extract_gt_annotations()`, `.clean_gt()`, `gt_to_pagelist()`, `export_tfl.gt_tbl()`, `export_tfl.list()` with gt_tbl objects, S3 dispatch |
| `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 |
| `test-integration.R` | Multi-file end-to-end smoke tests spanning the full pipeline |

---
Expand Down Expand Up @@ -274,6 +274,19 @@ test_that(".rebuild_gt_subset preserves row groups in subset", ...)
test_that(".rebuild_gt_subset re-indexes formats", ...)
test_that(".rebuild_gt_subset re-indexes styles", ...)
test_that(".rebuild_gt_subset converts to valid grob", ...)
test_that(".rebuild_gt_subset drops formats not in subset", ...)
test_that(".rebuild_gt_subset preserves summary_rows for present groups", ...)
test_that(".rebuild_gt_subset drops summary_rows for absent groups", ...)
test_that(".rebuild_gt_subset copies grand summary for ungrouped tables", ...)
test_that(".rebuild_gt_subset copies transforms and substitutions", ...)
test_that(".rebuild_gt_subset preserves locale", ...)
test_that(".rebuild_gt_subset preserves stubhead label", ...)
test_that(".rebuild_gt_subset preserves sub_missing() substitutions", ...)
test_that(".rebuild_gt_subset drops substitutions for excluded rows", ...)
test_that(".rebuild_gt_subset preserves text_transform()", ...)
test_that(".rebuild_gt_subset drops transforms targeting excluded rows", ...)
test_that(".rebuild_gt_subset preserves tab_options()", ...)
test_that(".rebuild_gt_subset copies _summary_cols when present", ...)

# .gt_content_height() / .gt_grob_height()
test_that(".gt_content_height returns positive numeric", ...)
Expand Down
107 changes: 107 additions & 0 deletions tests/testthat/test-gt.R
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,113 @@ test_that(".rebuild_gt_subset copies transforms and substitutions", {
expect_true("_substitutions" %in% names(sub))
})

test_that(".rebuild_gt_subset preserves locale", {
tbl <- gt::gt(mtcars[1:6, 1:4], locale = "de")
cleaned <- writetfl:::.clean_gt(tbl)
sub <- writetfl:::.rebuild_gt_subset(cleaned, 1:3)

expect_equal(sub[["_locale"]], cleaned[["_locale"]])
})

test_that(".rebuild_gt_subset preserves stubhead label", {
tbl <- gt::gt(mtcars[1:6, 1:4], rownames_to_stub = TRUE) |>
gt::tab_stubhead(label = "Car")
cleaned <- writetfl:::.clean_gt(tbl)
sub <- writetfl:::.rebuild_gt_subset(cleaned, 1:3)

expect_equal(sub[["_stubhead"]]$label, "Car")
grob <- gt::as_gtable(sub)
expect_true(inherits(grob, "grob"))
})

test_that(".rebuild_gt_subset preserves sub_missing() substitutions", {
df <- data.frame(a = c(1, NA, 3, NA, 5, 6), b = c(10, 20, 30, 40, 50, 60))
tbl <- gt::gt(df) |>
gt::sub_missing(columns = a, missing_text = "N/A")
cleaned <- writetfl:::.clean_gt(tbl)
sub <- writetfl:::.rebuild_gt_subset(cleaned, c(1, 2, 3))

expect_true(length(sub[["_substitutions"]]) > 0L)
grob <- gt::as_gtable(sub)
expect_true(inherits(grob, "grob"))
})

test_that(".rebuild_gt_subset drops substitutions for excluded rows", {
df <- data.frame(a = c(1, NA, 3, 4, 5, 6), b = 1:6)
tbl <- gt::gt(df) |>
gt::sub_missing(columns = a, missing_text = "N/A")
cleaned <- writetfl:::.clean_gt(tbl)
# Row 2 has NA; subset rows 3:6 excludes it
sub <- writetfl:::.rebuild_gt_subset(cleaned, c(3, 4, 5, 6))
# Substitution should still exist (applied to all rows) but re-indexed
grob <- gt::as_gtable(sub)
expect_true(inherits(grob, "grob"))
})

test_that(".rebuild_gt_subset preserves text_transform()", {
tbl <- gt::gt(mtcars[1:6, 1:4]) |>
gt::text_transform(
locations = gt::cells_body(columns = mpg),
fn = function(x) paste0(x, " mpg")
)
cleaned <- writetfl:::.clean_gt(tbl)
sub <- writetfl:::.rebuild_gt_subset(cleaned, 1:3)

expect_true(length(sub[["_transforms"]]) > 0L)
grob <- gt::as_gtable(sub)
expect_true(inherits(grob, "grob"))
})

test_that(".rebuild_gt_subset drops transforms targeting excluded rows", {
tbl <- gt::gt(mtcars[1:6, 1:4]) |>
gt::text_transform(
locations = gt::cells_body(columns = mpg, rows = 1:2),
fn = function(x) paste0(x, " mpg")
)
cleaned <- writetfl:::.clean_gt(tbl)
# Subset rows 4:6 — transform targets rows 1:2 only, should be dropped
sub <- writetfl:::.rebuild_gt_subset(cleaned, 4:6)
expect_length(sub[["_transforms"]], 0L)
})

test_that(".rebuild_gt_subset preserves tab_options()", {
tbl <- gt::gt(mtcars[1:6, 1:4]) |>
gt::tab_options(
table.font.size = gt::px(10),
row.striping.include_table_body = TRUE
)
cleaned <- writetfl:::.clean_gt(tbl)
sub <- writetfl:::.rebuild_gt_subset(cleaned, 1:3)

expect_equal(sub[["_options"]], cleaned[["_options"]])
grob <- gt::as_gtable(sub)
expect_true(inherits(grob, "grob"))
})

test_that(".rebuild_gt_subset copies _summary_cols when present", {
df <- data.frame(
group = rep(c("A", "B"), each = 3),
val = c(10, 20, 30, 40, 50, 60)
)
tbl <- gt::gt(df, groupname_col = "group") |>
gt::summary_rows(
groups = gt::everything(),
columns = val,
fns = list(Total = ~ sum(.))
)
cleaned <- writetfl:::.clean_gt(tbl)
sub <- writetfl:::.rebuild_gt_subset(cleaned, 1:3)

# _summary_cols may or may not be populated depending on gt version,

# but the slot should be carried through if present
if (length(cleaned[["_summary_cols"]]) > 0L) {
expect_equal(sub[["_summary_cols"]], cleaned[["_summary_cols"]])
} else {
expect_true(TRUE) # slot empty in this gt version
}
})

# .gt_content_height() / .gt_grob_height() --------------------------------

test_that(".gt_content_height returns positive numeric", {
Expand Down
83 changes: 82 additions & 1 deletion vignettes/v05-gt_tables.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,13 @@ The following gt features are preserved through pagination:
| `fmt_*()` functions | Yes | Re-indexed per page subset |
| `tab_style()` | Yes | Re-indexed per page subset |
| `cols_merge()` | Yes | Carried through boxhead |
| `summary_rows()` | Yes | Filtered to groups present on each page |
| `cols_label()` | Yes | Carried through boxhead |
| `summary_rows()` | Yes | Filtered to groups present on each page |
| `sub_*()` functions | Yes | Re-indexed per page subset |
| `text_transform()` | Yes | Re-indexed per page subset |
| `tab_options()` | Yes | Copied to every page |
| `tab_stubhead()` | Yes | Copied to every page |
| `gt(locale = ...)` | Yes | Locale preserved through pagination |

```{r spanner-example, eval = FALSE}
tbl <- gt(mtcars[, 1:6]) |>
Expand Down Expand Up @@ -275,3 +280,79 @@ tbl <- gt(df, groupname_col = "group") |>

export_tfl(tbl, preview = TRUE)
```

### Missing value substitutions

`sub_missing()` and other `sub_*()` functions replace cell values with
display text. These are re-indexed per page subset during pagination.

```{r sub-missing, fig.width = 11, fig.height = 8.5, out.width = "100%"}
df <- data.frame(
name = c("Alice", "Bob", "Carol", "Dave"),
score = c(95, NA, 87, NA)
)

tbl <- gt(df) |>
tab_header(title = "Scores with Missing Values") |>
sub_missing(columns = score, missing_text = "N/A")

export_tfl(tbl, preview = TRUE)
```

### Text transforms

`text_transform()` applies arbitrary functions to cell text. Transforms
are re-indexed so only rows present on each page are processed.

```{r text-transform, fig.width = 11, fig.height = 8.5, out.width = "100%"}
tbl <- gt(head(mtcars[, 1:4], 6)) |>
tab_header(title = "Transformed Text") |>
text_transform(
locations = cells_body(columns = mpg),
fn = function(x) paste0(x, " mpg")
)

export_tfl(tbl, preview = TRUE)
```

### Table options

`tab_options()` settings (font size, row striping, etc.) are preserved
on every paginated page.

```{r tab-options, fig.width = 11, fig.height = 8.5, out.width = "100%"}
tbl <- gt(head(mtcars[, 1:4], 8)) |>
tab_header(title = "Custom Table Options") |>
tab_options(
table.font.size = px(10),
row.striping.include_table_body = TRUE
)

export_tfl(tbl, preview = TRUE)
```

### Stub head labels

When row names are used as a stub column, `tab_stubhead()` sets a label for
that column. The label is preserved on every paginated page.

```{r stubhead, fig.width = 11, fig.height = 8.5, out.width = "100%"}
tbl <- gt(head(mtcars[, 1:4], 8), rownames_to_stub = TRUE) |>
tab_header(title = "Stubhead Label Example") |>
tab_stubhead(label = "Car")

export_tfl(tbl, preview = TRUE)
```

### Locale support

When a locale is set via `gt(locale = ...)`, it is preserved through
pagination so that number formatting respects locale conventions.

```{r locale, eval = FALSE}
tbl <- gt(head(mtcars[, 1:4], 8), locale = "de") |>
tab_header(title = "German Locale Formatting") |>
fmt_number(columns = mpg, decimals = 1)

export_tfl(tbl, file = "locale.pdf")
```
Loading