Column Width Management in ksTFL
ksTFL Development Team
2026-06-19
Source:vignettes/Column_Width_Management.Rmd
Column_Width_Management.RmdOverview
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.
Core Concepts
Column States
ksTFL partitions table columns into four categories that determine how widths are calculated:
-
VISIBLE Columns (
isVisible != FALSE)- Displayed in the report output
- Participate in width calculations
- This is the default state for all columns
-
LOCKED Columns (width explicitly set via
colWidth)- Maintain their exact specified width (any unit: %, cm, in, mm, pt)
- Fixed during automatic recalculation
- Can use relative (%) or absolute (cm, in) units
-
UNLOCKED Columns (no
colWidthset)- Automatically recalculated to fill available space
- Normalized proportionally based on initial auto-detected weights
- Only visible unlocked columns participate in recalculation
-
INVISIBLE Columns (
isVisible = FALSE)- Hidden from output completely
- Automatically assigned width “0.0cm”
- Excluded from all width calculations
- Data still accessible for conditional logic (e.g.,
compute_cols())
Initial Width Distribution
When you create a table, ksTFL automatically:
- Analyzes each column’s data type and content
- Estimates visual width based on:
- Maximum value length
- Column name length
- Label length (if provided)
- Distributes widths proportionally so they sum to 100%
- Applies minimum width constraints (0.5% for relative widths)
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 columnsThe autoColWidth Option
The autoColWidth option (default: TRUE)
controls whether widths are automatically recalculated when you lock
columns:
# Check current setting
tfl_get_option("autoColWidth") # TRUE by default
# Disable for manual width management
tfl_set_options(autoColWidth = FALSE)
# Re-enable (restore default behavior)
tfl_set_options(autoColWidth = TRUE)Width Locking Workflow
Basic Locking
When you set colWidth for a column, it becomes
LOCKED:
# Lock the 'id' column at 15%
spec <- create_table(data) |>
define_cols(id, colWidth = "15%")
# Result:
# - id: 15% (LOCKED)
# - Other visible columns: auto-recalculated to fill remaining 85%Multiple Locked Columns
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% proportionallyMixing Relative and Absolute Units
You can mix percentage widths with absolute units:
spec <- create_table(data) |>
define_cols(id, colWidth = "2.5cm") |> # Fixed width (doesn't reduce % space)
define_cols(patient_id, colWidth = "20%") # Takes 20% of available
# Result:
# - id: 2.5cm (LOCKED, absolute)
# - patient_id: 20% (LOCKED, relative)
# - Other columns: share remaining 80% proportionallyImportant: Fixed-unit columns (cm, in, mm, pt) don’t reduce the available percentage space—only locked percentage columns do.
Width Recalculation Algorithm
When autoColWidth = TRUE and you lock a column,
ksTFL:
- Partitions columns into LOCKED and UNLOCKED
- Calculates available space: 100% minus sum of locked percentage widths
- Normalizes unlocked widths to fill available space proportionally
- Rounds to 1 decimal place with drift correction
Example Walkthrough
# Initial auto-distribution (example values):
# id: 15%, patient_id: 25%, age: 20%, weight_kg: 20%, treatment_group: 20%
spec <- create_table(data) |>
define_cols(id, colWidth = "10%")
# After locking id at 10%:
# - Available space: 100% - 10% = 90%
# - Unlocked weights: patient_id=25, age=20, weight_kg=20, treatment_group=20 (sum=85)
# - Normalized: patient_id=26.5%, age=21.2%, weight_kg=21.2%, treatment_group=21.2%
# - Result sums to 100.1% (rounding), drift corrected to largest columnInvisible Columns
Making Columns Invisible
Use isVisible = FALSE to hide columns from output:
spec <- create_table(data) |>
define_cols(id, isVisible = FALSE)
# Result:
# - id: hidden, width = "0.0cm" (automatic)
# - Other columns: recalculated to fill 100%Important Constraints
You cannot set colWidth for invisible
columns:
# This will error:
spec <- create_table(data) |>
define_cols(id, isVisible = FALSE, colWidth = "15%")
# Error message:
# "Cannot set colWidth for invisible column 'id'"Why? Invisible columns are always “0.0cm”—setting a width would be contradictory.
Using Invisible Columns for Logic
Invisible columns are perfect for conditional formatting:
spec <- create_table(data) |>
# Hide the flag column but keep data available
define_cols(patient_id, isVisible = FALSE) |>
# Use it in compute_cols() for conditional styling
compute_cols(
startsWith(patient_id, "PAT-001"),
c_style(age, styleRef = "highlight_yellow")
)Manual Width Management
Disabling Auto-Recalculation
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)Validation and Constraints
Minimum Width Thresholds
ksTFL enforces minimum widths to prevent unreadable columns:
Relative widths (%): - Minimum: 0.5%
Absolute widths (cm, in, mm, pt): - Minimum: 0.2cm (~0.08in)
# This will error:
spec <- create_table(data) |>
define_cols(id, colWidth = "0.1%") # Below 0.5% minimum
# Error: "Column width '0.1%' is below minimum allowed"Space Constraint Validation
ksTFL prevents you from locking widths that leave insufficient space for other columns:
# 5 columns with minColWidth = 0.5% (default)
# Minimum space needed for 4 unlocked columns: 4 × 0.5% = 2%
# This will error:
spec <- create_table(data) |>
define_cols(id, colWidth = "99%") # Leaves only 1% for 4 columns
# Error: "Cannot set column 'id' to '99%'"
# "This would leave insufficient space for the remaining 4 unlocked columns"
# "Maximum allowed relative width for id: 98.0%"Adjusting Minimum Width
You can customize the minimum width threshold:
# Allow tighter columns (use with caution)
tfl_set_options(minColWidth = 0.3)
# Now you can use narrower relative widths
spec <- create_table(data) |>
define_cols(id, colWidth = "95%") # More space for this column
# Reset to default
tfl_set_options(minColWidth = 0.5)Common Patterns
Pattern 1: ID Column + Auto Widths
spec <- create_table(data) |>
define_cols(id, colWidth = "8%", isID = TRUE) |>
define_cols(patient_id, colWidth = "15%")
# Result: ID columns fixed, others auto-distributePattern 2: Fixed-Width Text + Flex Numeric
spec <- create_table(data) |>
define_cols(
c(id, patient_id, treatment_group),
colWidth = c("8%", "20%", "22%")
)
# age and weight_kg auto-fill remaining 50%Pattern 3: All Manual Widths
tfl_set_options(autoColWidth = FALSE)
spec <- create_table(data) |>
define_cols(
c(id, patient_id, age, weight_kg, treatment_group),
colWidth = c("8%", "22%", "15%", "18%", "37%")
) # Sum = 100% exactly
tfl_set_options(autoColWidth = TRUE)Pattern 4: Progressive Locking
# Lock columns one at a time, observing effects
spec <- create_table(data)
print(spec) # See initial distribution
spec <- spec |>
define_cols(id, colWidth = "10%")
print(spec) # See after first lock
spec <- spec |>
define_cols(patient_id, colWidth = "20%")
print(spec) # See after second lockTroubleshooting
Issue: “Insufficient space for remaining columns”
Cause: Locked columns leave <
minColWidth % per unlocked column
Solutions: 1. Reduce the locked width you’re trying
to set 2. Lower minColWidth via
tfl_set_options(minColWidth = 0.3) 3. Lock more columns
explicitly to reduce unlocked count 4. Make some columns invisible to
exclude them
Issue: Widths don’t sum to exactly 100%
Cause: Rounding errors from drift correction
Solution: This is expected and handled automatically. Drift is always ≤ 0.1% and applied to the largest column. The rendered output will be correct.
Advanced: Width Metadata
Note:
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: - Recalculate widths without re-parsing width strings - Track
which columns are locked vs. unlocked - Preserve initial proportions for
normalization
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.
Best Practices
- Start with defaults: Let ksTFL auto-detect widths initially
- Lock strategically: Fix only the columns that need exact widths
-
Use print(): Inspect the spec after each
define_cols()call - Test rendering: Verify widths in actual output documents
- Document intent: Add comments explaining width choices
- Use invisibility: Hide helper columns instead of tiny widths
Summary
- Four states: VISIBLE/LOCKED/UNLOCKED/INVISIBLE determine width behavior
- Auto-recalculation: Triggered when you lock widths (if enabled)
- Validation: Minimum thresholds and space constraints prevent errors
- Flexibility: Mix relative (%) and absolute (cm, in) units
- Control: Disable auto-recalculation for manual width management
For more examples, see:
- Reporting Examples — complete end-to-end workflows including column width patterns
- Getting Started — overview of the full pipeline
-
Advanced StyleRows — using
invisible columns with
compute_cols()for conditional logic