fmt_number)
sub)merge)move)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 });
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).
LT.build({
data: {
Metric: ["Revenue", "Costs", "Profit", "Loss"],
Value: [1234567.891, 987654.321, 246913.57, -42.001]
}
});
Set auto_fmt: false to show raw values. Compare with the table above:
LT.build({
data: {
Metric: ["Revenue", "Costs", "Profit", "Loss"],
Value: [1234567.891, 987654.321, 246913.57, -42.001]
},
auto_fmt: false
});
Columns whose label matches /%|[ _](pct|percent)$/i are auto-detected
as percentages: values are multiplied by 100 and suffixed with “%”.
LT.build({
data: {
Endpoint: ["Primary", "Secondary"],
success_pct: [0.7823, 0.4512]
}
});
LT.build({
data: iris,
header: {
title: "Iris Measurements",
subtitle: "First five observations"
}
});
Rename column headers without modifying the data keys:
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" }
]
});
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: "%" }
]
});
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" }
]
});
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 });
Pass row_group as a string to use separator-row style:
LT.build({
data: {
arm: ["Placebo", "Placebo", "Treatment", "Treatment"],
stat: ["n", "Mean", "n", "Mean"],
value: [30, 4.2, 31, 6.8]
},
row_group: "arm"
});
Pass row_group as an array to render as rowspan cells on the left:
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"]
});
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 }
]
});
LT.build({
data: mtcars,
ops: [
{ type: "width", widths: { mpg: "10em", cyl: "5em", disp: "10em", hp: "8em" } }
]
});
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 }
]
});
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; }
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"] } }
]
});