lt.js — JavaScript API Reference

This document demonstrates the lt.js runtime directly from JavaScript. Each example shows a JSON spec and renders the table with LT.build().

1 Including lt.js

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"]
};

2 Basics

2.1 Basic table

The simplest spec: just data (a column-oriented object).

LT.build({ data: mtcars });

2.2 Title and subtitle

LT.build({
  data: iris,
  header: {
    title: "Iris Measurements",
    subtitle: "First five observations"
  }
});

2.3 Column labels

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)"
    }}
  ]
});

2.4 Column alignment

By default, numeric columns are right-aligned. Override with an align op:

LT.build({
  data: mtcars,
  ops: [
    { type: "align", columns: ["mpg", "cyl"], align: "center" }
  ]
});

3 Formatting

3.1 Auto-formatting

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]
  }
});

3.1.1 Disabling auto-format

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
});

3.2 Number formatting (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 }
  ]
});

3.2.1 Percent without multiplication

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: "%" }
  ]
});

3.2.2 Prefix and suffix (currency, units)

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" }
  ]
});

3.3 Date formatting (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"] }
  ]
});

3.3.1 With locale and options

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" } }
  ]
});

3.3.2 Using a specific Date method

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" }
  ]
});

3.3.3 Datetime values

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" } }
  ]
});

3.4 Value substitution (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" }
  ]
});

3.5 Infinity values

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 }
  ]
});

4 Column structure

4.1 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.

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)" } }
  ]
});

4.2 Column spanners

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"] }
  ]
});

4.2.1 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.

LT.build({ data: iris, auto_span: true });

4.3 Column widths

LT.build({
  data: mtcars,
  ops: [
    { type: "width", widths: { mpg: "10em", cyl: "5em", disp: "10em", hp: "8em" } }
  ]
});

4.4 Table width

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" } }
  ]
});

4.5 Column reordering (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 }
  ]
});

5 Row groups

5.1 Group by column

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"]
});

5.2 Auto separator rows for long labels

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
});

5.3 Group by multiple columns (rowspan)

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"]
});

5.4 Separator-row style

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"
});

5.5 Row group ordering

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"] }
  ]
});

5.6 Manual row groups

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] }
  ]
});

5.7 Row indentation

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 }
  ]
});

6 Styling

6.1 Cell styling

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" }
  ]
});

6.2 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:

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; }

6.2.1 Highlighting missing values

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; }

7 HTML content

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:

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>&#9733;</sup>"] },
  ops: [
    { type: "label", labels: { Package: ["Name<sup>*</sup>"] } }
  ],
  notes: [["See <a href='https://yihui.org/'>the author</a>."]]
});

8 Footnotes and notes

8.1 Footnotes

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"] } }
  ]
});

8.2 Source notes

Notes appear below footnotes in the footer:

LT.build({
  data: mtcars,
  notes: ["Data from the 1974 Motor Trend US magazine."]
});

9 Complete example

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"] } }
  ]
});

10 Interactivity

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.