--- title: "Column Width Management in ksTFL" author: "ksTFL Development Team" date: "`r Sys.Date()`" output: rmarkdown::html_vignette: toc: true toc_depth: 3 dev: pdf css: ksTFL-vignette.css vignette: > %\VignetteIndexEntry{Column Width Management in ksTFL} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: markdown: wrap: 80 --- ```{r setup, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE, dev = "png", dev.args = if (capabilities("cairo")) list(type = "cairo") else NULL ) options(pkgdown.internet = FALSE) ``` ksTFL logo ## Overview 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: 1. **VISIBLE Columns** (`isVisible != FALSE`) - Displayed in the report output - Participate in width calculations - This is the default state for all columns 2. **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 3. **UNLOCKED Columns** (no `colWidth` set) - Automatically recalculated to fill available space - Normalized proportionally based on initial auto-detected weights - Only visible unlocked columns participate in recalculation 4. **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: 1. Analyzes each column's data type and content 2. Estimates visual width based on: - Maximum value length - Column name length - Label length (if provided) 3. Distributes widths proportionally so they sum to 100% 4. Applies minimum width constraints (0.5% for relative widths) ```{r initial-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 columns ``` ### The `autoColWidth` Option The `autoColWidth` option (default: `TRUE`) controls whether widths are automatically recalculated when you lock columns: ```{r autocol-option} # 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: ```{r basic-locking} # 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: ```{r multiple-locks} 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% proportionally ``` ### Mixing Relative and Absolute Units You can mix percentage widths with absolute units: ```{r mixed-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% proportionally ``` **Important:** 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: 1. **Partitions columns** into LOCKED and UNLOCKED 2. **Calculates available space**: 100% minus sum of locked percentage widths 3. **Normalizes unlocked widths** to fill available space proportionally 4. **Rounds to 1 decimal place** with drift correction ### Example Walkthrough ```{r recalc-example} # 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 column ``` ### Drift Correction To ensure widths sum exactly to 100%, ksTFL applies drift correction: - Rounds all widths to 1 decimal place - Calculates total rounding error (drift) - Adds/subtracts drift from the largest unlocked column This guarantees valid output while maintaining proportions. ## Invisible Columns ### Making Columns Invisible Use `isVisible = FALSE` to hide columns from output: ```{r invisible-basic} 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:** ```{r invisible-error, error=TRUE} # 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: ```{r invisible-logic} 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: ```{r manual-widths} # 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) ``` ### Why Use Manual Mode? - **Precision**: When you need exact widths without rounding - **Complex layouts**: Multi-level headers with specific alignments - **Pre-calculated**: When you've determined optimal widths externally ## 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) ```{r min-width-error, error=TRUE} # 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: ```{r space-constraint-error, error=TRUE} # 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: ```{r custom-min} # 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 ```{r pattern-id} spec <- create_table(data) |> define_cols(id, colWidth = "8%", isID = TRUE) |> define_cols(patient_id, colWidth = "15%") # Result: ID columns fixed, others auto-distribute ``` ### Pattern 2: Fixed-Width Text + Flex Numeric ```{r pattern-fixed-flex} 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 ```{r pattern-all-manual} 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 ```{r pattern-progressive} # 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 lock ``` ## Troubleshooting ### 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. ### Issue: Can't set width for invisible column **Cause:** Trying to use `colWidth` with `isVisible = FALSE` **Solution:** Remove `colWidth` parameter—invisible columns are always "0.0cm" ### Issue: Widths change unexpectedly after define_cols() **Cause:** Auto-recalculation triggered by locking a column **Solution:** - This is expected behavior when `autoColWidth = TRUE` - Disable with `tfl_set_options(autoColWidth = FALSE)` for manual control - Or lock all columns explicitly ## Advanced: Width Metadata > **Note**: `spec$.metadata` is an internal field. Its structure may > change between package versions. Use `print(spec)` and `define_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 1. **Start with defaults**: Let ksTFL auto-detect widths initially 2. **Lock strategically**: Fix only the columns that need exact widths 3. **Use print()**: Inspect the spec after each `define_cols()` call 4. **Test rendering**: Verify widths in actual output documents 5. **Document intent**: Add comments explaining width choices 6. **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](Reporting_Examples_with_ksTFL.html) — complete end-to-end workflows including column width patterns - [Getting Started](Getting_Started_with_ksTFL.html) — overview of the full pipeline - [Advanced StyleRows](Advanced_StyleRows.html) — using invisible columns with `compute_cols()` for conditional logic