Word wrapping#34
Merged
Merged
Conversation
Wraps long cell text and column-header labels so a column can be narrower, addressing the silent off-page overflow reported in the issue. All wrapping logic lives in a new R/wrap.R module with one disable lever (wrap_cols = FALSE). Behavioral changes: - tfl_table(wrap_cols) defaults to "auto" - a non-group column is wrap-eligible when its data or header contains a configured break character; numeric and single-token columns are skipped. TRUE / FALSE / a character vector continue to work; tfl_colspec(wrap = NA) is the new "inherit" sentinel. - New tfl_table(wrap_breaks = wrap_breaks(...)) configures break characters. The default drops whitespace at the break; opt in to keep_before chars (e.g. "-") that stay on the left of the break. - Column headers auto-wrap on the same eligibility as cells. - A row whose wrapped height exceeds one page now errors via the same overflow_action = "error" / "warn" switch added for issue 30. Algorithm: water-from-top. Each iteration finds the widest wrap-eligible columns above their floor and shrinks them together until they meet the next-widest competitor or hit a floor. Floor is the larger of min_col_width and the rendered width of the column's longest unbreakable token, so the algorithm cannot promise a width the renderer can't honour. Tests: 47 unit tests in tests/testthat/test-wrap.R plus 8 end-to-end tests in tests/testthat/test-tfl_table.R. Full suite passes. Demos: examples/wrap_demos.R generates one PDF per behavior plus a README to a persistent temp directory for hands-on review. examples/ is added to .Rbuildignore so it does not ship with the built package. Documentation: new "Word wrapping" section in v03-tfl_table_styling.Rmd spelling out the distinction between text-wrap (wrap_cols) and page-column-split (allow_col_split). Design rationale in design/DESIGN.md and decision entry D-41 in design/DECISIONS.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verified via tfl_table_to_pagelist() that 04 (wrap_cols = c("alpha")) and
05 (per-colspec wrap = FALSE on bravo) both produce single-page output
with the demo's wide_df: alpha can shrink enough to absorb the entire
page-width deficit before hitting its longest-token floor, so no
page-column-split is triggered.
The previous blurbs claimed each demo "forces a page split", which would
have appeared in the rendered PDFs as the rotated "Columns continue ..."
side annotations. The actual demonstration is more subtle: only one
column wraps, so it wraps more aggressively than in 02 where both
string columns shared the burden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The column-width pipeline measured every string in a column with the cell gpar (regular weight), but headers actually render with the header_row gpar (bold by default). A bold header is a few percent wider than the same string in regular weight, so a column auto-sized against the regular-weight measurement undersized the bold header by enough to bleed into the neighboring column at draw time - wiping out the next column's header (visible in demo 08 where the second column header was invisible). Fix - three places: 1. compute_col_widths() Pass 1 (auto-sizing): split each column's strings into header lines (measured with header_row gpar) and data values (measured with cell gpar) via the new .split_col_strings() helper. Take the larger of the two as the natural width. 2. .compute_wrapped_widths() floor calculation: the longest-unbreakable- token measurement was likewise running everything through the cell gpar. Now header tokens are measured with header_row gpar. This prevents the wrap algorithm from promising a width the renderer cannot honour. 3. .draw_cell_text() clip-viewport widening: stringWidth() picks up the active viewport's gpar, not the explicit `gp` arg, so it was measuring "Concomitant" in regular weight and then widening the clip for the bold rendering - bleeding past the column edge. Replaced with grobWidth(textGrob(text, gp = gp)) which honours the actual rendering gpar. Also capped the clip widening to a 0.05 in font-metric tolerance: a column genuinely too narrow for its content now clips at the column edge instead of overlapping the neighbor. Tests: two new regression tests in test-tfl_table.R verify that an auto-sized column accommodates a bold header, and that the wrap floor reflects the bold header token width. Demo 08 now uses a 1.3-inch column (wider than longest bold token, narrower than the full header) so the header wraps cleanly to three lines without overlapping the next column. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When two consecutive rows both contain wrapped (multi-line) cells, the bottom of one row's wrapped text can sit visually flush against the top of the next row's wrapped text - making it ambiguous where one row ends and the next begins (visible in demo 07 with the path-like column). New tfl_table() argument `wrap_extra_padding`, default unit(0.25, "lines"), adds that much vertical space at the bottom of any cell whose displayed text spans more than one line. Single-line cells are unaffected so compact tables are not inflated. Set to unit(0, "lines") to disable. The trigger is "the displayed text contains a newline after wrapping", so cells that became multi-line via the wrap algorithm AND cells with explicit \n in their content both receive the extra. Headers receive the same treatment so a wrapped header is also visually separated from the first data row. Threaded through table_pagelist.R: measure_row_heights_tbl() and .measure_header_row_height() both gain a wrap_extra_pad_in numeric arg. The drawDetails fallback path in table_draw.R applies the same logic when it builds its own per-page row-height matrix. Tests in test-tfl_table.R: wrap_extra_padding > 0 makes a multi-line cell taller than the same cell with the option = 0; single-line cells have identical height regardless. The arg is validated as a length-1 unit object. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default water-fill (wrap_balance = "width") balances column widths but is
content-blind: when one wrap-eligible column has dense content and another
has sparse content, the dense column wraps to many more lines than its
neighbour even though they end up at the same width. The new opt-in
height-balance pass shifts width away from columns that have slack
(short content with room to give up width down to their floor) and into
columns whose cells are the row-height bottleneck. It accepts a move
only when the resulting *total table height* (sum of per-row heights,
plus header) is smaller, so opting in cannot produce a worse table.
Algorithm: bounded greedy local search starting from the water-fill
widths. Each iteration finds the row with the maximum cell height,
identifies the wrap-eligible column whose cell drives that row's max
(bottleneck) and the wrap-eligible column with the shortest cell in
that row (slack), and tries deltas of {0.5, 0.25, 0.1, 0.05} inches
from slack to bottleneck. Capped at 20 iterations and 1 second
wall-time. Cell heights are cached per (column, width) and per
unique cell string so re-measurements within the loop are free in
the common case. Total widths are preserved exactly; column floors
(longest unbreakable token + padding) are honoured; columns are
never grown past their natural content width.
Any error or invariant violation falls back silently to the input
widths via tryCatch + a defensive sanity check on the result, so the
opt-in is never worse than the default.
Concrete win on the demo asym_df (24-token vs 5-token columns, 7
rows): width-balance produces a 2-page output with row height 1.09 in;
height-balance produces a 1-page output with row height 0.88 in - a
~19% reduction in total table height in ~0.3 s.
Tests: end-to-end test in test-tfl_table.R that height-balance
strictly reduces total table height vs width-balance on asymmetric
content; a no-op test confirming a single-eligible-column table has
identical widths under both modes; a validator test for invalid
wrap_balance values.
Demos: two new PDFs (14_balance_width.pdf and 14_balance_height.pdf)
side-by-side on the same input to show the difference.
Vignette: new "Optimising for height" subsection in
v03-tfl_table_styling.Rmd describing when to opt in and the safety
properties (no-worse-than-default, time-budgeted, automatic
fallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 0.25 line default ended up too small to be visually perceptible against the natural inter-line gap from `lineheight = 1.05` (the lineheight gap is already ~0.05 lines, so 0.25 lines extra was only a ~5x increase, not visually obvious in a typical 12 pt PDF). Bumping to 0.5 lines makes the inter-row gap clearly distinguishable from the inter-line gap inside a cell without making compact tables noticeably loose. Disable with `wrap_extra_padding = unit(0, "lines")`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflects what the wrap module ships with as of issue 28: - README.md: bullet list under "Paginated data-frame tables" updated to document `wrap_cols = "auto"` as the default, the `wrap_breaks()` break-character spec with `keep_before`, the `wrap_balance = "height"` opt-in, the row-overflow guard via `overflow_action`, and the new `wrap_extra_padding` default. - vignettes/writetfl.Rmd: matching one-paragraph mention in the data- frame-tables overview. - vignettes/v02-tfl_table_intro.Rmd: rewrote the Word-wrapping section with a value table for `wrap_cols`, sub-sections for per-column override, custom break characters via `wrap_breaks()`, the `wrap_balance = "height"` opt-in, and the row-overflow guard. Added `wrap_breaks`, `wrap_balance`, and `wrap_extra_padding` to the summary table at the bottom of the vignette. - vignettes/v04-troubleshooting.Rmd: added a "Row wrapped to taller than one page" section explaining the row-overflow guard and how `overflow_action = "warn"` helps diagnose it. Updated the "Content too wide" solutions list to mention auto-detect, the `wrap_breaks(keep_before = …)` escape hatch for unbreakable tokens, and `wrap_balance = "height"` for uneven content density. No code changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.