Skip to content

Pivot: preserve expanded rows when adding a field, sorting, or refreshing - #9785

Open
mahdi13 wants to merge 3 commits into
rilldata:mainfrom
inkitt:pivot-value-based-expansion
Open

mahdi13 wants to merge 3 commits into
rilldata:mainfrom
inkitt:pivot-value-based-expansion

Conversation

@mahdi13

@mahdi13 mahdi13 commented Aug 4, 2026

Copy link
Copy Markdown

The pivot table tracked expanded rows by TanStack's positional row ids (dot paths like 0.1.2 that index into subRows). Any config change re-partitions or re-orders the row tree, so those ids stop pointing at the same rows and the expanded state was discarded. Adding a measure or dimension, reordering row dimensions, or sorting all collapsed the tree back to the top level.

This switches expansion to value-based hierarchical keys: the dimension values from the root to a node, so a node keeps the same key across add, sort, and data refreshes.

  • PivotTable sets a value-based getRowId (parent id plus this row's value), with a reserved id for the prepended grand-totals row.
  • getValuesForExpandedKey and addExpandedDataToPivot resolve a key by matching each row's own value (stored under rowDimensions[0] at every depth) instead of positional indices, which also removes the totals-row offset.
  • queryExpandedRowMeasureValues derives nesting depth from the key's segment count.
  • getFiltersForCell, getValuesForFlatTable, getRawRowValues, the show-more row limits, and the ancestor-highlight walk all use the value keys.
  • The defensive expanded = {} resets on add/sort/columns/rows are dropped. A reorder just leaves keys that match nothing (inert), and the legacy positional pivotExpanded proto field is no longer read.
  • Existing unit tests updated for value-based resolution, plus a Playwright test that expands a nested row and asserts it stays open after adding a measure and sorting.

Reordering the row dimensions changes a node's value path, so those specific nodes are not restored (they were not before either). Adding a field, sorting, and refreshing now keep the tree expanded.

Closes #9781

Checklist:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

mahdi13 added 2 commits August 4, 2026 15:28
Foundation for keying pivot row expansion by dimension-value paths instead
of positional TanStack row ids, so expansion survives sorting, adding a
field, and data refreshes (rilldata#9781). Keys are the dimension
values from root to a node, NUL-joined and hierarchical (depth is the
segment count, parent is strip-last-segment). Pure helpers with unit tests.
Wire the value-based expand keys through the pivot so expansion survives
sorting, adding a field, and data refreshes (rilldata#9781).

- PivotTable sets a hierarchical value-based getRowId, with a reserved id
  for the grand-totals row.
- getValuesForExpandedKey and addExpandedDataToPivot resolve a key by
  matching each row's value (stored under rowDimensions[0]) instead of
  positional indices.
- queryExpandedRowMeasureValues derives depth from the key's segment count.
- getFiltersForCell, getValuesForFlatTable, getRawRowValues, the show-more
  keys, and the ancestor-highlight walk use the value keys.
- Drop the expanded={} resets on add/sort/columns/rows and stop reading the
  legacy positional pivotExpanded proto field.
- Tests updated for value-based resolution.

Fixes rilldata#9781
@mahdi13
mahdi13 force-pushed the pivot-value-based-expansion branch from 63818ef to 0262ac9 Compare August 4, 2026 13:29
Expand a nested row, then add a measure and sort, and assert the row stays
expanded (rilldata#9781).
@mahdi13
mahdi13 force-pushed the pivot-value-based-expansion branch from 0262ac9 to c35cd9a Compare August 4, 2026 15:09
@mahdi13
mahdi13 marked this pull request as ready for review August 4, 2026 15:10
@nishantmonu51 nishantmonu51 added Type:Bug Something isn't working Area:Pivot Size:L Large change: 500-1,999 lines labels Aug 7, 2026
@nishantmonu51 nishantmonu51 mentioned this pull request Aug 9, 2026
8 tasks
@mahdi13

mahdi13 commented Aug 21, 2026

Copy link
Copy Markdown
Author

@nishantmonu51 I'd appreciate if you or someone from the team could take a look at the PR, thanks!

@nishantmonu51 nishantmonu51 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three findings are anchored inline. Two smaller things: toProto no longer writes pivotExpanded and fromProto now ignores it, so the proto field is dead and marking it deprecated in proto/ would make that explicit; and the <NULL> sentinel collides with a genuine dimension value of "<NULL>", which is unlikely to bite but worth a one-line comment given that pivot-click-selection.ts encodes nulls differently. The Playwright test covers add-measure and sort but not the data-refresh case named in the title. The branch merges cleanly onto current main.

*/

// NUL separator, since dimension values won't contain it.
export const EXPAND_KEY_SEP = "\0";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consumers of the old dot-path row ids were not migrated to this separator. web-common/src/features/canvas/components/pivot/pivot-click-to-filter.ts:658 still computes isNestedChild = isNested && rowId.includes("."), so every child row whose value has no dot now takes the dimKeyFromRow branch, and that branch only reads rowDimensionNames[0]. With rows [outer, inner], clicking the revenue cell of X under A stores dimKey "X" while NestedTable.svelte computes the same row's key via nestedDimKeyFromRow as "A\0X": the clicked cell never renders as selected, a second click is not recognised as a deselect, and X collides across parents, which is exactly the collision the comment above that line says the branch exists to prevent. parentExpandKey(rowId) !== "" is the equivalent test. pivot-click-to-filter.spec.ts also fails 23 of 47 tests on this head (all pass on main) because its fixtures still pass positional ids such as "1.0".

for (const row of flatRows) {
// Always skip the prepended grand-totals row.
if (hasTotalsRow && row.id === "0") continue;
if (hasTotalsRow && row.id === PIVOT_TOTALS_ROW_ID) continue;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three sibling totals-row checks still compare against the positional id "0", which getRowId no longer returns: PivotTable.svelte:391, FlatTable.svelte:245 and NestedTable.svelte:570. In PivotTable.svelte a canvas click on a totals-row measure cell is therefore no longer short-circuited and falls through to onCellClickToFilter; getValuesForExpandedKey resolves no row values for "\0totals", so getFiltersForCell applies a filter built from the column entries alone. In the two table components isTotalsRow is false for the totals row, so getCellFormatting stops returning null and totals cells pick up conditional-formatting colours, and flatCellState/nestedCellState mark them interactiveCell when click-to-filter is enabled.

config.pivot?.showTotalsRow !== false && numMeasures > 0,
);

if (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard only rejects an empty resolution, but getValuesForExpandedKey now stops at the deepest resolvable segment, so a partially resolved key falls through and fires an aggregation query with an incomplete filter on every refresh. With rows [publisher, time], key "A\0Jan" expanded, and domain then inserted to give [publisher, domain, time], the key still has depth 2 so the nestLevel early return does not fire, but it resolves to values = ["A"]; the query for time values filtered only by publisher = A is issued and then discarded by addExpandedDataToPivot, since node is undefined after the failed second segment. Because the PR deliberately keeps inert keys around after setPivotRows, this repeats until the key is collapsed. Guarding on values.length !== nestLevel would cover it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area:Pivot Size:L Large change: 500-1,999 lines Type:Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pivot: expanded rows collapse on any config change (adding a measure/dimension, reordering, or sorting)

2 participants