lt vs gt — gsDesign2 examples (source: gsDesign2.Rmd) ⏵
---
title: "lt vs gt — gsDesign2 examples"
---
This document re-runs a few of the `as_gt()` examples from
[gsDesign2](https://github.com/Merck/gsDesign2)'s documentation, but builds
the tables with `lt` instead of `gt`. The first goal of `lt` is to be a
drop-in lightweight replacement for `gt` in gsDesign2 outputs.
Because `lt()` is an S3 generic, we define methods for gsDesign2's summary
classes. In practice these methods would live in gsDesign2 (or a bridge
package); here they're defined inline for demonstration.
```{r setup, message=FALSE}
library(lt)
library(gsDesign2)
enroll_rate = define_enroll_rate(duration = 18, rate = 20)
fail_rate = define_fail_rate(
duration = c(4, 100), fail_rate = log(2) / 12,
dropout_rate = .001, hr = c(1, .6)
)
study_duration = 36
alpha = 0.025
beta = 0.1
lt.fixed_design_summary = function(data, ...) {
x = lt(as.data.frame(data), ...)
if (!is.null(attr(data, "title")))
x = lt_header(x, title = attr(data, "title"))
if (!is.null(attr(data, "footnote")))
x = lt_footnote(x, attr(data, "footnote"), "title")
x
}
lt.gs_design_summary = function(data, ...) {
x = lt(as.data.frame(data), ...) |>
lt_group(~ Analysis, sep = TRUE) |>
lt_spanner(
label = "Cumulative boundary crossing probability",
columns = c("Alternate hypothesis", "Null hypothesis")
) |>
lt_format(
c("Z", "~HR at bound", "Nominal p",
"Alternate hypothesis", "Null hypothesis"),
decimals = 4
)
x
}
registerS3method("lt", "fixed_design_summary", lt.fixed_design_summary, asNamespace("lt"))
registerS3method("lt", "gs_design_summary", lt.gs_design_summary, asNamespace("lt"))
```
## Fixed design — AHR method
Equivalent of `fixed_design_ahr() |> summary() |> as_gt()`.
```{r fixed-ahr}
fixed_design_ahr(
alpha = alpha, power = 1 - beta,
enroll_rate = enroll_rate, fail_rate = fail_rate,
study_duration = study_duration, ratio = 1
) |> summary() |> lt()
```
## Fixed design — Fleming-Harrington
Equivalent of `fixed_design_fh() |> summary() |> as_gt()`.
```{r fixed-fh}
fixed_design_fh(
alpha = alpha, power = 1 - beta,
enroll_rate = enroll_rate, fail_rate = fail_rate,
study_duration = study_duration, ratio = 1
) |> summary() |> lt()
```
## Group sequential — `gs_power_ahr`
Equivalent of `gs_power_ahr(lpar = ...) |> summary() |> as_gt()`. The long
"Analysis: N Time: ... AHR: ..." label is a natural row-group header.
The `lt.gs_design_summary` method handles grouping, spanner, and
formatting automatically.
```{r gs-power-ahr}
gs_power_ahr(lpar = list(sf = gsDesign::sfLDOF, total_spend = 0.1)) |>
summary() |>
lt() |>
lt_header(
title = "Bound summary for AHR design",
subtitle = "AHR approximations of ~HR at bound"
) |>
lt_footnote(
"Approximate hazard ratio to cross bound.",
"column", "~HR at bound"
) |>
lt_footnote(
"One-sided p-value for experimental vs control treatment.",
"column", "Nominal p"
)
```
## Group sequential — `gs_design_ahr`
Equivalent of `gs_design_ahr() |> summary() |> as_gt()`.
```{r gs-design-ahr}
gs_design_ahr() |>
summary() |>
lt() |>
lt_header(
title = "Bound summary for AHR design",
subtitle = "AHR approximations of ~HR at bound"
)
```
## What's missing vs `as_gt()`
With `lt()` as an S3 generic, gsDesign2 can ship its own `lt.gs_design_summary`
and `lt.fixed_design_summary` methods (similar to the ones defined above),
making `summary() |> lt()` work out of the box. The mapping from gt to lt is:
| gt | lt |
|-----------------------------------|-------------------------------------|
| `gt::gt(groupname_col=, rowname_col=)` | `lt_group(~ col)` (first column auto-becomes stub) |
| `gt::tab_header(title=, subtitle=)` | `lt_header(title=, subtitle=)` |
| `gt::tab_spanner(label=, columns=)` | `lt_spanner(label=, columns=)` |
| `gt::tab_footnote(footnote=, locations=cells_*())` | `lt_footnote(text, where, columns)` |
| `gt::tab_source_note(source_note=)` | `lt_note(text)` |
| `gt::fmt_number(columns=, decimals=)` | `lt_format(columns, decimals)` |
lt.js — JavaScript API Reference (source: lt-js.Rmd) ⏵
---
title: "lt.js — JavaScript API Reference"
output:
html:
meta:
css: ["@default", "@lt"]
js: ["@lt"]
---
This document demonstrates the lt.js runtime directly from JavaScript.
Each example shows a JSON spec and renders the table with `LT.build()`.
## Including lt.js
Add the stylesheet and script to your HTML page:
``` html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xiee/utils/css/lt.min.css">
<script src="https://cdn.jsdelivr.net/npm/@xiee/utils/js/lt.min.js"></script>
```
Then call `LT.build(spec)` from an inline `<script>` wherever you want a
table to appear. The runtime renders the table immediately after the
calling script element.
```{r, echo = FALSE}
iris5 = head(iris, 5)
mtcars6 = head(mtcars[, 1:6], 6)
```
```{js, echo = FALSE}
// Shim: lt.js is deferred, so queue calls until it loads
if (!window.LT) window.LT = {
q: [],
build(spec) { this.q.push({s: document.currentScript, d: spec}); }
};
```
Define sample datasets as JavaScript objects (interpolated from R via
litedown's `fill` option):
```{js, fill = xfun::tojson}
const mtcars = `{ mtcars6 }`;
const iris = `{ iris5 }`;
```
## Basic table
The simplest spec: just `data` (a column-oriented object).
```{js}
LT.build({ data: mtcars });
```
## Auto-formatting
Numeric columns are automatically formatted: decimal places are capped
dynamically based on magnitude, large numbers get thousand separators
(non-breaking space), and negative values use typographic minus (U+2212).
```{js}
LT.build({
data: {
Metric: ["Revenue", "Costs", "Profit", "Loss"],
Value: [1234567.891, 987654.321, 246913.57, -42.001]
}
});
```
### Disabling auto-format
Set `auto_fmt: false` to show raw values. Compare with the table above:
```{js}
LT.build({
data: {
Metric: ["Revenue", "Costs", "Profit", "Loss"],
Value: [1234567.891, 987654.321, 246913.57, -42.001]
},
auto_fmt: false
});
```
### Percentage detection
Columns whose label matches `/%|[ _](pct|percent)$/i` are auto-detected
as percentages: values are multiplied by 100 and suffixed with "%".
```{js}
LT.build({
data: {
Endpoint: ["Primary", "Secondary"],
success_pct: [0.7823, 0.4512]
}
});
```
## Title and subtitle
```{js}
LT.build({
data: iris,
header: {
title: "Iris Measurements",
subtitle: "First five observations"
}
});
```
## Column labels
Rename column headers without modifying the data keys:
```{js}
LT.build({
data: iris,
ops: [
{ type: "label", labels: {
"Sepal.Length": "Length (cm)", "Sepal.Width": "Width (cm)",
"Petal.Length": "Length (cm)", "Petal.Width": "Width (cm)"
}}
]
});
```
## Column alignment
By default, numeric columns are right-aligned. Override with an `align` op:
```{js}
LT.build({
data: mtcars,
ops: [
{ type: "align", columns: ["mpg", "cyl"], align: "center" }
]
});
```
## Number formatting (`fmt_number`)
Explicit control over decimals and thousands separators:
```{js}
LT.build({
data: {
Item: ["Widget", "Gadget", "Doohickey"],
Revenue: [1234567.891, 987654.321, 246913.57],
Margin: [0.1234, 0.0567, 0.2345]
},
ops: [
{ type: "fmt_number", columns: ["Revenue"], decimals: 0, big_mark: "," },
{ type: "fmt_number", columns: ["Margin"], decimals: 2, percent: true }
]
});
```
### Percent without multiplication
Use `percent: "%"` when values are already in percent scale (no `*100`):
```{js}
LT.build({
data: {
Test: ["A", "B"],
Rate: [78.5, 42.1]
},
ops: [
{ type: "fmt_number", columns: ["Rate"], decimals: 1, percent: "%" }
]
});
```
## Value substitution (`sub`)
Replace missing, zero, or small values with display text:
```{js}
LT.build({
data: {
Metric: ["HR", "p-value", "Events", "Rate"],
Value: [0.62, 0.0003, 0, null]
},
ops: [
{ type: "fmt_number", columns: ["Value"], decimals: 2 },
{ type: "sub", columns: ["Value"], missing: "n/a", zero: "—", small: 0.001, small_text: "< 0.001" }
]
});
```
## Column merging (`merge`)
Combine multiple columns into one display column. The `pattern` uses
`{1}`, `{2}`, etc. for column positions. Wrap sections in `<<` `>>` for
conditional NA handling.
```{js}
LT.build({
data: {
Endpoint: ["Primary", "Secondary", "Tertiary"],
est: [0.61, 0.79, 0.45],
ci_lo: [0.40, 0.57, null],
ci_hi: [0.82, 1.01, null]
},
ops: [
{ type: "fmt_number", columns: ["est", "ci_lo", "ci_hi"], decimals: 2 },
{ type: "merge", columns: ["est", "ci_lo", "ci_hi"], pattern: "{1}<< ({2}, {3})>>" },
{ type: "label", labels: { est: "Estimate (95% CI)" } }
]
});
```
## Column spanners
Group contiguous columns under a shared header:
```{js}
LT.build({
data: iris,
spanners: [
{ label: "Sepal", columns: ["Sepal.Length", "Sepal.Width"] },
{ label: "Petal", columns: ["Petal.Length", "Petal.Width"] }
]
});
```
### Auto-inferred spanners
Set `auto_span: true` to infer spanners from column names. Names are
split on the first `.` or `_`; contiguous columns sharing a prefix are
grouped, and labels are shortened to the suffix. Pass a custom regex
string (e.g., `"[.]"`) to change the separator.
```{js}
LT.build({ data: iris, auto_span: true });
```
## Row groups (separator rows)
Pass `row_group` as a string to use separator-row style:
```{js}
LT.build({
data: {
arm: ["Placebo", "Placebo", "Treatment", "Treatment"],
stat: ["n", "Mean", "n", "Mean"],
value: [30, 4.2, 31, 6.8]
},
row_group: "arm"
});
```
## Row groups (rowspan)
Pass `row_group` as an array to render as rowspan cells on the left:
```{js}
LT.build({
data: {
Region: ["East", "East", "East", "West", "West", "West"],
State: ["NY", "NY", "MA", "WA", "WA", "OR"],
City: ["New York", "Buffalo", "Boston", "Seattle", "Spokane", "Portland"],
Population: [8336817, 278349, 675647, 737015, 228989, 652503]
},
row_group: ["Region", "State"]
});
```
## Row group ordering
Reorder separator-row groups with a `group_order` op:
```{js}
LT.build({
data: {
arm: ["Placebo", "Placebo", "Treatment", "Treatment"],
stat: ["n", "Mean", "n", "Mean"],
value: ["30", "4.2", "31", "6.8"]
},
row_group: "arm",
ops: [
{ type: "group_order", order: ["Treatment", "Placebo"] }
]
});
```
## Manual row groups
Define groups explicitly by row index:
```{js}
LT.build({
data: mtcars,
ops: [
{ type: "row_group", label: "First three", rows: [1, 2, 3] },
{ type: "row_group", label: "Last three", rows: [4, 5, 6] }
]
});
```
## Row indentation
Indent specific rows to show hierarchy:
```{js}
LT.build({
data: {
label: ["Any AE", "SOC: Cardiac", "Tachycardia", "Bradycardia", "SOC: GI", "Nausea"],
n_pct: ["45 (67%)", "30 (45%)", "15 (22%)", "18 (27%)", "20 (30%)", "12 (18%)"]
},
ops: [
{ type: "indent", rows: [2, 5], level: 1 },
{ type: "indent", rows: [3, 4, 6], level: 2 }
]
});
```
## Column widths
```{js}
LT.build({
data: mtcars,
ops: [
{ type: "width", widths: { mpg: "10em", cyl: "5em", disp: "10em", hp: "8em" } }
]
});
```
## Column reordering (`move`)
Move columns to the start or after a specific column:
```{js}
LT.build({
data: iris,
ops: [
{ type: "move", columns: ["Petal.Length", "Petal.Width"], after: null }
]
});
```
## Cell styling
Apply CSS to specific cells by column and/or row:
```{js}
LT.build({
data: {
Endpoint: ["Primary", "Secondary", "Exploratory"],
HR: [0.62, 0.79, 0.91],
P: [0.001, 0.042, 0.38]
},
ops: [
{ type: "fmt_number", columns: ["HR"], decimals: 2 },
{ type: "fmt_number", columns: ["P"], decimals: 3 },
{ type: "style", columns: ["P"], rows: [1, 2], css: "font-weight:bold;color:#06c" },
{ type: "style", columns: ["HR"], rows: [1], css: "background:#e8f4e8;border-bottom:2px solid #4a4" }
]
});
```
## Conditional styling
Use `test` with a JavaScript function to apply styles based on cell
values. The function receives the raw (pre-format) value and should
return `true` to apply. Use `class` to assign CSS classes instead of
inline `css`:
```{js}
LT.build({
data: {
Endpoint: ["Primary", "Secondary", "Exploratory"],
HR: [0.62, 0.79, 1.05],
P: [0.001, 0.042, 0.38]
},
ops: [
{ type: "fmt_number", columns: ["HR", "P"], decimals: 3 },
{ type: "style", columns: ["HR"], test: v => v < 1, class: "good" },
{ type: "style", columns: ["P"], test: v => v < 0.05, css: "font-weight:bold" }
]
});
```
```{css}
.lt-table .good { color: green; }
```
### Highlighting missing values
Apply a class to all null cells across the table (omit `columns` to
target all):
```{js}
LT.build({
data: {
arm: ["Treatment", "Control", "Treatment"],
n: [30, null, 28],
response: [0.67, null, 0.71]
},
ops: [
{ type: "style", test: v => v == null, class: "na" }
]
});
```
```{css}
.lt-table .na { background: #fee; }
```
## Footnotes
Footnotes are de-duplicated by text and numbered automatically. Supported
locations: `title`, `column_labels`, `column_spanners`, `row_groups`, `body`.
```{js}
LT.build({
data: mtcars,
header: { title: "Motor Trend Cars" },
footnotes: [
{ text: "Source: 1974 Motor Trend US magazine.", location: { type: "title", group: "title" } },
{ text: "Miles per US gallon.", location: { type: "column_labels", columns: ["mpg"] } }
]
});
```
## Source notes
Notes appear below footnotes in the footer:
```{js}
LT.build({
data: mtcars,
notes: ["Data from the 1974 Motor Trend US magazine."]
});
```
## Complete example
Combining multiple features in a single spec:
```{js}
LT.build({
data: {
Group: ["Treatment", "Treatment", "Control", "Control"],
Endpoint: ["Primary", "Secondary", "Primary", "Secondary"],
Estimate: [0.6123, 0.7891, 0.4567, 0.5432],
CI_Lower: [0.4012, 0.5678, 0.2345, 0.321],
CI_Upper: [0.8234, 1.0104, 0.6789, 0.7654],
P_Value: [0.0012, 0.0456, 0.1234, 0.2345]
},
row_group: "Group",
header: {
title: "Study Results",
subtitle: "Primary and secondary endpoints"
},
spanners: [
{ label: "95% CI", columns: ["CI_Lower", "CI_Upper"] }
],
ops: [
{ type: "fmt_number", columns: ["Estimate", "CI_Lower", "CI_Upper"], decimals: 3 },
{ type: "fmt_number", columns: ["P_Value"], decimals: 4 },
{ type: "style", columns: ["P_Value"], rows: [1, 3], css: "font-weight:bold" }
],
footnotes: [
{ text: "Two-sided p-value from log-rank test.", location: { type: "column_labels", columns: ["P_Value"] } }
]
});
```
---
title: Examples
---
```{r}
library(lt)
```
## A simple table
Pass any data frame to `lt()`:
```{r}
tbl_cars = lt(head(mtcars))
tbl_cars
```
## Title and subtitle
```{r}
tbl_iris = lt(head(iris))
tbl_iris |>
lt_header(title = "Iris Measurements", subtitle = "First six observations")
```
## Column alignment
By default, numeric columns are right-aligned and character columns are
left-aligned. Override with `lt_align()`:
```{r}
tbl_cars |> lt_align(~ mpg + cyl, "center") |> lt_width(mpg = "6em")
```
## Number formatting
```{r}
tbl_cars |> lt_format(~ mpg + disp, decimals = 1)
```
### Decimal places
Control the number of decimal places with `decimals`:
```{r}
d = data.frame(
Metric = c("Revenue", "Costs", "Profit"),
Q1 = c(1234567.891, 987654.321, 246913.570),
Q2 = c(1345678.912, 1012345.678, 333333.234)
)
lt(d) |>
lt_format(~ Q1 + Q2, decimals = 2)
```
### Big mark (thousands separator)
```{r}
lt(d) |>
lt_format(~ Q1 + Q2, decimals = 0, big_mark = ",")
```
## Footnotes
```{r}
tbl_cars |>
lt_header(title = "Motor Trend Cars") |>
lt_footnote("Source: 1974 Motor Trend US magazine.", "title") |>
lt_footnote("Miles per US gallon.", "column", "mpg")
```
## Notes
Notes appear in the footer below numbered footnotes:
```{r}
tbl_cars |> lt_note("Data from the 1974 Motor Trend US magazine.")
```
## Column spanners
A spanner groups contiguous columns under a shared label:
```{r}
tbl_iris |>
lt_spanner(Sepal ~ Sepal.Length + Sepal.Width) |>
lt_spanner(Petal ~ Petal.Length + Petal.Width)
```
### Auto-inferred spanners
When column names share a common prefix separated by `.` or `_`, call
`lt_spanner()` with no arguments to infer spanners automatically:
```{r}
tbl_iris |> lt_spanner()
```
### Spanner with formatting
```{r}
tbl_iris |>
lt_header(title = "Iris Dataset") |>
lt_spanner(Sepal ~ Sepal.Length + Sepal.Width) |>
lt_spanner(Petal ~ Petal.Length + Petal.Width) |>
lt_format(~ Sepal.Length + Sepal.Width + Petal.Length + Petal.Width, decimals = 1)
```
## Row groups
### Group by column
Pass a column name to `lt_group()` to partition rows by that column's
values. The column is removed from the body and its values become group
headers:
```{r}
d = data.frame(
Region = c("East", "East", "West", "West", "West"),
City = c("New York", "Boston", "Seattle", "Portland", "Denver"),
Population = c(8336817, 675647, 737015, 652503, 715522)
)
lt(d) |>
lt_group(~ Region) |>
lt_header(title = "US Cities by Region") |>
lt_format(~ Population, big_mark = ",")
```
### Row groups with data columns
```{r}
d = data.frame(
Category = c("Fruits", "Fruits", "Vegetables", "Vegetables"),
Item = c("Apple", "Banana", "Carrot", "Broccoli"),
Calories = c(95, 105, 25, 55),
Fiber_g = c(4.4, 3.1, 2.8, 5.1)
)
lt(d) |>
lt_group(~ Category) |>
lt_header(title = "Nutritional Information") |>
lt_format(~ Fiber_g, decimals = 1)
```
### Group by multiple columns (rowspan)
Pass multiple columns to `lt_group()` to render hierarchical rowspan
cells on the left side of the table:
```{r}
d = data.frame(
Region = c("East", "East", "East", "West", "West", "West"),
State = c("NY", "NY", "MA", "WA", "WA", "OR"),
City = c("New York", "Buffalo", "Boston", "Seattle", "Spokane", "Portland"),
Population = c(8336817, 278349, 675647, 737015, 228989, 652503)
)
lt(d) |>
lt_group(~ Region + State) |>
lt_header(title = "Cities by Region and State") |>
lt_format(~ Population, big_mark = ",")
```
### Separator-row style
Use `sep = TRUE` to render groups as full-width separator rows instead of
rowspan cells:
```{r}
d = data.frame(
Region = c("East", "East", "West", "West", "West"),
City = c("New York", "Boston", "Seattle", "Portland", "Denver"),
Population = c(8336817, 675647, 737015, 652503, 715522)
)
lt(d) |>
lt_group(~ Region, sep = TRUE) |>
lt_header(title = "US Cities by Region") |>
lt_format(~ Population, big_mark = ",")
```
### Manual row groups
Use named arguments in `lt_group()` for explicit control over which rows
belong to each group:
```{r}
tbl_cars |> lt_group("First three" = 1:3, "Last three" = 4:6)
```
## Column merging
Combine columns into one using a pattern. Source columns are hidden
automatically:
```{r}
d = data.frame(
stat = c("Age", "Weight", "Height"),
n = c(67, 65, 64),
pct = c(100, 97.0, 95.5)
)
lt(d) |>
lt_format(~ pct, decimals = 1) |>
lt_merge(~ n + pct, pattern = "{1} ({2}%)") |>
lt_label(n = "n (%)")
```
### Conditional merge with `<< >>`
Wrap pattern sections in `<<` and `>>` to drop them when any referenced
column is empty/NA:
```{r}
d = data.frame(
endpoint = c("Primary", "Secondary", "Tertiary"),
est = c(0.61, 0.79, 0.45),
ci_lo = c(0.40, 0.57, NA),
ci_hi = c(0.82, 1.01, NA)
)
lt(d) |>
lt_format(~ est + ci_lo + ci_hi, decimals = 2) |>
lt_merge(~ est + ci_lo + ci_hi, pattern = "{1}<< ({2}, {3})>>") |>
lt_label(est = "Estimate (95% CI)")
```
## Row indentation
Indent the first column to show hierarchy:
```{r}
d = data.frame(
label = c("Any AE", "SOC: Cardiac", "Tachycardia", "Bradycardia",
"SOC: GI", "Nausea"),
n_pct = c("45 (67%)", "30 (45%)", "15 (22%)", "18 (27%)", "20 (30%)", "12 (18%)")
)
lt(d) |>
lt_header("Adverse Events", "Safety Population") |>
lt_indent(c(2, 5), level = 1) |>
lt_indent(c(3, 4, 6), level = 2)
```
## Substituting missing/small values
```{r}
d = data.frame(
Metric = c("HR", "p-value", "Events", "Rate"),
Value = c(0.62, 0.0003, 0, NA)
)
lt(d) |>
lt_format(~ Value, decimals = 2) |>
lt_sub(~ Value, missing = "n/a", zero = "—", small = 0.001, small_text = "< 0.001")
```
## Cell styling
Highlight specific cells with bold, color, background, or any CSS
property (camelCase or dash-case):
```{r}
d = data.frame(
Endpoint = c("Primary", "Secondary", "Exploratory"),
HR = c(0.62, 0.79, 0.91),
P = c(0.001, 0.042, 0.38)
)
lt(d) |>
lt_format(~ HR, decimals = 2) |>
lt_format(~ P, decimals = 3) |>
lt_style("P", rows = 1:2L, bold = TRUE, color = "#06c") |>
lt_style("HR", rows = 1L, bg = "#e8f4e8", borderBottom = "2px solid #4a4")
```
## Conditional styling
Apply styles based on cell values using a JavaScript test function. Use
`class` to assign CSS classes, then define the rules with `lt_css()`:
```{r}
d = data.frame(
Endpoint = c("Primary", "Secondary", "Exploratory"),
HR = c(0.62, 0.79, 1.05),
P = c(0.001, 0.042, 0.38)
)
lt(d) |>
lt_format(~ HR + P, decimals = 3) |>
lt_style("HR", test = "v => v < 1", class = "good") |>
lt_style("P", test = "v => v < 0.05", class = "sig") |>
lt_css(.good = list(color = "green"), .sig = list(fontWeight = "bold"))
```
### Highlighting NA cells
```{r}
d = data.frame(
arm = c("Treatment", "Control", "Treatment"),
n = c(30, NA, 28),
response = c(0.67, NA, NA)
)
lt(d) |>
lt_style(test = "v => v == null", class = "na") |>
lt_css(.na = list(background = "#fee"))
```
## Column widths
```{r}
tbl_cars |> lt_width(mpg = "8em", cyl = "5em", disp = "8em", hp = "6em")
```
## Column reordering
```{r}
tbl_iris |> lt_move(~ Petal.Length + Petal.Width, after = NULL)
```
## Row group ordering
```{r}
d = data.frame(
arm = c("Placebo", "Placebo", "Treatment", "Treatment"),
stat = c("n", "Mean", "n", "Mean"),
value = c("30", "4.2", "31", "6.8")
)
lt(d) |>
lt_group(~ arm, sep = TRUE) |>
lt_group("Treatment", "Placebo")
```
## Combining formatting and structure
```{r}
d = data.frame(
Group = c("Treatment", "Treatment", "Control", "Control"),
Endpoint = c("Primary", "Secondary", "Primary", "Secondary"),
Estimate = c(0.6123, 0.7891, 0.4567, 0.5432),
CI_Lower = c(0.4012, 0.5678, 0.2345, 0.3210),
CI_Upper = c(0.8234, 1.0104, 0.6789, 0.7654),
P_Value = c(0.0012, 0.0456, 0.1234, 0.2345)
)
lt(d) |>
lt_group(~ Group) |>
lt_header(
title = "Study Results",
subtitle = "Primary and secondary endpoints"
) |>
lt_spanner(`95% CI` ~ CI_Lower + CI_Upper) |>
lt_format(~ Estimate + CI_Lower + CI_Upper, decimals = 3) |>
lt_format(~ P_Value, decimals = 4) |>
lt_footnote("Two-sided p-value from log-rank test.", "column", "P_Value")
```