Quick start guide to rtflite¶
Overview¶
rtflite is a Python package for creating production-ready tables and figures in RTF format. The package is designed to:
- Provide simple Python classes that map to table elements (title, headers, body, footnotes) for intuitive table construction.
- Offer a canonical Python API with a clear, composable interface.
- Focus exclusively on table formatting and layout, leaving data manipulation to dataframe libraries like polars or pandas.
- Minimize external dependencies for maximum portability and reliability.
Creating an RTF table involves three steps:
- Design the desired table layout and structure.
- Configure the appropriate rtflite components.
- Generate and save the RTF document.
This guide introduces rtflite's core components and demonstrates how to turn dataframes into Tables, Listings, and Figures (TLFs) for clinical reporting.
Data: adverse events¶
To explore the RTF generation capabilities in rtflite, we will use the
dataset adae.parquet
. This dataset contains adverse events (AE) information
from a clinical trial.
Below is the meaning of relevant variables.
USUBJID
: Unique Subject IdentifierTRTA
: Actual TreatmentAEDECOD
: Dictionary-Derived Term
# Load adverse events data
data_path = files("rtflite.data").joinpath("adae.parquet")
df = pl.read_parquet(data_path)
df.select(["USUBJID", "TRTA", "AEDECOD"]).head(4)
Table-ready data¶
We use polars for data manipulation to create a dataframe with the information we want to render in an RTF table.
Note
Other dataframe packages can also be used for the same purpose.
In this AE example, we provide the number of subjects with each type of AE by treatment group.
tbl = (
df.group_by(["TRTA", "AEDECOD"])
.agg(pl.len().alias("n"))
.sort("TRTA")
.pivot(values="n", index="AEDECOD", on="TRTA")
.fill_null(0)
.sort("AEDECOD") # Sort by adverse event name to match R output
)
print(tbl.head(4))
shape: (4, 4)
┌──────────────────────┬─────────┬──────────────────────┬─────────────────────┐
│ AEDECOD ┆ Placebo ┆ Xanomeline High Dose ┆ Xanomeline Low Dose │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ u32 ┆ u32 ┆ u32 │
╞══════════════════════╪═════════╪══════════════════════╪═════════════════════╡
│ ABDOMINAL DISCOMFORT ┆ 0 ┆ 1 ┆ 0 │
│ ABDOMINAL PAIN ┆ 1 ┆ 2 ┆ 3 │
│ ACROCHORDON EXCISION ┆ 0 ┆ 1 ┆ 0 │
│ ACTINIC KERATOSIS ┆ 0 ┆ 1 ┆ 0 │
└──────────────────────┴─────────┴──────────────────────┴─────────────────────┘
Table component classes¶
rtflite provides dedicated classes for each table component. Commonly used classes include:
RTFPage
: RTF page information (orientation, margins, pagination).RTFPageHeader
: Page headers with page numbering (compatible with r2rtf).RTFPageFooter
: Page footers for attribution and notices.RTFTitle
: RTF title information.RTFColumnHeader
: RTF column header information.RTFBody
: RTF table body information.RTFFootnote
: RTF footnote information.RTFSource
: RTF data source information.
These component classes work together to build complete RTF documents. A full list of all classes and their parameters can be found in the API reference.
Simple example¶
A minimal example below illustrates how to combine components to create an RTF table.
RTFBody()
defines table body layout.RTFDocument()
transfers table layout information into RTF syntax.write_rtf()
saves encoded RTF into a.rtf
file.
# Create simple RTF document
doc = rtf.RTFDocument(
df=tbl.head(6),
rtf_body=rtf.RTFBody(), # Step 1: Add table attributes
)
# Step 2 & 3: Convert to RTF and write to file
doc.write_rtf("intro-ae1.rtf")
Column width¶
If we want to adjust the width of each column to provide more space to the first column,
this can be achieved by updating col_rel_width
in RTFBody
.
The input of col_rel_width
is a list with same length for number of columns.
This argument defines the relative length of each column within a pre-defined total column width.
In this example, the defined relative width is 3:2:2:2.
Only the ratio of col_rel_width
is used.
Therefore it is equivalent to use col_rel_width = [6,4,4,4]
or col_rel_width = [1.5,1,1,1]
.
# Create RTF document with custom column widths
doc = rtf.RTFDocument(
df=tbl.head(6),
rtf_body=rtf.RTFBody(
col_rel_width=[3, 2, 2, 2] # Define relative width
),
)
doc.write_rtf("intro-ae2.rtf")
Column headers¶
In RTFColumnHeader
, the text
argument provides the column header content
as a list of strings.
# Create RTF document with column headers
doc = rtf.RTFDocument(
df=tbl.head(6),
rtf_column_header=rtf.RTFColumnHeader(
text=[
"Adverse Events",
"Placebo",
"Xanomeline High Dose",
"Xanomeline Low Dose",
],
),
rtf_body=rtf.RTFBody(col_rel_width=[3, 2, 2, 2]),
)
doc.write_rtf("intro-ae3.rtf")
We also allow column headers be displayed in multiple lines.
If an empty column name is needed for a column,
you can insert an empty string. For example, ["name 1", "", "name 3"]
.
In RTFColumnHeader
, the col_rel_width
can be used to align column header
with different number of columns.
By using RTFColumnHeader
with col_rel_width
, one can customize complicated column headers.
If there are multiple pages, column header will repeat at each page by default.
# Create RTF document with multi-line column headers
doc = rtf.RTFDocument(
df=tbl.head(50),
rtf_page=rtf.RTFPage(nrow=15),
rtf_column_header=[
rtf.RTFColumnHeader(text=[" ", "Treatment"], col_rel_width=[3, 3]),
rtf.RTFColumnHeader(
text=[
"Adverse Events",
"Placebo",
"Xanomeline High Dose",
"Xanomeline Low Dose",
],
col_rel_width=[3, 1, 1, 1],
),
],
rtf_body=rtf.RTFBody(col_rel_width=[3, 1, 1, 1]),
)
doc.write_rtf("intro-ae4.rtf")
Titles, footnotes, and data source¶
RTF documents can include additional components to provide context and documentation:
RTFTitle
: Add document titles and subtitlesRTFFootnote
: Add explanatory footnotesRTFSource
: Add data source attribution
# Create RTF document with titles, footnotes, and source
doc = rtf.RTFDocument(
df=tbl.head(15),
rtf_title=rtf.RTFTitle(
text=["Summary of Adverse Events by Treatment Group", "Safety Analysis Set"]
),
rtf_column_header=rtf.RTFColumnHeader(
text=[
"Adverse Events",
"Placebo\\line (N=86)",
"Xanomeline High Dose\\line (N=84)",
"Xanomeline Low Dose\\line (N=84)",
],
),
rtf_body=rtf.RTFBody(col_rel_width=[3, 2, 2, 2]),
rtf_footnote=rtf.RTFFootnote(
text=[
"Adverse events are coded using MedDRA version 25.0.",
"Events are sorted alphabetically by preferred term.",
]
),
rtf_source=rtf.RTFSource(text="Source: ADAE dataset, Data cutoff: 01JAN2023"),
)
doc.write_rtf("intro-ae5.rtf")
Note the use of \\line
in column headers to create line breaks within cells.
Text formatting and alignment¶
rtflite supports various text formatting options:
- Text formatting: Bold (
b
), italic (i
), underline (u
), strikethrough (s
) - Text alignment: Left (
l
), center (c
), right (r
), justify (j
) - Font properties: Font size, font family
# Create RTF document with text formatting and alignment
doc = rtf.RTFDocument(
df=tbl.head(10),
rtf_column_header=rtf.RTFColumnHeader(
text=[
"Adverse Events",
"Placebo",
"Xanomeline High Dose",
"Xanomeline Low Dose",
],
text_format="b", # Bold headers
text_justification=["l", "c", "c", "c"],
),
rtf_body=rtf.RTFBody(
col_rel_width=[3, 1, 1, 1],
text_justification=["l", "c", "c", "c"],
),
)
doc.write_rtf("intro-ae6.rtf")
Text conversion control¶
rtflite supports LaTeX-style text conversion for mathematical symbols and formatting.
By default, text conversion is enabled for titles and data content, but can be controlled with the text_convert
parameter.
Text conversion examples¶
When text_convert = True
(default for titles and data):
- \\alpha
converts to \u03b1 (Greek alpha)
- \\beta
converts to \u03b2 (Greek beta)
- a_b
converts to subscript format (a subscript b)
When text_convert = False
:
- LaTeX patterns like a_b
remain unchanged as literal text
- Underscores stay as underscores: a_b
displays as a_b
# Example showing text_convert behavior with subscript patterns
# Create example data with underscore patterns
data_with_underscores = pl.DataFrame(
{
"Parameter": ["x_max", "y_min", "z_avg", "a_b_ratio"],
"Value": [15.2, 8.7, 12.1, 0.85],
"Unit": ["mg/L", "cm", "C", "ratio"],
}
)
doc_converted = rtf.RTFDocument(
df=data_with_underscores,
rtf_title=rtf.RTFTitle(text="Study Parameters with Text Conversion Enabled"),
rtf_column_header=rtf.RTFColumnHeader(
text=["Parameter", "Value", "Unit"],
),
rtf_body=rtf.RTFBody(
col_rel_width=[2, 1, 1],
),
rtf_footnote=rtf.RTFFootnote(
text="Note: Underscores x_max and y_min in footnote as is",
text_convert=False, # Keep footnote text as-is
),
)
doc_converted.write_rtf("text-convert.rtf")
Key points about text conversion
- Default behavior:
text_convert = True
for all components (titles, data, footnotes, sources). - Underscore patterns:
a_b
becomes subscript when conversion is enabled. - LaTeX symbols:
\\alpha
,\\beta
, etc. convert to Unicode symbols. - Control per component: Each RTF component can have independent conversion settings.
- Performance: Disabling conversion can improve performance for large tables with no LaTeX content.
Border customization¶
Table borders can be customized extensively:
- Border styles:
single
,double
,thick
,dotted
,dashed
- Border sides:
border_top
,border_bottom
,border_left
,border_right
- Page borders:
border_first
,border_last
for first/last rows across pages
# Create RTF document with custom borders
doc = rtf.RTFDocument(
df=tbl.head(8),
rtf_column_header=rtf.RTFColumnHeader(
text=[
"Adverse Events",
"Placebo",
"Xanomeline High Dose",
"Xanomeline Low Dose",
],
border_bottom=["single", "double", "single", "single"],
),
rtf_body=rtf.RTFBody(
col_rel_width=[3, 2, 2, 2],
border_left=["single", "", "", ""],
),
)
doc.write_rtf("intro-ae7.rtf")
Page headers and footers¶
RTF documents can include page headers and footers that appear on every page, positioned outside the main content area (compatible with r2rtf):
RTFPageHeader
: Add headers with page numbering and custom textRTFPageFooter
: Add footers with attribution or confidentiality notices
# Create RTF document with page headers and footers
doc = rtf.RTFDocument(
df=tbl.head(15),
rtf_page_header=rtf.RTFPageHeader(
# Default: "Page \chpgn of {\field{\*\fldinst NUMPAGES }}"
# Uses r2rtf-compatible RTF field codes
),
rtf_page_footer=rtf.RTFPageFooter(text="Confidential - Clinical Study Report"),
rtf_title=rtf.RTFTitle(
text=[
"Summary of Adverse Events by Treatment Group",
"With Page Headers and Footers",
]
),
rtf_column_header=rtf.RTFColumnHeader(
text=[
"Adverse Events",
"Placebo (N=86)",
"Xanomeline High Dose (N=84)",
"Xanomeline Low Dose (N=84)",
],
),
rtf_body=rtf.RTFBody(col_rel_width=[3, 2, 2, 2]),
)
doc.write_rtf("intro-ae8.rtf")
Custom header and footer formatting¶
Headers and footers support full text formatting including custom alignment, font sizes, and styling:
# Create RTF document with custom formatted headers and footers
doc = rtf.RTFDocument(
df=tbl.head(10),
rtf_page_header=rtf.RTFPageHeader(
text="Study XYZ-123 | Page \\chpgn",
text_font_size=10,
text_justification="c", # Center aligned
text_format="b", # Bold
),
rtf_page_footer=rtf.RTFPageFooter(
text=["Company Confidential"],
text_font_size=8,
text_justification="l", # Left aligned
),
rtf_title=rtf.RTFTitle(text="Adverse Events with Custom Headers/Footers"),
rtf_column_header=rtf.RTFColumnHeader(
text=[
"Adverse Events",
"Placebo",
"Xanomeline High Dose",
"Xanomeline Low Dose",
],
),
rtf_body=rtf.RTFBody(col_rel_width=[3, 2, 2, 2]),
)
doc.write_rtf("intro-ae8b.rtf")
Page layout and orientation¶
RTFPage
provides control over page layout:
- Orientation:
portrait
orlandscape
- Page size: Custom width and height
- Margins: Left, right, top, bottom, header, footer margins
- Rows per page: Control pagination with
nrow
# Create RTF document with landscape layout
doc = rtf.RTFDocument(
df=tbl.head(20),
rtf_page=rtf.RTFPage(
orientation="landscape", # Landscape for wider tables
nrow=10,
border_first="dashed", # Dash border for first/last pages
border_last="dashed",
),
rtf_title=rtf.RTFTitle(text="Adverse Events Summary - Landscape Layout"),
rtf_column_header=rtf.RTFColumnHeader(
text=[
"Adverse Events",
"Placebo (N=86)",
"Xanomeline High Dose (N=84)",
"Xanomeline Low Dose (N=84)",
],
),
rtf_body=rtf.RTFBody(col_rel_width=[4, 2, 2, 2]),
)
doc.write_rtf("intro-ae10.rtf")
Cell-level formatting¶
Using the BroadcastValue pattern, you can apply formatting to individual cells:
# Example of cell-level border control for specific cells
from rtflite.attributes import BroadcastValue
# Create custom border patterns
border_pattern = [
["single", "", "single", ""], # Row 1: borders on columns 1 and 3
["", "double", "", "double"], # Row 2: borders on columns 2 and 4
["single", "single", "single", "single"], # Row 3: borders on all columns
]
doc = rtf.RTFDocument(
df=tbl.head(3),
rtf_column_header=rtf.RTFColumnHeader(
text=[
"Adverse Events",
"Placebo",
"Xanomeline High Dose",
"Xanomeline Low Dose",
],
),
rtf_body=rtf.RTFBody(
col_rel_width=[3, 2, 2, 2],
border_top=border_pattern, # Apply custom border pattern
),
)
doc.write_rtf("intro-ae11.rtf")
Multi-page considerations
For large tables spanning multiple pages, rtflite handles:
- Automatic page breaks based on
nrow
setting - Column header repetition on each page
- Consistent border styling across page boundaries
- Proper footnote and source placement
# Large table with consistent formatting across pages
doc = rtf.RTFDocument(
df=tbl.head(50),
rtf_page=rtf.RTFPage(
nrow=15,
),
rtf_title=rtf.RTFTitle(
text=[
"Complete Adverse Events Summary",
"All Treatment Groups - Multi-page Table",
]
),
rtf_column_header=rtf.RTFColumnHeader(
text=[
"Adverse Events",
"Placebo\\line (N=86)",
"Xanomeline High Dose\\line (N=84)",
"Xanomeline Low Dose\\line (N=84)",
],
text_format="b",
),
rtf_body=rtf.RTFBody(
col_rel_width=[3, 1, 1, 1],
text_justification=["l", "c", "c", "c"],
),
rtf_footnote=rtf.RTFFootnote(
text=[
"MedDRA version 25.0 coding applied.",
"Table includes all reported adverse events regardless of relationship to study drug.",
"Events sorted alphabetically by preferred term.",
]
),
rtf_source=rtf.RTFSource(text="Dataset: ADAE | Cutoff: 01JAN2023"),
)
doc.write_rtf("intro-ae12.rtf")