Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion public/demo/nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
{ path: 'find-in-table/index.html', label: '12. Find in table' },
{ path: 'matrix/index.html', label: '13. Matrix fixture' },
{ path: 'copy-as-csv/index.html', label: '14. Copy as CSV' },
{ path: 'twin-table/index.html', label: '15. Twin table (investigation)' }
{ path: 'twin-table/index.html', label: '15. Twin table (investigation)' },
{ path: 'sliders/merged-headers.html', label: '16. Merged headers' }
];

var STYLE_ID = 'gs-demo-nav-styles';
Expand Down
104 changes: 104 additions & 0 deletions public/demo/sliders/merged-headers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Grid-Sight · Slider · Merged / banner headers</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 960px; margin: 24px auto; padding: 0 16px; color: #222; }
h1 { margin-top: 0; }
h2 { margin-top: 32px; }
table { border-collapse: collapse; margin-top: 8px; }
th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: right; font-variant-numeric: tabular-nums; }
thead th { background: #f4f4f4; }
/* The merged banner cell reads as a title row. */
thead th[colspan] { text-align: center; font-weight: 600; background: #eef2f7; }
p.hint { color: #555; }
code { background: #f4f4f4; padding: 1px 4px; border-radius: 3px; }
</style>
</head>
<body>
<nav data-gs-demo-nav></nav>

<h1>Slider · Merged / banner headers</h1>
<p class="hint">
Speed-vs-length matrix tables often carry a merged <strong>&ldquo;Length Overall (m)&rdquo;</strong>
banner above the numeric column headers. Grid-Sight peels off the banner and
binds interpolation to the <em>leaf</em> header row, so both author
permutations below offer the sliders. Click the <strong>GS</strong> toggle,
open the <strong>+</strong> on the top-left corner cell, choose
<strong>Sliders</strong>, then drag either axis to interpolate — the
bracketing four cells highlight live.
</p>

<h2>Permutation 1 — full-width <code>colspan</code> banner</h2>
<p class="hint">
A single merged cell spans the whole header; the leaf row still carries the
<strong>Speed Knots</strong> corner alongside the length headers.
</p>
<table id="perm1-colspan-banner"></table>

<h2>Permutation 2 — <code>rowspan</code> corner + <code>colspan</code> banner</h2>
<p class="hint">
<strong>Speed Knots</strong> spans both header rows, so the leaf row is
<em>all</em> length headers (no corner cell) with its cells shifted right
under the rowspan. This is the case that previously blocked interpolation.
</p>
<table id="perm2-rowspan-corner"></table>

<script>
// Build both matrices BEFORE the bundle loads so Grid-Sight sees a complete
// table at init time. Lengths mirror the reported pattern (10→50 by 10, then
// to 90 by 20); speeds run 10→90 by 10. Cell = smooth f(speed, length) so
// interpolation + heatmap read sensibly.
(function () {
var LENGTHS = [10, 20, 30, 40, 50, 70, 90];
var SPEEDS = [10, 20, 30, 40, 50, 60, 70, 80, 90];
function cell(speed, len) {
return (len * 0.12 + speed * 0.05).toFixed(1);
}

function bodyRows() {
var out = '';
for (var i = 0; i < SPEEDS.length; i++) {
var s = SPEEDS[i];
out += '<tr><th>' + s + '</th>';
for (var j = 0; j < LENGTHS.length; j++) {
out += '<td>' + cell(s, LENGTHS[j]) + '</td>';
}
out += '</tr>';
}
return out;
}

function leafHeaderCells() {
var out = '';
for (var j = 0; j < LENGTHS.length; j++) out += '<th>' + LENGTHS[j] + '</th>';
return out;
}

// Permutation 1: banner spans the corner + every length column.
document.getElementById('perm1-colspan-banner').innerHTML =
'<thead>' +
'<tr><th colspan="' + (LENGTHS.length + 1) + '">Length Overall (m)</th></tr>' +
'<tr><th>Speed Knots</th>' + leafHeaderCells() + '</tr>' +
'</thead>' +
'<tbody>' + bodyRows() + '</tbody>';

// Permutation 2: rowspan corner beside a banner spanning only the lengths.
document.getElementById('perm2-rowspan-corner').innerHTML =
'<thead>' +
'<tr><th rowspan="2">Speed Knots</th>' +
'<th colspan="' + LENGTHS.length + '">Length Overall (m)</th></tr>' +
'<tr>' + leafHeaderCells() + '</tr>' +
'</thead>' +
'<tbody>' + bodyRows() + '</tbody>';
})();

// Offer heatmap + sliders + statistics on this page.
window.gridSight = { pageConfig: { enrichments: ['heatmap', 'sliders', 'statistics'] } };
</script>
<script src="../../grid-sight.iife.js"></script>
<script src="../nav.js"></script>
</body>
</html>
5 changes: 5 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ <h2>Read between the rows</h2>
<p class="demo-links">
<a href="demo/sliders/interpolation.html">Interpolation demo →</a>
<a href="demo/sliders/alternate-calc-models.html">Calc models →</a>
<a href="demo/sliders/merged-headers.html">Merged headers →</a>
</p>
</div>
<div class="feature__demo">
Expand Down Expand Up @@ -399,6 +400,10 @@ <h3>Alternate calc models</h3>
<h3>Persistent URL</h3>
<p>Slider positions encoded in the URL fragment — bookmark, share, or reload at a value.</p>
</a>
<a class="demo-card" href="demo/sliders/merged-headers.html">
<h3>Merged headers</h3>
<p>Interpolation on a speed-vs-length matrix with a merged banner header — colspan banner and rowspan corner.</p>
</a>
<a class="demo-card" href="demo/toggle/live-enrichments.html">
<h3>Live toggles — all on</h3>
<p>Pre-ticked enrichment panel: untick any feature and its lozenges vanish within a frame.</p>
Expand Down
79 changes: 79 additions & 0 deletions src/core/__tests__/table-grid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
isVirtualColumn,
gridRows,
headerRow,
headerRows,
bodyRows,
gridCells,
sourceCells,
Expand All @@ -23,6 +24,7 @@ import {
cellAt,
columnCells,
headerCellFor,
dataHeaderCells,
logicalColIndexOf,
logicalRowIndexOf,
cellValue,
Expand Down Expand Up @@ -124,6 +126,83 @@ describe('row access', () => {
});
});

/* ── Merged / banner headers ────────────────────────────────────────── */

describe('merged banner headers', () => {
it('picks the leaf header row past a full-width banner (permutation 1)', () => {
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr><th colspan="4">Length Overall (m)</th></tr>
<tr><th>Speed Knots</th><th>10</th><th>20</th><th>30</th></tr>
</thead>
<tbody>
<tr><th>10</th><td>1</td><td>2</td><td>3</td></tr>
<tr><th>20</th><td>4</td><td>5</td><td>6</td></tr>
</tbody>
`;
document.body.appendChild(table);
expect(headerRow(table)!.cells[0].textContent).toBe('Speed Knots');
expect(headerRows(table)).toHaveLength(2);
expect(bodyRows(table)).toHaveLength(2);
// Column headers drop the corner label, keep the numeric leaf headers.
expect(dataHeaderCells(table).map((c) => c.textContent)).toEqual([
'10',
'20',
'30',
]);
});

it('resolves a rowspan corner + banner: leaf cells are shifted right (permutation 2)', () => {
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr><th rowspan="2">Speed Knots</th><th colspan="3">Length Overall (m)</th></tr>
<tr><th>10</th><th>20</th><th>30</th></tr>
</thead>
<tbody>
<tr><th>10</th><td>1</td><td>2</td><td>3</td></tr>
<tr><th>20</th><td>4</td><td>5</td><td>6</td></tr>
</tbody>
`;
document.body.appendChild(table);
// Leaf row is the numeric header row; it has NO corner cell of its own.
expect(headerRow(table)!.cells[0].textContent).toBe('10');
expect(headerRows(table)).toHaveLength(2);
// Occupancy-aware: none of the three leaf cells is dropped as a row label.
expect(dataHeaderCells(table).map((c) => c.textContent)).toEqual([
'10',
'20',
'30',
]);
});

it('leaves a plain single-row header unchanged (dataHeaderCells == slice(1))', () => {
const { table } = buildNumericGrid();
const head = headerRow(table)!;
expect(headerRows(table)).toHaveLength(1);
expect(dataHeaderCells(table)).toEqual(sourceCells(head).slice(1));
});

it('skips a no-<thead> leading banner row', () => {
const table = document.createElement('table');
table.innerHTML = `
<tr><th></th><th colspan="3">Length Overall (m)</th></tr>
<tr><th>Speed Knots</th><th>10</th><th>20</th><th>30</th></tr>
<tr><th>10</th><td>1</td><td>2</td><td>3</td></tr>
<tr><th>20</th><td>4</td><td>5</td><td>6</td></tr>
`;
document.body.appendChild(table);
expect(headerRow(table)!.cells[1].textContent).toBe('10');
expect(bodyRows(table)).toHaveLength(2);
expect(dataHeaderCells(table).map((c) => c.textContent)).toEqual([
'10',
'20',
'30',
]);
});
});

/* ── Cells + counts ─────────────────────────────────────────────────── */

describe('cell views and counts', () => {
Expand Down
Loading
Loading