Skip to content

Commit 7788441

Browse files
committed
Add settings panel & other fixes
1 parent f3cdbc8 commit 7788441

16 files changed

Lines changed: 5729 additions & 2635 deletions

README.md

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ st_pivot_table(
155155
)
156156
```
157157

158-
In the interactive toolbar, aggregation is edited inside the `Values` dropdown, and raw measure chips display the selected aggregation inline in a compact name-first format such as `Revenue (Sum)`.
158+
In the interactive toolbar, aggregation is edited inside the **Settings Panel**. Open the panel, click the aggregation badge on a value chip to change it, then click **Apply**. Raw measure chips in the toolbar display the selected aggregation inline in a compact name-first format such as `Revenue (Sum)`.
159159

160160
### Synthetic Measures (V1)
161161

@@ -473,7 +473,7 @@ st_pivot_table(
473473

474474
- Each group shows a subtotal row with a collapse/expand toggle (+/−).
475475
- Collapsed groups hide child rows but keep the subtotal visible.
476-
- Expand All / Collapse All controls are available in the Settings popover (gear icon in the toolbar).
476+
- Expand All / Collapse All controls are available in the toolbar utility menu.
477477

478478
**Grouping vs. leaf dimensions:** When subtotals are on, all dimensions except
479479
the innermost are *grouping dimensions*. They define collapsible groups and
@@ -569,7 +569,7 @@ st_pivot_table(
569569

570570
### Locked Mode
571571

572-
Use `locked=True` for a viewer-mode experience with exploration enabled. Toolbar config controls stay locked so end-users cannot change rows, columns, values, per-measure aggregation, or settings toggles. Reset, Swap, and config import/export are hidden, while data export remains available and the Settings gear stays visible for read-only display status plus Expand/Collapse All group controls. Header-menu sorting, filtering, and `Show Values As` remain available, and drill-down still works.
572+
Use `locked=True` for a viewer-mode experience with exploration enabled. The Settings Panel and toolbar config controls are locked so end-users cannot change rows, columns, values, or per-measure aggregation. Reset, Swap, and config import/export are hidden, while data export remains available. Expand/Collapse All group controls remain accessible in the toolbar utility menu. Header-menu sorting, filtering, and `Show Values As` remain available, and drill-down still works.
573573

574574
```python
575575
st_pivot_table(
@@ -582,9 +582,9 @@ st_pivot_table(
582582
)
583583
```
584584

585-
### Toolbar Utility Menu
585+
### Toolbar and Settings Panel
586586

587-
When `interactive=True`, hovering over the top-right of the toolbar reveals utility actions:
587+
When `interactive=True`, the toolbar displays read-only zone cards showing current **Rows**, **Columns**, and **Values** assignments. Hovering over the top-right reveals utility actions:
588588

589589
| Action | Description |
590590
|--------|-------------|
@@ -593,28 +593,38 @@ When `interactive=True`, hovering over the top-right of the toolbar reveals util
593593
| **Copy Config** | Copies the current config as JSON to clipboard |
594594
| **Import Config** | Paste a JSON config to apply |
595595
| **Export Data** | Open the export popover (Excel / CSV / TSV / Clipboard). Use `export_filename` to customize the download filename. |
596+
| **Expand / Collapse All** | Expand or collapse all row/column groups (visible when subtotals are enabled or 2+ column dimensions exist) |
596597
| **Fullscreen** (expand icon) | Toggles fullscreen mode — the table fills the entire viewport. Press Escape or click the collapse icon to exit. |
597-
| **Settings** (gear icon) | Opens a popover with display toggles: Row Totals, Column Totals, Subtotals, Repeat Labels, Sticky Headers, and Expand/Collapse All group controls |
598+
| **Settings** (pivot icon) | Opens the Settings Panel for full field configuration |
598599

599-
In **locked mode**, Reset, Swap, and config import/export are hidden. `Export Data` remains available as a viewer action. The Settings gear remains visible, its popover shows read-only display status plus group expand/collapse actions, and header-menu sorting, filtering, and `Show Values As` stay enabled.
600+
#### Settings Panel (Staged Commit UX)
600601

601-
### Toolbar Field Search
602+
The Settings Panel is the primary authoring surface for pivot configuration. Changes are staged locally and only applied when you click **Apply**. Click **Cancel** or press **Escape** to discard.
602603

603-
When a toolbar picker has more than **8 available fields**, the `Rows`, `Columns`, or `Values` dropdown automatically shows a field search input at the top of the panel.
604+
The panel contains:
604605

605-
- Typing filters the available field list in place.
606-
- `ArrowDown` from the search box moves focus into the first matching option.
607-
- `Escape` closes the dropdown, even when the current query has zero matches.
608-
- In the `Values` dropdown, search only filters the field checklist. Synthetic-measure actions and per-measure aggregation controls remain visible below it.
606+
- **Available Fields** — unassigned columns shown as draggable chips. Click a chip's menu to add it to Rows, Columns, or Values. When more than 8 fields are available, a search input appears.
607+
- **Rows / Columns / Values** drop zones — drag chips to reorder within a zone, drag between zones, or use the `x` button to remove. Value chips show an aggregation picker (click the badge to change).
608+
- **Synthetic Measures** — click **+ Add measure** to create derived metrics (ratio of sums, difference of sums) with optional format patterns.
609+
- **Display Toggles** — Row Totals, Column Totals, Subtotals, Repeat Labels, and Sticky Headers.
610+
611+
External config changes (toolbar DnD, Reset, Swap, config import) while the panel is open will close it and discard uncommitted edits.
612+
613+
In **locked mode**, Reset, Swap, and config import/export are hidden. `Export Data` remains available as a viewer action. The Settings icon shows read-only display status, and header-menu sorting, filtering, and `Show Values As` stay enabled.
614+
615+
### Field Search
616+
617+
When the Settings Panel has more than **8 available fields**, a search input appears at the top of the Available Fields section. Typing filters the field chips in place. The container maintains its initial height even when search reduces the visible chips.
609618

610619
This is a frontend-only convenience feature; no Python parameter is needed to enable it.
611620

612621
### Drag-and-Drop Field Configuration
613622

614-
When `interactive=True`, each chip in the Rows, Columns, and Values toolbar zones has a **grip-dots drag handle** on its left side. Drag chips to:
623+
Drag-and-drop is available in two contexts:
615624

616-
- **Reorder within a zone** — change the grouping hierarchy (e.g., swap which dimension is the outer vs. inner group in Rows).
617-
- **Move between zones** — drag a chip from Rows to Columns (or vice versa), or between Rows/Columns and Values. The Values zone only accepts numeric columns; non-numeric drops are silently rejected.
625+
**Toolbar DnD:** Each chip in the Rows, Columns, and Values toolbar zones has a **grip-dots drag handle**. Drag to reorder within a zone or move between zones. These are immediate (non-staged) changes.
626+
627+
**Settings Panel DnD:** Inside the Settings Panel, chips in Available Fields and all zone sections are draggable. Drag from Available Fields into a zone, reorder within zones, or move between zones. These changes are staged and applied on **Apply**.
618628

619629
**Visual feedback:**
620630
- A floating overlay chip follows the cursor during drag.
@@ -624,9 +634,11 @@ When `interactive=True`, each chip in the Rows, Columns, and Values toolbar zone
624634

625635
**Constraints:**
626636
- `frozen_columns` render without drag handles and cannot be dragged.
627-
- Synthetic measures cannot be dragged to other zones.
637+
- Non-numeric fields are rejected from the Values zone.
638+
- Rows and Columns are mutually exclusive (a field cannot be in both).
639+
- A field can be in Values and one dimension zone simultaneously.
628640
- When `locked=True`, drag-and-drop is fully disabled.
629-
- A 5 px activation distance distinguishes clicks from drags, so remove buttons and dropdown toggles work normally.
641+
- A 5 px activation distance distinguishes clicks from drags.
630642

631643
**Config cleanup on move:** When fields move between zones, related config properties (aggregation, sort, collapsed groups, subtotals, conditional formatting, show-values-as, per-measure totals) are automatically synchronized.
632644

@@ -712,13 +724,62 @@ The component follows WAI-ARIA patterns for all interactive elements:
712724
- **Drag-and-drop**: Space to pick up a chip, arrow keys to move, Space to drop at the new position. Screen reader announcements provided by dnd-kit.
713725
- **Header menus**: Escape closes. Arrow keys navigate options. Space/Enter selects.
714726
- **Export/Import popovers**: Focus is automatically placed on the first interactive element when opened. Tab/Shift+Tab moves between controls; tabbing out closes the popover.
715-
- **Settings popover** (gear icon): Focus moves to first checkbox on open. Escape closes. Tab navigates between toggles.
727+
- **Settings Panel** (pivot icon): Focus moves into the panel on open. Escape closes and discards staged changes. Tab navigates between fields, zones, toggles, and buttons. Aggregation dropdowns support Enter/Space for keyboard selection.
716728
- **Radio groups** (export format/content): Arrow keys move focus between options. Space/Enter selects.
717729
- **Drill-down panel**: Focus moves to the close button on open. Escape closes.
718730
- **Data cells**: Focusable via Tab. Space/Enter triggers cell click.
719731

720732
---
721733

734+
## Performance: Using Fragments
735+
736+
Streamlit reruns the entire script whenever a widget's state changes. In apps with multiple pivot tables or expensive data preparation, this means every toolbar change, sort, or filter in one table triggers a full rerun — including all other tables.
737+
738+
Wrapping each pivot table in [`@st.fragment`](https://docs.streamlit.io/develop/api-reference/execution-flow/st.fragment) scopes reruns to just the fragment that changed, leaving the rest of the app untouched.
739+
740+
### Basic pattern
741+
742+
```python
743+
import streamlit as st
744+
from streamlit_pivot import st_pivot_table
745+
746+
df = load_data() # runs once per full rerun, not on fragment reruns
747+
748+
@st.fragment
749+
def sales_pivot():
750+
result = st_pivot_table(df, key="sales", rows=["Region"], values=["Revenue"])
751+
if result and result.get("cell_click"):
752+
st.info(f"Clicked: {result['cell_click']}")
753+
754+
sales_pivot()
755+
756+
@st.fragment
757+
def product_pivot():
758+
st_pivot_table(df, key="products", rows=["Product"], values=["Units"])
759+
760+
product_pivot()
761+
```
762+
763+
Interacting with "sales" only re-executes `sales_pivot()` — the data load and `product_pivot()` are not re-executed.
764+
765+
### When fragments help
766+
767+
| Scenario | Benefit |
768+
|---|---|
769+
| App with multiple pivot tables | Interactions in one table don't re-execute the others |
770+
| Expensive data loading / transformation | Data prep runs only on full reruns, not on every config change |
771+
| Hybrid drilldown (`execution_mode="threshold_hybrid"`) | Server round-trips for drill-down are scoped to the fragment |
772+
773+
### Caveats
774+
775+
- **Return values**: Streamlit ignores fragment return values during fragment reruns. Code that reads the result of `st_pivot_table()` should live inside the same fragment, or use `st.session_state[key]` instead.
776+
- **Data prep with randomness**: Keep DataFrame generation that uses random seeds outside the fragment to avoid non-deterministic data on fragment reruns.
777+
- **Callbacks**: `on_config_change` and `on_cell_click` fire during fragment reruns, which is the expected behavior.
778+
779+
The demo app (`streamlit_app.py`) wraps each of its 19 sections in `@st.fragment` as a reference implementation.
780+
781+
---
782+
722783
## Development
723784

724785
### Development install (editable)
@@ -735,7 +796,7 @@ uv pip install -e '.[with-streamlit]' --force-reinstall
735796
uv run streamlit run streamlit_app.py
736797
```
737798

738-
The example app (`streamlit_app.py`) contains 18 sections covering the major features and usage patterns with interactive examples and inline documentation.
799+
The example app (`streamlit_app.py`) contains 19 sections covering the major features and usage patterns with interactive examples and inline documentation.
739800

740801
### Building the frontend
741802

@@ -768,7 +829,7 @@ npx vitest run
768829
uv build
769830
```
770831

771-
Output: `dist/streamlit_pivot-0.1.0-py3-none-any.whl`
832+
Output: `dist/streamlit_pivot-<version>-py3-none-any.whl`
772833

773834
### Requirements
774835

e2e_playwright/e2e_utils.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,20 +173,34 @@ def get_pivot(page: Page, key: str) -> Locator:
173173
return container
174174

175175

176-
def open_settings_popover(page: Page, container: Locator) -> Locator:
177-
"""Open the gear settings popover in the toolbar."""
178-
panel = page.get_by_test_id("toolbar-settings-panel")
176+
def open_settings_panel(page: Page, container: Locator) -> Locator:
177+
"""Open the settings panel in the toolbar.
178+
179+
Waits for any exit animation to complete before re-opening.
180+
"""
181+
panel = page.get_by_test_id("settings-panel")
182+
183+
# If the panel is animating out, wait for it to fully disappear first
179184
if panel.count():
180-
expect(panel).to_be_visible(timeout=5000)
181-
return panel
185+
try:
186+
panel.wait_for(state="hidden", timeout=500)
187+
except Exception:
188+
expect(panel).to_be_visible(timeout=5000)
189+
return panel
182190

183191
button = container.get_by_test_id("toolbar-settings")
184192
button.scroll_into_view_if_needed()
185193
button.evaluate("el => el.click()")
186194
expect(panel).to_be_visible(timeout=5000)
195+
page.wait_for_timeout(100)
187196
return panel
188197

189198

199+
def open_settings_popover(page: Page, container: Locator) -> Locator:
200+
"""Deprecated alias for open_settings_panel."""
201+
return open_settings_panel(page, container)
202+
203+
190204
def _find_free_port() -> int:
191205
"""Find and return a free port on the local machine."""
192206
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:

e2e_playwright/pivot_table_data_test.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import pytest
2424
from playwright.sync_api import Page, expect
2525

26-
from e2e_utils import get_pivot, open_settings_popover
26+
from e2e_utils import get_pivot, open_settings_panel
2727
from pivot_table_app_support import _load_main_fixture
2828

2929

@@ -371,8 +371,9 @@ def test_sticky_headers_during_scroll(page_at_app: Page):
371371
header_box_after["y"] >= wrapper_box["y"] - 2
372372
), "Sticky header should remain within the wrapper viewport"
373373

374-
open_settings_popover(page, container)
375-
container.get_by_test_id("toolbar-sticky-headers").locator("input").click()
374+
panel = open_settings_panel(page, container)
375+
panel.get_by_test_id("settings-sticky-headers").locator("input").click()
376+
panel.get_by_test_id("settings-apply").click()
376377

377378
table = container.get_by_test_id("pivot-table")
378379
page.wait_for_function(

0 commit comments

Comments
 (0)