This document demonstrates the lt.js runtime directly from JavaScript.
Each example shows a JSON spec and renders the table with LT.build().
Add the stylesheet and script to your HTML page:
<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.
Define sample datasets as JavaScript objects (interpolated from R via
litedown’s fill option):
const mtcars = {
"mpg": [21, 21, 22.8, 21.4, 18.7, 18.1],
"cyl": [6, 6, 4, 6, 8, 6],
"disp": [160, 160, 108, 258, 360, 225],
"hp": [110, 110, 93, 110, 175, 105],
"drat": [3.9, 3.9, 3.85, 3.08, 3.15, 2.76],
"wt": [2.62, 2.875, 2.32, 3.215, 3.44, 3.46]
};
const iris = {
"Sepal.Length": [5.1, 4.9, 4.7, 4.6, 5],
"Sepal.Width": [3.5, 3, 3.2, 3.1, 3.6],
"Petal.Length": [1.4, 1.4, 1.3, 1.5, 1.4],
"Petal.Width": [0.2, 0.2, 0.2, 0.2, 0.2],
"Species": ["setosa", "setosa", "setosa", "setosa", "setosa"]
};
The simplest spec: just data (a column-oriented object).
LT.build({ data: mtcars });
LT.build({
data: iris,
header: {
title: "Iris Measurements",
subtitle: "First five observations"
}
});
By default, column names are cleaned for display by replacing . and
_ with spaces. Set auto_label: false to show raw names, or use a
label op to override specific columns:
// auto_label on by default: "Sepal.Length" displays as "Sepal Length"
LT.build({ data: iris });
// Disable auto-labeling
LT.build({ data: iris, auto_label: false });
// Explicit label overrides
LT.build({
data: iris,
ops: [
{ type: "label", labels: {
"Sepal.Length": "Length (cm)", "Sepal.Width": "Width (cm)",
"Petal.Length": "Length (cm)", "Petal.Width": "Width (cm)"
}}
]
});
By default, numeric columns are right-aligned. Override with an align op:
LT.build({
data: mtcars,
ops: [
{ type: "align", columns: ["mpg", "cyl"], align: "center" }
]
});
Numeric columns are automatically formatted. The following example shows
all behaviors at once: rounding to ~4 significant digits (adapting
decimal places to magnitude), thin-space thousands separator,
typographic minus (U+2212) for negatives, percentage detection (column
names matching /%|[ _](pct|percent)$/i are multiplied by 100 and
suffixed with “%”), and year-column skipping (columns matching
/year/i with 4-digit integers are left untouched).
LT.build({
data: {
Year: [2020, 2021, 2022],
Revenue: [1234567.89, 2345678.12, -3456.789],
Margin_pct: [0.1234, 0.0567, 0.2345]
}
});
Set auto_format: false to show raw values. Compare with the table above:
LT.build({
data: {
Year: [2020, 2021, 2022],
Revenue: [1234567.89, 2345678.12, -3456.789],
Margin_pct: [0.1234, 0.0567, 0.2345]
},
auto_format: false
});
fmt_number)Explicit control over decimals and thousands separators:
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 }
]
});
Use percent: "%" when values are already in percent scale (no *100):
LT.build({
data: {
Test: ["A", "B"],
Rate: [78.5, 42.1]
},
ops: [
{ type: "fmt_number", columns: ["Rate"], decimals: 1, percent: "%" }
]
});
Use prefix and suffix to prepend or append symbols:
LT.build({
data: {
Item: ["Widget", "Gadget", "Doohickey"],
Price: [1234.5, 678.9, 42.0],
Weight: [1250, 340.5, 89.12]
},
ops: [
{ type: "fmt_number", columns: ["Price"], decimals: 2, big_mark: ",", prefix: "$" },
{ type: "fmt_number", columns: ["Weight"], decimals: 1, suffix: " kg" }
]
});
fmt_date)Format date values using JavaScript’s native Date methods. Values can be
Date objects or ISO strings. The default calls toLocaleDateString();
pass method to use a different Date method, or locale/options for
Intl.DateTimeFormat control.
Note: The formatted date may differ from the input depending on the
viewer’s timezone. new Date("2024-01-15") is parsed as UTC midnight,
but toLocaleDateString() converts to local time — so 2024-01-15
displays as 2024-01-14 at GMT-6. Pass options: { timeZone: "UTC" }
to avoid this.
LT.build({
data: {
Event: ["Launch", "Update", "Release"],
Date: [new Date("2024-01-15"), new Date("2024-06-30"), new Date("2024-12-01")]
},
ops: [
{ type: "fmt_date", columns: ["Date"] }
]
});
LT.build({
data: {
Event: ["Launch", "Update"],
Date: [new Date("2024-01-15"), new Date("2024-06-30")]
},
ops: [
{ type: "fmt_date", columns: ["Date"], locale: "zh-CN",
options: { year: "numeric", month: "long", day: "numeric",
weekday: "long", timeZone: "UTC" } }
]
});
LT.build({
data: {
Event: ["Launch", "Update"],
Date: [new Date("2024-01-15"), new Date("2024-06-30")]
},
ops: [
{ type: "fmt_date", columns: ["Date"], method: "toDateString" }
]
});
For datetime values, include time components in options:
LT.build({
data: {
Event: ["Start", "End"],
Time: [new Date("2024-01-15T09:30:00Z"), new Date("2024-01-15T17:45:00Z")]
},
ops: [
{ type: "fmt_date", columns: ["Time"], locale: "en-US",
options: { year: "numeric", month: "short", day: "numeric",
hour: "2-digit", minute: "2-digit" } }
]
});
sub)Replace missing, zero, or small values with display text:
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" }
]
});
Inf and -Inf (JSON: use Infinity/-Infinity — but since JSON doesn’t
support Infinity, pass them as a JS expression):
LT.build({
data: {
Metric: ["Lower bound", "Upper bound", "Rate", "Missing"],
Value: [-Infinity, Infinity, 1.5, null]
},
ops: [
{ type: "fmt_number", columns: ["Value"], decimals: 2 }
]
});
merge)Combine multiple columns into one display column. The pattern uses
{1}, {2}, etc. for column positions. Wrap sections in << >> for
conditional NA handling.
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)" } }
]
});
Group contiguous columns under a shared header:
LT.build({
data: iris,
spanners: [
{ label: "Sepal", columns: ["Sepal.Length", "Sepal.Width"] },
{ label: "Petal", columns: ["Petal.Length", "Petal.Width"] }
]
});
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.
LT.build({ data: iris, auto_span: true });
LT.build({
data: mtcars,
ops: [
{ type: "width", widths: { mpg: "10em", cyl: "5em", disp: "10em", hp: "8em" } }
]
});
Set table on a width op to size the whole table. It can be combined
with column widths:
LT.build({
data: iris,
ops: [
{ type: "width", table: "80%" }
]
});
LT.build({
data: iris,
ops: [
{ type: "width", table: "100%", widths: { Species: "20em" } }
]
});
move)Move columns to the start or after a specific column:
LT.build({
data: iris,
ops: [
{ type: "move", columns: ["Petal.Length", "Petal.Width"], after: null }
]
});
Pass row_group as a single-element array to group by that column’s
values (rendered as rowspan cells on the left by default):
LT.build({
data: {
Region: ["East", "East", "West", "West", "West"],
City: ["New York", "Boston", "Seattle", "Portland", "Denver"],
Population: [8336817, 675647, 737015, 652503, 715522]
},
row_group: ["Region"]
});
When row_group is a single-element array and the column is character
with any value longer than 20 characters, separator rows are used
automatically. Set auto_sep: false to force rowspan mode:
LT.build({
data: {
Department: ["Cardiovascular Research", "Cardiovascular Research",
"Biostatistics", "Biostatistics"],
Trial: ["ATLAS-2", "BEACON", "PRIME-1", "NEXUS"],
N: [450, 320, 280, 510]
},
row_group: ["Department"]
});
// Force rowspan mode by disabling auto-detection
LT.build({
data: {
Department: ["Cardiovascular Research", "Cardiovascular Research",
"Biostatistics", "Biostatistics"],
Trial: ["ATLAS-2", "BEACON", "PRIME-1", "NEXUS"],
N: [450, 320, 280, 510]
},
row_group: ["Department"],
auto_sep: false
});
Pass multiple columns to render hierarchical rowspan cells:
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"]
});
Pass row_group as a string to always use separator rows:
LT.build({
data: {
arm: ["Placebo", "Placebo", "Treatment", "Treatment"],
stat: ["n", "Mean", "n", "Mean"],
value: [30, 4.2, 31, 6.8]
},
row_group: "arm"
});
Reorder separator-row groups with a group_order op:
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"] }
]
});
Define groups explicitly by row index:
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] }
]
});
Indent specific rows to show hierarchy:
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 }
]
});
Apply CSS to specific cells by column and/or row:
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" }
]
});
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:
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" }
]
});
.lt-table .good { color: green; }
Apply a class to all null cells across the table (omit columns to
target all):
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" }
]
});
.lt-table .na { background: #fee; }
By default every cell value and text field is HTML-escaped, so characters
like <, >, and & render as literal text. Two mechanisms opt into raw
HTML:
html_cols to true (all columns) or an array of
column names. Cells in those columns are emitted verbatim.["<b>x</b>"] is emitted as raw HTML. (This is what the
R side’s I() wrapper serializes to.)LT.build({
data: {
Package: [
"<a href='https://cran.r-project.org/package=lt'>lt</a>",
"<a href='https://cran.r-project.org/package=xfun'>xfun</a>"
],
Status: ["<b style='color:#090'>stable</b>", "<i>active</i>"]
},
html_cols: ["Package", "Status"],
header: { title: ["Packages <sup>★</sup>"] },
ops: [
{ type: "label", labels: { Package: ["Name<sup>*</sup>"] } }
],
notes: [["See <a href='https://yihui.org/'>the author</a>."]]
});
Footnotes are de-duplicated by text and numbered automatically. Supported
locations: title, column_labels, column_spanners, row_groups, body.
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"] } }
]
});
Notes appear below footnotes in the footer:
LT.build({
data: mtcars,
notes: ["Data from the 1974 Motor Trend US magazine."]
});
Combining multiple features in a single spec:
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"] } }
]
});
When cell values are formatted, the original values are available in
tooltips (hover over a cell to see). You can also Alt + Click on a
table to reveal all raw values at once, or Alt + Double-Click to
toggle raw values for all tables on the page.