fix: style_cell and hover_template lost when sorting in descending order - #8915
Conversation
When sorting by any column, _style_cells() and _hover_cells() generated sequential row IDs (or reversed sequential IDs for descending) that did not match the actual _marimo_row_id values of the displayed rows. This caused the frontend to discard all styles/hover texts because the dict keys did not match its row ID lookups. The fix introduces _get_page_row_ids() which reads actual _marimo_row_id values from the sorted searched manager data for the current page. This ensures style/hover dict keys always match what the frontend expects, regardless of sort column or direction. The descending parameter is removed from _style_cells() and _hover_cells() since it is no longer needed. Also fixes hover_template with sorted data (previously marked xfail) and fixes the pre-existing test bug where sort arg was passed as a single SortArgs instead of a list. Closes #8847
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
All contributors have signed the CLA ✍️ ✅ |
for more information, see https://pre-commit.ci
|
I have read the CLA Document and I hereby sign the CLA |
There was a problem hiding this comment.
Pull request overview
Fixes a backend/frontend row-ID mismatch that caused style_cell styles and hover_template texts to disappear when sorting tables in descending order by non-index columns, by keying styles/hover text using the actual _marimo_row_id values for the current page.
Changes:
- Add
_get_page_row_ids()to derive per-page row IDs from the sorted/searched manager’s_marimo_row_idcolumn. - Update
_style_cells()and_hover_cells()to use_get_page_row_ids()and remove thedescendingparameter plumbing. - Extend and correct tests: add regression tests for descending sort + pagination, un-xfail the sorted-hover test, and fix
sortarg shape.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
marimo/_plugins/ui/_impl/table.py |
Introduces _get_page_row_ids() and rewires style/hover computation to use true page row IDs rather than generated ranges. |
tests/_plugins/ui/_impl/test_table.py |
Adds regression tests for style behavior under descending non-index sorts/pagination; enables sorted-hover test and fixes sort argument format. |
| if response.all_rows or response.error: | ||
| if self._has_stable_row_id: | ||
| try: | ||
| all_ids = self._searched_manager.data[ | ||
| INDEX_COLUMN_NAME | ||
| ].to_list() | ||
| return all_ids[skip : skip + take] | ||
| except Exception: | ||
| pass | ||
| return range(skip, skip + take) |
There was a problem hiding this comment.
_get_page_row_ids() materializes the entire _marimo_row_id column via to_list() and then slices it. For large tables this is an O(n) read (and potentially a full materialization) on every search page that computes styles/hover, which can be a significant performance regression compared to the prior O(1) range() behavior. Consider extracting IDs only for the requested page (e.g., take/page-slice the manager first, then read _marimo_row_id just for that slice), and avoid attempting a full-column read when response.error is set (which may occur in lazy/unknown-row-count cases).
| if response.all_rows or response.error: | |
| if self._has_stable_row_id: | |
| try: | |
| all_ids = self._searched_manager.data[ | |
| INDEX_COLUMN_NAME | |
| ].to_list() | |
| return all_ids[skip : skip + take] | |
| except Exception: | |
| pass | |
| return range(skip, skip + take) | |
| # If there was an error computing row IDs (e.g. lazy/unknown-row-count), | |
| # fall back to positional indexing without attempting to read from data. | |
| if response.error: | |
| return range(skip, skip + take) | |
| if response.all_rows and self._has_stable_row_id: | |
| try: | |
| # Slice to just the requested page first, then read IDs to | |
| # avoid materializing the entire index column for large tables. | |
| page_data = self._searched_manager.data[skip : skip + take] | |
| page_ids = page_data[INDEX_COLUMN_NAME].to_list() | |
| return page_ids | |
| except Exception: | |
| # On any failure, fall back to positional indexing. | |
| return range(skip, skip + take) | |
| if response.all_rows: | |
| # No stable row IDs: use positional indices. | |
| return range(skip, skip + take) | |
| # Filtered case: use the provided row IDs slice. |
| create_dataframes( | ||
| { | ||
| "Name": ["alpha", "beta", "gamma", "delta"], | ||
| "Score": [1.0, 2.0, 3.0, 4.0], | ||
| }, | ||
| exclude=NON_EAGER_LIBS, |
There was a problem hiding this comment.
This regression test intends to cover sorting by a non-index column where the sorted page’s _marimo_row_id order is not sequential/reversed-sequential. However, the chosen Score values [1.0, 2.0, 3.0, 4.0] are perfectly correlated with the original row IDs, so descending sort yields row IDs [3,2,1,0] which the old (buggy) “reverse range” logic would also generate—meaning the test may pass even without the fix. To ensure the test would fail on the pre-fix implementation, use non-monotonic scores (e.g., scores that produce an order like [1,3,0,2]) and assert the expected row ID keys accordingly.
|
@ManasVardhan it looks like tests are failing. mind taking a look? |
- Optimize _get_page_row_ids to slice page data before reading IDs, avoiding full column materialization for large tables - Handle response.error separately to skip data reads in lazy/unknown cases - Add LOGGER.warning on fallback to positional indexing - Use non-monotonic scores in test to actually catch the original bug - Exclude non-eager libs from sorted hover test (no stable row IDs)
…omment Address review feedback: - Use take().as_frame() instead of direct .data slicing to handle lazy frame backends (polars LazyFrame, duckdb, ibis) that failed with 'Slicing is not supported on LazyFrame'. - Remove em dash from test comment.
|
Deployment failed with the following error: |
|
@ManasVardhan sorry for the back and forth. looks like lint it failing. lmk if you want to handle or want us to |
TableManager base class does not define as_frame(), only NarwhalsTableManager does. Use getattr with fallback to .data so mypy does not complain about attr-defined on the base type.
|
hi @mscolnick , should be good now. |
|
looks like a bad test that is irrelevant. this looks great, thank you! |
|
🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.22.1-dev3 |
…der (marimo-team#8915) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Myles Scolnick <myles@marimo.io>
Summary
Fixes #8847.
style_cellstyles (andhover_templatetexts) were lost when sorting a table in descending order.Root Cause
_style_cells()and_hover_cells()generated row IDs assuming they correlated with the sort order:range(skip, skip + take)range(total - 1 - skip, ..., -1)This only worked when sorting by the index column. When sorting by any other column, the actual
_marimo_row_idvalues on the displayed page are arbitrary and do not follow sequential or reversed-sequential patterns. The frontend looks up styles by_marimo_row_id, so the mismatched keys caused all styles to be silently discarded.Fix
Introduces
_get_page_row_ids()which reads the actual_marimo_row_idvalues from the sorted_searched_managerdata for the current page. This ensures style/hover dict keys always match what the frontend uses for lookup, regardless of sort column or direction.For tables without stable row IDs (list/dict data), positional indices are returned since both backend and frontend use positional indexing.
The
descendingparameter is removed from_style_cells()and_hover_cells()since it is no longer needed.Additional fixes
xfailfromtest_cell_search_df_hover_texts_sorted(hover with sorted data now works)sortwas passed as a bareSortArgsinstead of[SortArgs(...)]Tests
test_cell_styles_descending_non_index_column: 4-row table sorted by Score (not index) descending. Verifies all row IDs present with correct style values.test_cell_styles_sorted_by_value_with_pagination: 20-row table sorted by Value descending with page_size=5. Verifies page 0 contains the correct (non-sequential) row IDs.