--- title: "Advanced StyleRows and Conditional Formatting 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 resource_files: - images/ vignette: > %\VignetteIndexEntry{Advanced StyleRows and Conditional Formatting 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 vignette explains the powerful conditional row styling system in ksTFL, centered around `compute_cols()` and its action functions: `c_style()`, `c_merge()`, and `c_addrow()`. These tools enable sophisticated data-driven formatting without manual post-processing. ## Category and prerequisites Prerequisites: complete [Getting Started](Getting_Started_with_ksTFL.html) and basic `compute_cols()` familiarity. The focus here is row-level logic, helper functions, and composable actions that stay readable as rules grow. ## Core Concept: Lazy Evaluation `compute_cols()` uses **lazy evaluation**: it captures conditions and actions as quosures, stores them in the spec metadata, and evaluates them later during `create_report()`. ### Why Lazy Evaluation? 1. **Deferred context**: column selections and data references are resolved only after the report structure is complete. 2. **Tidyselect support**: helpers such as `everything()` and `starts_with()` stay available inside the action definitions. 3. **Clean staging**: you specify rules first and execute them later, which keeps the build pipeline easier to inspect. ## The Three Action Functions ### `c_style()`: Conditional Styling Apply style references to cells based on conditions: ```{r c-style-basic} library(ksTFL) data <- data.frame( patient = sprintf("PAT-%03d", 1:20), age = c(23, 45, 67, 34, 89, 56, 42, 71, 38, 29, 55, 66, 44, 52, 60, 48, 35, 70, 41, 58), response = sample(c("CR", "PR", "SD", "PD"), 20, replace = TRUE) ) # Define styles first spec <- create_table(data) |> add_style(id = "highlight_green", s_font(color = "#006400", bold = TRUE)) |> add_style(id = "highlight_red", s_font(color = "#8B0000", bold = TRUE)) # Apply conditional styling spec <- spec |> compute_cols( response == "CR", c_style(response, styleRef = "highlight_green") ) |> compute_cols( response == "PD", c_style(response, styleRef = "highlight_red") ) ``` **Result:** Rows where `response == "CR"` show green bold text; rows where `response == "PD"` show red bold text: ![style conditional green red response](images/style-conditional-green-red-response.png) ### `c_merge()`: Conditional Cell Merging Merge multiple columns into a single display cell: ```{r c-merge-basic} data_groups <- data.frame( group = c("Treatment A", "Treatment A", "Treatment A", "Placebo", "Placebo"), visit = c("Week 0", "Week 4", "Week 8", "Week 0", "Week 4"), value = c(5.2, 6.1, 7.3, 4.8, 5.5) ) spec <- create_table(data_groups) |> compute_cols( !firstOf(group), # Not the first occurrence of this group value c_merge(c(group, visit)) ) ``` **Result:** When consecutive rows have the same `group`, their `group` and `visit` cells merge, displaying the merged content in the `group` column. ![merge group visit cells](images/merge-group-visit-cells.png) **Note:** The `cols` argument to `c_merge()` must resolve to at least two **consecutive** columns in the final report column order. The value shown in the merged cell is taken **from the first** column in the `cols` sequence. ### `c_addrow()`: Conditional Row Insertion Insert new rows based on data patterns: ```{r c-addrow-basic} spec <- create_table(data_groups) |> compute_cols( firstOf(group), # First row of each group c_addrow(pos = "above") # Insert an empty separator/header row above ) ``` **Result:** An empty separator row is inserted above each new group. To copy a single column's value into the inserted row use `value_from` (see later examples). Use `pos = "below"` to insert after the matching row instead. ![addrow separator above groups](images/addrow-separator-above-groups.png) ### `c_pageBreak()`: Conditional Page Break Insert a page break at the matching row. This is useful to force a new page when a logical grouping or large block ends. ```{r c-pagebreak-basic} spec <- create_table(data_groups) |> compute_cols( firstOf(group), # Insert page break starting from first row of each group c_pageBreak() ) ``` **Result:** The renderer will start a new page at rows matching the condition: ![pagebreak at group start](images/pagebreak-at-group-start.png) ## Evaluation Context ### The Data Environment Conditions and actions are evaluated in `spec$.metadata$data_env`, which contains: 1. **Data columns**: Direct access to all columns (even invisible ones) 2. **Helper functions**: `firstOf()`, `lastOf()`, `firstRow()`, `lastRow()`, `rowNumber()`, `everyNth()`, `firstOfBlock()` 3. **Embedded functions**: Internal utilities for column access > **Important**: These helper functions are only available inside the > `cond` argument of `compute_cols()`. **They are not standalone > exported functions** — you cannot call them outside of > `compute_cols()`. ```{r data-env-example} # You can reference any column directly in conditions spec <- create_table(data) |> compute_cols( age > 60 & response %in% c("CR", "PR"), # Multi-column condition c_style(c(age, response), styleRef = f_combine('b', 'bg_mint')) ) ``` ![style multi column conditional](images/style-multi-column-conditional.png){width="316"} ### Available Helper Functions The following helpers are available exclusively inside `compute_cols()` conditions: - **`firstOf(...)`**: Logical vector marking first occurrence of each value combination in specified columns - **`lastOf(...)`**: Logical vector marking last occurrence of each value combination in specified columns - **`firstRow()`**: Logical vector TRUE only at first row - **`lastRow()`**: Logical vector TRUE only at last row - **`rowNumber()`**: Integer vector of row numbers (1-based) - **`everyNth(n)`**: Logical vector TRUE at every nth row - **`firstOfBlock(col, n, offset)`**: Returns logical vector marking first row of every n-th block defined by `col` ```{r helpers-example} # Highlight first and last rows using helper functions spec <- create_table(data) |> compute_cols( firstRow() | lastRow(), c_style(everything(), styleRef = "border_emphasis") ) # Use firstOf/lastOf for value-based boundaries spec <- create_table(data_groups) |> add_style(id = "group_boundary", s_font(bold = TRUE)) |> compute_cols( firstOf(group) | lastOf(group), c_style(group, styleRef = "group_boundary") ) # Use everyNth for alternating patterns spec <- create_table(data) |> add_style(id = "gray_bg", s_table_style(background_color = "#F5F5F5")) |> compute_cols( everyNth(2), # Every 2nd row starting from row 1 c_style(everything(), styleRef = "gray_bg") ) ``` ## Advanced `c_style()` Patterns ### Multiple Column Styling Use tidyselect to style multiple columns at once: ```{r c-style-multi} data_lab <- data.frame( patient = sprintf("PAT-%03d", 1:10), hemoglobin = rnorm(10, 13.5, 1.5), glucose = rnorm(10, 95, 15), cholesterol = rnorm(10, 200, 30) ) spec <- create_table(data_lab) |> add_style(id = "out_of_range", s_font(color = "#FF4500", bold = TRUE)) |> compute_cols( hemoglobin < 12 | hemoglobin > 16, c_style(hemoglobin, styleRef = "out_of_range") ) |> compute_cols( glucose < 70 | glucose > 140, c_style(glucose, styleRef = "out_of_range") ) |> compute_cols( cholesterol > 240, c_style(cholesterol, styleRef = "out_of_range") ) ``` ![style out of range highlight](images/style-out-of-range-highlight.png) ### Conditional Styling with Complex Logic Combine multiple conditions: ```{r c-style-complex} spec <- create_table(data) |> add_style(id = "critical_senior", s_font(color = "#8B0000", bold = TRUE), s_table_style(background_color = "#FFEBCD")) |> compute_cols( age >= 60 & response == "PD", c_style(c(patient, age, response), styleRef = "critical_senior") ) ``` ![style critical senior age](images/style-critical-senior-age.png) ### Row-Level Styling Style entire rows by targeting all columns: ```{r c-style-row} spec <- create_table(data) |> add_style(id = "alternate_row", s_table_style(background_color = "#F0F0F0")) |> compute_cols( rowNumber() %% 2 == 0, # Even rows c_style(everything(), styleRef = "alternate_row") ) ``` ![style alternating row colors](images/style-alternating-row-colors.png) ## Advanced `c_merge()` Patterns ### Multi-Column Grouping Merges Merge across multiple grouping levels: ```{r c-merge-multi} data_nested <- data.frame( study = rep(c("Study A", "Study B"), each = 6), phase = rep(c("Phase I", "Phase II", "Phase III"), 4), site = rep(c("Site 1", "Site 2", "Site 1", "Site 2"), 3), enrollment = sample(10:50, 12) ) spec <- create_table(data_nested) |> # Merge study column for consecutive same-study rows compute_cols( !firstOf(study), c_merge(c(study, phase, site)) ) |> # Merge phase column within same study compute_cols( !firstOf(study, phase), c_merge(c(phase, site)) ) ``` ![merge multi column grouping](images/merge-multi-column-grouping.png) ### Conditional Display Column Content ### Merging with Styling Combine merging with conditional styles: ```{r c-merge-style} spec <- create_table(data_nested) |> add_style(id = "merged_header", s_font(bold = TRUE), s_table_style(background_color = "#E0E0E0")) |> compute_cols( !firstOf(study), c_merge(c(study, phase, site)) ) |> compute_cols( firstOf(study), # First row of group c_style(study, styleRef = "merged_header") ) ``` ![merge with styling](images/merge-with-styling.png) ## Advanced `c_addrow()` Patterns ### Summary Rows Insert calculated summary rows: ```{r c-addrow-summary} data_sales <- data.frame( region = c("North", "North", "South", "South", "West", "West"), product = rep(c("A", "B"), 3), revenue = c(100, 150, 200, 120, 180, 160), total = c(250, 250, 320, 320, 340, 340) ) spec <- create_table(data_sales) |> add_style(id = "summary_row", s_font(bold = TRUE), s_table_style(background_color = "#D3D3D3")) |> # we do not need the `total` column itself - set to invisible define_cols(total, isVisible = F) |> compute_cols( lastOf(region), # Last row of each region # Insert subtotal row c_addrow(pos = "below", value_from = total, #value from total column styleRef = f_combine("summary_row", 'ar')) ) ``` ![addrow summary subtotals](images/addrow-summary-subtotals.png) ### Header Rows Insert section (group) headers to make a stub: ```{r c-addrow-header} data_sales <- data.frame( region = c("North", "North", "South", "South", "West", "West"), product = rep(c("Gas", "Oil"), 3), revenue = c(100, 150, 200, 120, 180, 160) ) spec <- create_table(data_sales) |> # Custom style just for fun add_style(id = "section_header", s_font(bold = TRUE, font_size = "10pt", color = '#FFFFFF'), s_table_style(background_color = "#4682B4")) |> # Hide the `region` column as we want to use its value as heading define_cols(region, isVisible = F) |> # Add column labels: define_cols( c(product, revenue), label = c('Region
Product', 'Revenue
(Million of $)'), # Use embedded indent style to indent the `product` value in a column valueStyleRef = c('indent_1', NA) # NA here means we are not using any style for `revenue` ) |> # Use `c_addrow` to add a line with the value from `region` column compute_cols( firstOf(region), # First row of each new region c_addrow(pos = "above", value_from = region, styleRef = "section_header") ) ``` ![addrow header from column](images/addrow-header-from-column.png) More complex example with two-level indents: ```{r c-addrow-header-two} data_sales <- data.frame( region = c("North", "North", "North", "South", "South", "South", "West", "West", "West"), product = rep(c("Total","Gas", "Oil"), 3), revenue = c(250, 100, 150, 320, 200, 120, 340, 180, 160) ) spec <- create_table(data_sales) |> # Hide the `region` column as we want to use its value as heading define_cols(region, isVisible = F) |> # Add column labels: define_cols( c(product, revenue), label = c('Region
Product', 'Revenue
(Million of $)'), # Use embedded indent style to indent the `product` value in a column ) |> # Use `c_addrow` to add a line with the value from `region` column compute_cols( firstOf(region), # First row of each new region c_addrow(pos = "above", value_from = region, styleRef = 'b') ) |> # the `Total` value will be indented 0.5cm compute_cols( product == 'Total', c_style(product, f_combine('i', 'indent_1')), c_style(revenue, 'i') ) |> # Other values ('Gas', 'Oil') will be indented by 1cm compute_cols( product != 'Total', c_style(product, 'indent_2') ) ``` As a result we are getting two-level stub: ![addrow two level stub indent](images/addrow-two-level-stub-indent.png) ## `c_glue()`: Append or Prepend Text to Cell Values `c_glue()` concatenates a literal string or a data column value to the display text of matching cells — useful for appending units, prefixing markers, or building composite labels without creating extra columns. **Parameters:** \- `cols`: columns to modify (tidyselect) \- `position`: `"before"` or `"after"` — where to attach the text \- `glue_col`: name of a data column whose value to attach (mutually exclusive with `text`) \- `text`: a literal string to attach (mutually exclusive with `glue_col`) \- `separator`: string inserted between original value and the glued text (default `NULL`) ```{r c-glue-basic} data_units <- data.frame( parameter = c("Hemoglobin", "Glucose", "Cholesterol"), value = c(13.5, 95.0, 200.0), unit = c("g/dL", "mg/dL", "mg/dL") ) spec <- create_table(data_units) |> # Hide the unit column — use it only as a glue source define_cols(unit, isVisible = FALSE) |> # Append unit to value: "13.5" → "13.5 g/dL" compute_cols( !is.na(value), c_glue(value, position = "after", glue_col = unit, separator = " ") ) ``` ![glue append units](images/glue-append-units.png) `c_glue()` is fully compatible with `c_style()` and `c_merge()` in the same `compute_cols()` call. ------------------------------------------------------------------------ ## `c_clear()`: Blank Cell Content in Matching Rows `c_clear()` renders specified cells as empty (blank) in matching rows without removing the column or affecting layout. Useful for conditional deduplication, when a `dedupe` parameter of `define_col()` is not enough. ```{r c-clear-basic} data_groups <- data.frame( group = c("Treatment A", "Treatment A", "Treatment A", "Placebo", "Placebo"), visit = c("Week 0", "Week 4", "Week 8", "Week 0", "Week 4"), value = c(5.2, 6.1, 7.3, 4.8, 5.5) ) spec <- create_table(data_groups) |> # Show group label only on first row of each group; blank it on the rest compute_cols( !firstOf(group), c_clear(group) ) ``` ![clear blank cells](images/clear-blank-cells.png) **Note:** `c_clear()` only affects the rendered display text. The underlying data value is still available for conditions in other `compute_cols()` calls. ------------------------------------------------------------------------ ## Combining Actions Together Chain multiple `compute_cols()` calls and `c_*` actions to build a fully formatted table: ```{r combine-sequential} data_sales <- data.frame( region = c("North", "North", "North", "South", "South", "South", "West", "West", "West"), product = rep(c("Oil", "Gas", "TOTAL"), 3), revenue = c(100, 150, 250, 200, 120, 320, 180, 160, 340) ) spec <- create_table(data_sales) |> add_style(id = "section_header", s_font(bold = TRUE, font_size = "11pt", color = '#FFFFFF'), s_table_style(background_color = "#4682B4")) |> # Make Region invisible, since we want to make it as header row define_cols(region, isVisible = F) |> # Define column labels define_cols(c(product, revenue), label = c('Product', 'Revenue
(Million of $)')) |> # Make a header row from the `region` value compute_cols( firstOf(region), # First row of each new region c_addrow(pos = "above", value_from = region, styleRef = "section_header") ) |> # Create a summary row for `Total` value # Note here how a consecutive calls to `c_*` atomic functions produce final result compute_cols( product == 'TOTAL', # merge `product` and `revenue` to a single column (the value became value from `product`) c_merge(c(product, revenue), styleRef = f_combine('b','i','bt_th','ar')), # clear the value, since we want to rebuild it: c_clear(product), # take the value of `region` to the merged cells.. c_glue(product, 'after', region), # add the word 'total' ... c_glue(product, 'after', text = ' total: '), # and add the value for `revenue` column to get the final string c_glue(product, 'after', revenue) ) ``` Here we can see how a simple planar data frame: ![data simple planar dataframe](images/data-simple-planar-dataframe.png) become a production ready table:\ ![result production ready table](images/result-production-ready-table.png) ## Performance Tips ### 1. Minimize `compute_cols()` Calls Each call adds evaluation overhead. Combine conditions when possible: ```{r perf-minimize} # ❌ Less efficient: spec <- create_table(data) |> compute_cols(age < 30, c_style(age, styleRef = "young")) |> compute_cols(age >= 30 & age < 60, c_style(age, styleRef = "middle")) |> compute_cols(age >= 60, c_style(age, styleRef = "senior")) # ✅ More efficient (single pass with case_when logic): spec <- create_table(data) |> add_style(id = "young", s_font(color = "#008000")) |> add_style(id = "middle", s_font(color = "#0000FF")) |> add_style(id = "senior", s_font(color = "#FF0000")) |> compute_cols( age < 30, c_style(age, styleRef = "young") ) |> compute_cols( age >= 30 & age < 60, c_style(age, styleRef = "middle") ) |> compute_cols( age >= 60, c_style(age, styleRef = "senior") ) # Note: Consider refactoring to reduce compute_cols() calls if performance is critical ``` ### 2. Use Vectorized Conditions Avoid row-by-row operations in custom functions: ```{r perf-vectorized} # ❌ Slower (scalar logic): spec <- create_table(data) |> compute_cols( sapply(response, function(x) x %in% c("CR", "PR")), # Row-by-row c_style(response, styleRef = "responder") ) # ✅ Faster (vectorized): spec <- create_table(data) |> compute_cols( response %in% c("CR", "PR"), # Vectorized c_style(response, styleRef = "responder") ) ``` ### 3. Pre-Filter Data When Possible If conditions apply to a small subset, consider filtering data upfront: ```{r perf-prefilter} # If only 5% of rows need special formatting: # Consider creating separate tables and combining in report data_outliers <- subset(data, age > 80) data_normal <- subset(data, age <= 80) spec_outliers <- create_table(data_outliers) |> add_style(id = "outlier", s_font(color = "#FF0000", bold = TRUE)) |> compute_cols(TRUE, c_style(everything(), styleRef = "outlier")) spec_normal <- create_table(data_normal) # Combine in report report <- create_report(spec_normal, spec_outliers) ``` ### 4. Style Consolidation ksTFL automatically consolidates identical styles, but you can help by reusing style references: ```{r perf-consolidation} # ✅ Define once, use many times: spec <- create_table(data) |> add_style(id = "critical", s_font(color = "#FF0000", bold = TRUE)) |> compute_cols(age > 80, c_style(age, styleRef = "critical")) |> compute_cols(response == "PD", c_style(response, styleRef = "critical")) # ❌ Avoid duplicate style definitions: # (This creates two identical but separate styles) spec <- create_table(data) |> add_style(id = "critical_age", s_font(color = "#FF0000", bold = TRUE)) |> add_style(id = "critical_response", s_font(color = "#FF0000", bold = TRUE)) ``` ## Debugging and Inspection ### Viewing Captured Actions Inspect what `compute_cols()` has stored: ```{r debug-inspect} spec <- create_table(data) |> compute_cols( age > 60, c_style(age, styleRef = "elderly") ) # Examine metadata str(spec$.metadata$compute_cols) # Shows captured quosures and action types ``` ### Testing Conditions Manually Test conditions on your data frame before adding to spec: ```{r debug-test} # Test your condition directly on the data before passing to compute_cols() test_condition <- with(data, age > 60) sum(test_condition) # How many rows match? data[test_condition, ] # Which rows? # Once confirmed, add to spec spec <- spec |> compute_cols(age > 60, c_style(age, styleRef = "elderly")) ``` ### Incremental Building Add `compute_cols()` one at a time and inspect results: ```{r debug-incremental} spec <- create_table(data) # Add first action spec <- spec |> compute_cols(age > 60, c_style(age, styleRef = "elderly")) print(spec) # Check structure # Add second action spec <- spec |> compute_cols(response == "CR", c_style(response, styleRef = "success")) print(spec) # Check again ``` ## Common Patterns Library ### Pattern 1: Alternating Row Colors ```{r pattern-alternate} spec <- create_table(data) |> add_style(id = "gray_bg", s_table_style(background_color = "#F5F5F5")) |> compute_cols( rowNumber() %% 2 == 0, c_style(everything(), styleRef = "gray_bg") ) ``` ### Pattern 2: Grouped Section Headers with Merging ```{r pattern-grouped} spec <- create_table(data_groups) |> add_style(id = "group_header", s_font(bold = TRUE, font_size = "11pt"), s_table_style(background_color = "#D0D0D0")) |> # Insert header row before each new group compute_cols( firstOf(group), c_addrow(pos = "above", styleRef = "group_header") ) |> # Merge consecutive same-group cells compute_cols( !firstOf(group), c_merge(c(group, visit)) ) ``` ### Pattern 3: Conditional Highlighting with Thresholds ```{r pattern-thresholds} spec <- create_table(data_lab) |> add_style(id = "low", s_font(color = "#0000FF")) |> add_style(id = "normal", s_font(color = "#008000")) |> add_style(id = "high", s_font(color = "#FF0000")) |> compute_cols( hemoglobin < 12, c_style(hemoglobin, styleRef = "low") ) |> compute_cols( hemoglobin >= 12 & hemoglobin <= 16, c_style(hemoglobin, styleRef = "normal") ) |> compute_cols( hemoglobin > 16, c_style(hemoglobin, styleRef = "high") ) ``` ### Pattern 4: Summary Rows with Totals ```{r pattern-totals} spec <- create_table(data_sales) |> add_style(id = "total_row", s_font(bold = TRUE), s_table_style(background_color = "#FFD700")) |> compute_cols( lastOf(region), # Last row of each region c_addrow(pos = "below", styleRef = "total_row") ) ``` ## Limitations and Workarounds ### Limitation 1: No Direct Aggregate Functions in Conditions You can't use `sum()`, `mean()`, etc. directly in conditions: ```{r limit-aggregates, error=TRUE} # ❌ This won't work as expected: spec <- create_table(data) |> compute_cols( age > mean(age), # Evaluates mean() at condition capture, not evaluation c_style(age, styleRef = "above_average") ) ``` **Workaround:** Pre-calculate and add as a column: ```{r workaround-aggregates} data$age_above_avg <- data$age > mean(data$age) spec <- create_table(data) |> compute_cols( age_above_avg, c_style(age, styleRef = "above_average") ) ``` ### Limitation 2: No Nested `c_*()` Functions You can't nest action functions: ```{r limit-nested, error=TRUE} # ❌ This is invalid: spec <- create_table(data) |> compute_cols( age > 60, c_style(age, styleRef = c_merge(patient, age)) # Not allowed ) ``` **Workaround:** Use separate `compute_cols()` calls: ```{r workaround-nested} spec <- create_table(data) |> compute_cols(age > 60, c_style(age, styleRef = "elderly")) |> compute_cols(age > 60, c_merge(c(patient, age))) ``` ### Limitation 3: Style References Must Exist `styleRef` must reference a previously defined style: ```{r limit-styleref, error=TRUE} # ❌ This will error at evaluation time: spec <- create_table(data) |> compute_cols(age > 60, c_style(age, styleRef = "undefined_style")) ``` **Workaround:** Always define styles before using them: ```{r workaround-styleref} spec <- create_table(data) |> add_style(id = "elderly", s_font(bold = TRUE)) |> # Define first compute_cols(age > 60, c_style(age, styleRef = "elderly")) ``` ## Integration with Other ksTFL Features ### Using with `define_cols()` `compute_cols()` works alongside column definitions: ```{r integrate-define} spec <- create_table(data) |> define_cols(age, type = "numeric", format = "0.0", colWidth = "15%") |> compute_cols( age > 65, c_style(age, styleRef = "elderly") ) ``` ### Using with Invisible Columns Reference invisible columns in conditions: ```{r integrate-invisible} data$flag <- sample(c(TRUE, FALSE), nrow(data), replace = TRUE) spec <- create_table(data) |> define_cols(flag, isVisible = FALSE) |> # Hide column compute_cols( flag == TRUE, # But use it in condition c_style(response, styleRef = "flagged") ) ``` ### Using in Multi-Spec Reports Each spec can have independent `compute_cols()` logic: ```{r integrate-report} spec1 <- create_table(data[1:10, ]) |> add_style(id = "elderly", s_font(bold = TRUE)) |> compute_cols(age > 60, c_style(age, styleRef = "elderly")) spec2 <- create_table(data[11:20, ]) |> add_style(id = "success", s_font(bold = TRUE)) |> compute_cols(response == "CR", c_style(response, styleRef = "success")) report <- create_report(spec1, spec2) ``` ## Best Practices 1. **Define styles first**: Use `add_style()` before `compute_cols()` 2. **Test conditions incrementally**: Add one `compute_cols()` at a time 3. **Use meaningful style names**: "elderly" is clearer than "style1" 4. **Document complex logic**: Add comments explaining condition rationale 5. **Prefer vectorized operations**: Avoid row-by-row functions where possible 6. **Reuse styles**: Define once, reference many times for consistency 7. **Keep conditions simple**: Complex logic is harder to debug 8. **Use helper functions**: `firstOf()`, `lastOf()`, `firstRow()`, `lastRow()` are more readable than complex comparisons ## Summary - **`compute_cols()`**: Captures conditions and actions via lazy evaluation - **`c_style()`**: Apply conditional styling to cells - **`c_merge()`**: Merge cells across columns based on conditions - **`c_addrow()`**: Insert new rows dynamically (`pos = "above"` or `"below"`) - **`c_pageBreak()`**: Insert a page break at the matching row - **`c_clear()`**: Blank the rendered display text of specified cells in matching rows (deduplication) - **Evaluation context**: Data environment with helper functions (`firstOf()`, `lastOf()`, `firstRow()`, `lastRow()`, `rowNumber()`, `everyNth()`, `firstOfBlock()`) - **Performance**: Minimize calls, use vectorized operations, consolidate styles - **Debugging**: Inspect metadata, test conditions manually, build incrementally For more information, see: - [Getting Started](Getting_Started_with_ksTFL.html) — full pipeline overview and `compute_cols()` introduction - [Styling Guide](Styling_Guide_with_ksTFL.html) — styling fundamentals and built-in atoms - [Reporting Examples](Reporting_Examples_with_ksTFL.html) — complete end-to-end workflows - [Column Width Management](Column_Width_Management.html) — invisible columns for conditional logic