SPIKE fix: stop corrupting group header colspans
Reported symptom: with split_rollup_mode on, columns looked shuffled and some month columns had no year header above them. That was this spike, not Perspective. Two mistakes, either enough on its own: - The original span was stashed on the cell as `_pfFullSpan` the first time it was seen. regular-table recycles header cells between draws, so the stashed value reappeared on an unrelated group and truncated it. - The work was guarded on `colSpan > 1`, so once a cell had been set to 1 it could never grow back. Both fired even with nothing collapsed, which is why merely turning rollup on was enough to break the header. Now the span is derived from the column paths every draw, and colSpan is only touched while something is actually collapsed — with nothing hidden, regular-table's own merge of adjacent equal header values is the right answer and is left alone. Measured on Reason x (Year, Month), 18 leaf columns, across first draw, sort, sort-cleared and horizontal scroll: before fix 2025 spans 3, group row covers 8/18 (10 orphaned) after fix 2025 spans 13, group row covers 18/18 no spike 2025 spans 13, group row covers 18/18 Collapse behaviour is unchanged: 8 -> 6 -> 3 -> 5 cells across collapse 2025, collapse 2026, expand 2025, with header and body width-aligned throughout and the state surviving a sort. Not fixed, because it is not a bug: rollup emits totals BEFORE their members, so the grand total sits far left and each year's subtotal precedes its own months. SplitRollupMode documents this as "totals before" order and offers only flat|rollup, so it cannot be moved to Excel's convention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoxNi8cFsQLPUSw3obb5NH
This commit is contained in:
parent
d2e706b483
commit
42d9d51f63
@ -37,6 +37,26 @@ export function levelsOfHeader(columnHeader, splitLen) {
|
||||
return out
|
||||
}
|
||||
|
||||
// How many consecutive columns from x share this cell's first chy+1 levels —
|
||||
// the span the group header would have if nothing were hidden.
|
||||
//
|
||||
// This has to be derived from the column paths on every draw. The obvious
|
||||
// shortcut, stashing the original span on the cell the first time we see it, is
|
||||
// wrong: regular-table recycles header cells between draws, so the stashed value
|
||||
// reappears on an unrelated group and silently truncates it.
|
||||
function fullSpan(columnPaths, x, chy) {
|
||||
const levels = levelsOf(columnPaths[x])
|
||||
if (levels.length < chy + 1) return 1 // a rollup placeholder covers only itself
|
||||
const prefix = levels.slice(0, chy + 1)
|
||||
let n = 0
|
||||
for (let i = x; i < columnPaths.length; i++) {
|
||||
const l = levelsOf(columnPaths[i])
|
||||
if (l.length < prefix.length || !prefix.every((v, k) => l[k] === v)) break
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
function isDescendant(levels, prefix) {
|
||||
return levels.length > prefix.length && prefix.every((v, i) => levels[i] === v)
|
||||
}
|
||||
@ -82,29 +102,34 @@ export function attachBranchCollapse(grid, { onChange } = {}) {
|
||||
if (!len) return
|
||||
const hidden = hiddenColumns(paths(), collapsed)
|
||||
|
||||
const columnPaths = paths()
|
||||
|
||||
for (const cell of rt.querySelectorAll('tbody td, tbody th, thead th')) {
|
||||
const meta = rt.getMeta(cell)
|
||||
if (!meta) continue
|
||||
|
||||
// A spanning group header covers [x, x + colSpan); it stays visible but
|
||||
// must shrink to the columns still showing under it, or the header row
|
||||
// and the body stop lining up.
|
||||
const span = cell.colSpan || 1
|
||||
if (meta.type === 'column_header' && span > 1 && meta.x != null) {
|
||||
// A group header stays visible but must shrink to the columns still
|
||||
// showing under it, or the header row and the body stop lining up.
|
||||
//
|
||||
// Only while something is actually collapsed. With nothing hidden this
|
||||
// must not touch colSpan at all — regular-table computes it each draw by
|
||||
// merging adjacent equal header values, and that answer is the right one.
|
||||
if (hidden.size && meta.type === 'column_header' && meta.x != null) {
|
||||
const chy = meta.column_header_y ?? 0
|
||||
if (chy < len) {
|
||||
const full = fullSpan(columnPaths, meta.x, chy)
|
||||
let visible = 0
|
||||
for (let i = meta.x; i < meta.x + (cell._pfFullSpan || span); i++) {
|
||||
if (!hidden.has(i)) visible++
|
||||
}
|
||||
cell._pfFullSpan = cell._pfFullSpan || span
|
||||
for (let i = meta.x; i < meta.x + full; i++) if (!hidden.has(i)) visible++
|
||||
cell.colSpan = Math.max(1, visible)
|
||||
}
|
||||
}
|
||||
|
||||
const hide = meta.x != null && hidden.has(meta.x)
|
||||
cell.style.display = hide ? 'none' : ''
|
||||
|
||||
// mark what can be clicked to collapse, and which way it would go
|
||||
if (meta.type === 'column_header' && meta.x != null && !hide) {
|
||||
const branch = branchAt(paths(), meta.x, meta.column_header_y ?? 0, len)
|
||||
const branch = branchAt(columnPaths, meta.x, meta.column_header_y ?? 0, len)
|
||||
if (branch) {
|
||||
cell.dataset.pfBranch = branch
|
||||
cell.style.cursor = 'pointer'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user