Version 1.2.7 · Zero dependencies · ES6 · Vanilla JS
DrillDowner turns a flat array of objects into an interactive drill-down table with collapsible hierarchy, subtotals, flexible grouping, and optional flat "ledger" views — all with no dependencies and no build step required.
<link rel="stylesheet" href="dist/drilldowner.min.css">
<script src="dist/DrillDowner.min.js"></script>Or use the source files during development:
<link rel="stylesheet" href="src/drilldowner.css">
<script src="src/DrillDowner.js"></script><div id="controls"></div>
<div id="table"></div>const data = [
{ category: "Electronics", brand: "Apple", product: "iPhone 15", qty: 12, price: 999 },
{ category: "Electronics", brand: "Samsung", product: "Galaxy S24", qty: 20, price: 849 },
{ category: "Clothing", brand: "Nike", product: "Air Max 90", qty: 50, price: 120 },
];
const dd = new DrillDowner('#table', data, {
groupOrder: ["category", "brand", "product"],
totals: ["qty", "price"],
colProperties: {
qty: { label: "Stock", decimals: 0 },
price: { label: "Price", formatter: (v) => `$${DrillDowner.formatNumber(v, 2)}` }
},
controlsSelector: "#controls"
});That's it. The table renders immediately with collapsible rows and grand totals.
| Array | What it does |
|---|---|
groupOrder |
Columns that form the hierarchy, outermost first |
totals |
Numeric columns summed at every group level |
columns |
Non-numeric display columns |
| Mode | How to activate | What you get |
|---|---|---|
| Grouped | groupOrder is non-empty |
Collapsible hierarchy with subtotals |
| Ledger | groupOrder: [] + ledger defined |
Flat sorted table, one row per item |
Both modes can live in the same widget — switch via the dropdown.
colProperties customises any column independently:
colProperties: {
revenue: {
label: "Revenue (USD)",
decimals: 2,
formatter: (v) => `$${DrillDowner.formatNumber(v, 2)}`
},
status: {
togglesUp: true, // group rows show distinct child values
formatter: (v) => v === "active" ? "✅ Active" : "⏳ Pending"
}
}- Drill-down hierarchy — expand / collapse any group level
- Subtotals roll up automatically at every level
- Ledger mode — flat sorted views with custom column order
- Mixed-unit subtotals via
subTotalBy(e.g. "500 m, 1,000 pcs") - Running balance column via
balanceBehavior(bank-statement style) - Grouping dropdown — switch between hierarchy permutations and ledger views
- Breadcrumb navigation — click to collapse to any level
- A–Z quick-jump bar — vertical or horizontal alphabet navigation
- Custom formatters and renderers per column
onLabelClickcallback — open sidebars, navigate, show detail panels- Remote / server-side expansion via
remoteUrl - Method chaining —
dd.changeGroupOrder([...]).showToLevel(1) - Static helpers —
DrillDowner.formatNumber(),DrillDowner.formatDate()
const dd = new DrillDowner('#table', transactions, {
groupOrder: [],
ledger: [
// sort: ascending — oldest first, Initial Balance row at top
{ label: "Chronological", cols: ["date", "desc", "deposit", "withdrawal", "balance"], sort: ["date"] },
// sort: descending — newest first, Initial Balance row at bottom
{ label: "Newest First", cols: ["date", "desc", "deposit", "withdrawal", "balance"], sort: ["-date"] },
// calcSort/viewSort: accumulate oldest-to-newest, display newest-to-oldest
{ label: "Latest First (correct balance)", cols: ["date", "desc", "deposit", "withdrawal", "balance"],
calcSort: ["date"], viewSort: ["-date"] }
],
totals: ["deposit", "withdrawal", "balance"],
colProperties: {
date: { formatter: DrillDowner.formatDate },
balance: {
balanceBehavior: { initialBalance: 1000, add: ["deposit"], subtract: ["withdrawal"] },
formatter: (v) => `$${DrillDowner.formatNumber(v, 2)}`
}
},
controlsSelector: "#controls"
});const dd = new DrillDowner('#table', data, {
groupOrder: ["region", "rep", "customer"],
groupOrderCombinations: [
["region", "rep"],
["rep", "customer"]
],
totals: ["revenue"],
controlsSelector: "#controls"
});const dd = new DrillDowner('#table', data, {
groupOrder: ["department", "employee"],
totals: ["hours"],
onLabelClick: (ctx) => {
if (ctx.isLeaf) showSidebar(ctx.hierarchyMap);
}
});dd.dataArr.push(newRow);
dd.render();
// Replace all data
dd.dataArr = await fetch('/api/data').then(r => r.json());
dd.render();
// Method chaining
dd.changeGroupOrder(["product", "region"]).showToLevel(1);const dd = new DrillDowner(container, dataArr, options)container — CSS selector string or DOM element
dataArr — flat array of plain objects
options — all keys optional (see full API docs)
| Option | Type | Default | Description |
|---|---|---|---|
groupOrder |
string[] |
[] |
Hierarchy columns, outermost first |
totals |
string[] |
[] |
Columns to sum |
columns |
string[] |
[] |
Display-only columns |
colProperties |
Object |
{} |
Per-column label, formatter, renderer, etc. |
ledger |
Array |
[] |
Flat-view definitions |
groupOrderCombinations |
`Array[] | null` | null |
controlsSelector |
`string | null` | null |
azBarSelector |
`string | null` | null |
azBarOrientation |
string |
"vertical" |
"vertical" or "horizontal" |
showGrandTotals |
boolean |
true |
Show/hide grand totals |
onLabelClick |
`Function | null` | null |
leafRenderer |
`Function | null` | null |
remoteUrl |
`string | null` | null |
| Method | Returns | Description |
|---|---|---|
showToLevel(level) |
this |
Expand to given depth; collapse deeper rows |
collapseAll() |
this |
Collapse to top level |
expandAll() |
this |
Expand all levels |
changeGroupOrder(newOrder) |
this |
Change hierarchy and re-render |
render() |
void |
Full re-render (recalculates totals) |
getTable() |
Element |
Returns the <table> DOM node |
destroy() |
void |
Remove event listeners, empty containers |
| Method | Description |
|---|---|
DrillDowner.formatNumber(n, decimals) |
1234567.8, 2 → "1,234,567.80" |
DrillDowner.formatDate(value, includeTime) |
"2024-03-15" → "15/Mar/24" |
DrillDowner.version |
"1.2.7" |
| Property | Type | Notes |
|---|---|---|
dataArr |
Array |
Live data — mutate then call render() |
grandTotals |
Object |
Computed totals updated on every render() |
options |
Object |
Merged options — writable |
container |
Element |
Main container |
table |
Element |
The <table> element |
controls |
`Element | null` |
azBar |
`Element | null` |
examples/Simple_Example.html— minimal grouped setup withleafRenderer.examples/Medium_Example.html— grouped + ledger + controls + A–Z bar.examples/Advanced_Example.html— running balance, label click callback, and custom leaf labels.
- Full API Reference — all options, colProperties, methods, CSS classes, and examples
- Class Diagram — Mermaid class diagram with options and callback signatures
- Examples — runnable HTML demos
DrillDowner is a presentation-layer UI component designed to render rich, developer-configured HTML (links, badges, styled text) via the formatter and renderer options.
By design, DrillDowner uses .innerHTML and does not sanitize input data. It is the host application's responsibility to sanitize any user-generated content before passing it into the dataArr or returning it from a custom renderer.
Uses Intl.Collator (for natural accent and case insensitive sort with es-MX locale).
Open test/drilldowner_tests.html in a browser, or serve locally:
npx http-server . -p 8080
# then open http://localhost:8080/test/drilldowner_tests.htmlMIT — see LICENSE.
Made with care by Raúl José Santos