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 Basic table

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

LT.build({ data: mtcars });

3 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).

LT.build({
  data: {
    Metric: ["Revenue", "Costs", "Profit", "Loss"],
    Value: [1234567.891, 987654.321, 246913.57, -42.001]
  }
});

3.1 Disabling auto-format

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

3.2 Percentage detection

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

4 Title and subtitle

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

5 Column labels

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

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

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

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

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

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

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

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

11 Row groups (separator rows)

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

12 Row groups (rowspan)

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

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

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

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

16 Column widths

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

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

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

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

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

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

21 Source notes

Notes appear below footnotes in the footer:

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

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