This guide covers column width management in ksTFL: the LOCKED/UNLOCKED/VISIBLE model, automatic recalculation, invisible-column rules, and the tradeoffs involved in making tables fit cleanly on the page.
ksTFL partitions table columns into four categories that determine how widths are calculated:
isVisible != FALSE)
colWidth)
colWidth set)
isVisible = FALSE)
compute_cols())When you create a table, ksTFL automatically:
%d / %.df
format), the longest line of the column name, and of the label —
whichever is longest. Frames over 10,000 rows are deterministically
sampled for the measurement.library(ksTFL)
# Create sample data
data <- data.frame(
id = 1:100,
patient_id = sprintf("PAT-%04d", 1:100),
age = round(rnorm(100, 45, 10)),
weight_kg = round(rnorm(100, 70, 15), 1),
treatment_group = sample(c("Placebo", "Treatment A", "Treatment B"), 100, replace = TRUE)
)
# Initial spec with auto-detected widths
spec <- create_table(data)
print(spec) # Shows auto-calculated widths for all columnsautoColWidth OptionThe autoColWidth option (default: TRUE)
controls whether widths are automatically recalculated when you lock a
column or change a column’s visibility:
When you set colWidth for a column, it becomes
LOCKED:
You can lock multiple columns; unlocked columns fill the remaining space:
spec <- create_table(data) |>
define_cols(id, colWidth = "10%") |> # Lock at 10%
define_cols(patient_id, colWidth = "20%") |> # Lock at 20%
define_cols(treatment_group, colWidth = "25%") # Lock at 25%
# Result:
# - id: 10% (LOCKED)
# - patient_id: 20% (LOCKED)
# - treatment_group: 25% (LOCKED)
# - age, weight_kg: share remaining 45% proportionallyFlag columns (isPaging, isGrouping,
isColBreak) recalculate like any other unlocked column —
lock their widths if size matters. In column-break segments, ID columns
keep their width while the remaining segment columns are rescaled to
fill the segment.
You can mix percentage widths with absolute units:
spec <- create_table(data) |>
define_cols(id, colWidth = "2.5cm") |> # Fixed width, kept exactly
define_cols(patient_id, colWidth = "20%") # 20% of the space left over
# Result:
# - id: 2.5cm (LOCKED, absolute)
# - patient_id: 20% (LOCKED, relative)
# - Other columns: share the remaining percentage pool proportionallyImportant: Fixed-unit columns (cm, in) keep their exact width at render. They do not shrink the percentage pool that the R-layer recalculation works with, but the renderer resolves every percentage column against the space left after the fixed columns are placed — a percentage width is a share of the remaining area, not of the full page.
When autoColWidth = TRUE and you lock a column,
ksTFL:
With the sample data above (100 rows), create_table()
measures the content and produces this distribution:
# Initial auto-distribution (measured for this dataset):
# id: 7.5%, patient_id: 25.0%, age: 7.5%, weight_kg: 22.5%, treatment_group: 37.5%
spec <- create_table(data) |>
define_cols(id, colWidth = "10%")
# After locking id at 10%:
# - Available space: 100% - 10% = 90%
# - Unlocked weights: patient_id=25.0, age=7.5, weight_kg=22.5, treatment_group=37.5 (sum=92.5)
# - Normalized into 90%: patient_id=24.3%, age=7.3%, weight_kg=21.9%, treatment_group=36.5%
# - Total is exactly 100.0% after drift correctionNote how content-aware the initial split is: the narrow numeric
id and age columns keep 7.5%, while the long
text column treatment_group takes 37.5%.
To ensure widths sum exactly to 100%, ksTFL applies drift correction:
The sum is exact; the largest unlocked column absorbs the correction.
Use isVisible = FALSE to hide columns from output:
You cannot set colWidth for invisible
columns:
# This will error:
spec <- create_table(data) |>
define_cols(id, isVisible = FALSE, colWidth = "15%")
# Error message (verbatim):
# Cannot set `colWidth` for invisible column "id"
# x Column "id" has `isVisible = FALSE`
# i Invisible columns automatically have `colWidth = "0.0cm"`Why? Invisible columns are always “0.0cm”—setting a width would be contradictory.
Invisible columns exist mainly for this: driving conditional formatting.
spec <- create_table(data) |>
add_style(id = "highlight_yellow",
s_font(bold = TRUE),
s_table_style(background_color = "#FFFF99")) |>
# Define/lock widths BEFORE hiding (invisible columns reject colWidth)
define_cols(patient_id, isVisible = FALSE) |>
# Use the hidden column in compute_cols() for conditional styling
compute_cols(
startsWith(patient_id, "PAT-001"),
c_style(age, styleRef = "highlight_yellow")
)For complete manual control:
# Disable auto-recalculation
tfl_set_options(autoColWidth = FALSE)
# Set exact widths - no automatic adjustment
spec <- create_table(data) |>
define_cols(
c(id, patient_id, age, weight_kg, treatment_group),
colWidth = c("10%", "25%", "20%", "20%", "25%")
)
# Widths stay exactly as specified (sum = 100%)
# Re-enable for other tables
tfl_set_options(autoColWidth = TRUE)Two different constants apply to widths:
minColWidth (default 0.5%): the space the validator
reserves for each column that stays UNLOCKED when you lock another
one# This will error:
spec <- create_table(data) |>
define_cols(id, colWidth = "0.1%") # Below the fixed 0.5% floor
# Error message (verbatim):
# Column width "0.1%" is below minimum allowed
# x Relative widths must be at least 0.5%
# i Proposed: 0.1%The floor for a width you type is fixed at 0.5% —
minColWidth does not lower it. What
minColWidth controls is the space check below.
ksTFL prevents you from locking widths that leave insufficient space for the columns that stay unlocked:
# 5 columns, minColWidth = 0.5% (default):
# the 4 unlocked columns need 4 x 0.5% = 2% of the space.
# This will error:
spec <- create_table(data) |>
define_cols(id, colWidth = "99%") # Leaves only 1% for 4 columns
# Error message (verbatim):
# Cannot set column `id` to "99%"
# x This would leave insufficient space for the remaining 4 unlocked
# columns to meet the minimum width of 0.5%.
# i After this change, 4 columns need 2% total. Remaining space available: 1%.
# i Maximum allowed relative width for `id`: 98.0%
# i Reduce the proposed width or adjust other locked widthsRule of thumb: while any column stays unlocked, locked relative
widths must sum to at most 99.5% — the validator keeps the
minColWidth reserve for the last unlocked column, and the
error names your exact allowed maximum.
minColWidthRaise or lower the per-unlocked-column reserve to trade strictness against layout freedom:
# A 12-column frame: locking one column at 95% would leave 5% for 11
# unlocked columns, each needing 0.5% (5.5% total) — rejected.
wide_data <- as.data.frame(replicate(12, sample(100:999, 10)))
names(wide_data) <- paste0("COL", 1:12)
tfl_set_options(minColWidth = 0.5)
spec <- create_table(wide_data) |>
define_cols(COL1, colWidth = "95%") # errors: max allowed is 94.5%
# Relax the reserve and the same lock goes through:
tfl_set_options(minColWidth = 0.3)
spec <- create_table(wide_data) |>
define_cols(COL1, colWidth = "95%") # OK
tfl_set_options(minColWidth = 0.5) # restore defaultCause: A locked width leaves less than
minColWidth % for each column that stays unlocked.
Solutions:
minColWidth via
tfl_set_options(minColWidth = 0.3)Invisibility does not help here — the reserve counts every column in the spec, visible or not.
Cause: Drift correction. The largest unlocked column absorbs the whole rounding correction so the total stays exactly 100%; with many unlocked columns that correction can exceed 0.1% on one column.
Solution: This is expected and handled automatically. The rendered output is correct.
Cause: Trying to use colWidth with
isVisible = FALSE
Solution: Remove the colWidth argument
— an invisible column is rendered at “0.0cm” and takes no space, so a
width on it would be meaningless.
Cause: Auto-recalculation triggered by locking a width or toggling a column’s visibility.
Solution:
autoColWidth = TRUEtfl_set_options(autoColWidth = FALSE) for
manual controlNote:
spec$.metadatais an internal field. Its structure may change between package versions. Useprint(spec)anddefine_cols()for all user-facing width inspection and control.
ksTFL stores width metadata internally in
spec$.metadata$colWidths. This is used by the package
itself to:
You do not need to read or write this field directly. Use
print(spec) to inspect current widths and
define_cols(spec, col, colWidth = ...) to modify them.
define_cols() callFor more examples, see:
compute_cols() for conditional
logic