Skip to content

Commit 420fc1e

Browse files
committed
Feat: expand column config display metadata support
1 parent 84c1b8b commit 420fc1e

20 files changed

Lines changed: 2574 additions & 57 deletions

‎README.md‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ Returns a `PivotTableResult` dict containing the current `config` state and opti
8989
| `column_alignment` | `dict[str, str] \| None` | `None` | Per-field text alignment: `"left"`, `"center"`, or `"right"`. |
9090
| `show_values_as` | `dict[str, str] \| None` | `None` | Per-field display mode. See [Show Values As](#show-values-as). |
9191
| `conditional_formatting` | `list[dict] \| None` | `None` | Visual formatting rules. See [Conditional Formatting](#conditional-formatting). |
92-
| `column_config` | `dict[str, Any] \| None` | `None` | Optional per-column format hints, using a subset of the Streamlit [`column_config`](https://docs.streamlit.io/develop/api-reference/data/st.column_config) shape. Each entry is a dict with a `format` key (d3-style or printf-style `"%,.2f"`) and optionally `type` (`"date"` / `"datetime"` / `"time"` map to `dimension_format`; everything else maps to `number_format`). Explicit `number_format` / `dimension_format` parameters always win. See [Formats from `Styler` and `column_config`](#formats-from-styler-and-column_config). |
92+
| `column_config` | `dict[str, Any] \| None` | `None` | Optional per-column display configuration, using a subset of the Streamlit [`column_config`](https://docs.streamlit.io/develop/api-reference/data/st.column_config) shape. Supported keys: `format`, `type`, `label`, `help`, `width` (`"small"` / `"medium"` / `"large"` / integer px), `pinned` (locks the field in the config UI; does not create a sticky column), and `alignment` (`"left"` / `"center"` / `"right"`, unions with the `column_alignment` kwarg; explicit kwarg wins). Explicit `number_format` / `dimension_format` / `column_alignment` parameters always win. See [Formats from `Styler` and `column_config`](#formats-from-styler-and-column_config). |
9393
| `empty_cell_value` | `str` | `"-"` | Display string for cells with no data. |
9494

9595
#### Layout
@@ -480,10 +480,17 @@ st_pivot_table(
480480
# -> number_format = {"Revenue": "$,.0f", "Profit": ",.2f"}
481481
```
482482

483-
**`column_config`.** A dict mapping column names to a small subset of the Streamlit [`column_config`](https://docs.streamlit.io/develop/api-reference/data/st.column_config) shape. Each entry is read for:
483+
**`column_config`.** A dict mapping column names to a small subset of the Streamlit [`column_config`](https://docs.streamlit.io/develop/api-reference/data/st.column_config) shape. Both plain dict literals and `st.column_config.*` typed objects are accepted. Supported keys:
484484

485485
- `format` — a format string. d3-style patterns (`",.2f"`, `"$,.0f"`) pass through as-is. Streamlit printf-style patterns (`"%,.2f"`) are normalized by stripping the leading `%`.
486486
- `type` — if it resolves to `"date"`, `"datetime"`, or `"time"`, the pattern contributes to `dimension_format`; otherwise it contributes to `number_format`. Plain `type_config = {"type": ...}` nesting is also accepted.
487+
- `label` — display name override for the field. Renames the field in row-dim headers, measure headers, chips (toolbar + settings panel), and exported header rows. The **underlying field id is unchanged** in the serialized config — sort, filter, and conditional-formatting rules still target the canonical id. Empty / whitespace-only labels fall back to the field id.
488+
- `help` — text rendered as a native `title` tooltip on the corresponding dimension or measure header.
489+
- `width` — either a preset (`"small"`=100px, `"medium"`=120px, `"large"`=200px) or an integer pixel value in the range `[20, 2000]`. Applies to row-dimension columns and measure columns (for the `col-single` header in single-value mode, and per-measure value-label cells in multi-value mode). Out-of-range / unparseable widths warn once per field and are skipped. **Interactive resize drags override the configured width at runtime but are not persisted to config**, so the width returns to the configured value after rerun/remount.
490+
- `pinned` — when `True` or `"left"`, locks the field in the **config UI** (equivalent to adding it to `frozen_columns`): the field cannot be removed from its zone or reordered via drag-and-drop. This does **not** create a visually sticky column. `"right"` is currently warned and ignored.
491+
- `alignment` — one of `"left"`, `"center"`, `"right"`. Unions with the `column_alignment` kwarg; when both set a value for the same field, the explicit `column_alignment` kwarg wins. Invalid values warn once per field and are skipped (unlike the `column_alignment` kwarg, which still raises on invalid values).
492+
493+
Unknown keys in dict literals warn once per `(field, key)` pair. Streamlit's internal defaults from typed `st.column_config.*` objects (`disabled`, `required`, `default`) are silently ignored. Recognized but unsupported column types (e.g. `line_chart`, `selectbox`) warn once per `(field, type)`.
487494

488495
```python
489496
st_pivot_table(
@@ -493,14 +500,15 @@ st_pivot_table(
493500
columns=["Order Date"],
494501
values=["Revenue", "Units"],
495502
column_config={
496-
"Revenue": {"format": "$,.0f"},
497-
"Units": {"format": ",.0f"},
503+
"Region": {"label": "Area", "help": "Geographic region", "width": "large", "pinned": True},
504+
"Revenue": {"format": "$,.0f", "label": "Rev", "width": 180, "alignment": "right"},
505+
"Units": {"format": ",.0f", "alignment": "center"},
498506
"Order Date": {"format": "YYYY-MM-DD", "type": "date"},
499507
},
500508
)
501509
```
502510

503-
**Precedence.** `explicit number_format / dimension_format` > `column_config` > `Styler`. The lower-priority sources only fill gaps — any field already present in an explicit format dict keeps the caller-supplied pattern.
511+
**Precedence.** For format fields: `explicit number_format / dimension_format` > `column_config` > `Styler`. For alignment: `explicit column_alignment` > `column_config.alignment` > default (right-aligned measures, left-aligned dimensions). The lower-priority sources only fill gaps — any field already present in an explicit format or alignment dict keeps the caller-supplied value. `label`, `help`, and `width` are `column_config`-driven only (no legacy kwargs). `pinned` **unions** with `frozen_columns` / `hidden_from_drag_drop`.
504512

505513
### Conditional Formatting
506514

‎SKILL.md‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ Creates a pivot table component. All parameters except `data` are keyword-only.
201201
| `column_alignment` | `dict[str, str] \| None` | `None` | `"left"` / `"center"` / `"right"`. |
202202
| `show_values_as` | `dict[str, str] \| None` | `None` | Per-field display mode. See [Show Values As](#show-values-as). |
203203
| `conditional_formatting` | `list[dict] \| None` | `None` | Color scales, data bars, thresholds. |
204-
| `column_config` | `dict[str, Any] \| None` | `None` | Streamlit-style format hints; auto-routed to `number_format` or `dimension_format`. |
204+
| `column_config` | `dict[str, Any] \| None` | `None` | Streamlit-style per-column display config. Keys: `format`, `type`, `label`, `help`, `width` (`"small"`/`"medium"`/`"large"`/int px ∈ [20, 2000]), `pinned` (config-UI lock only, not sticky), `alignment` (`"left"`/`"center"`/`"right"`; unions with `column_alignment` kwarg, explicit kwarg wins). Both dict literals and `st.column_config.*` objects are accepted. |
205205
| `empty_cell_value` | `str` | `"-"` | String for empty cells. |
206206

207207
#### Layout
@@ -371,13 +371,36 @@ st_pivot_table(
371371
)
372372
```
373373

374-
**Auto-formats from upstream sources.** Precedence: explicit `number_format` / `dimension_format` > `column_config` > Pandas `Styler`.
374+
**Auto-formats from upstream sources.** Precedence for format fields: explicit `number_format` / `dimension_format` > `column_config` > Pandas `Styler`. For alignment: explicit `column_alignment` > `column_config.alignment` > default (measures right, dims left). `label`, `help`, and `width` are `column_config`-driven only (no legacy kwargs). `pinned` **unions** with `frozen_columns`.
375375

376376
```python
377377
styled = df.style.format({"Revenue": "${:,.0f}"})
378378
st_pivot_table(styled, key="styler_demo", rows=["Region"], values=["Revenue"])
379379
```
380380

381+
**Display-oriented `column_config` keys (Tier 1).**
382+
383+
```python
384+
st_pivot_table(
385+
df,
386+
key="column_config_display",
387+
rows=["Region"],
388+
columns=["Year"],
389+
values=["Revenue"],
390+
column_config={
391+
"Region": {"label": "Area", "help": "Geographic region", "width": "large", "pinned": True},
392+
"Revenue": {"label": "Rev", "help": "Total revenue (USD)", "width": 180, "format": "$,.0f", "alignment": "right"},
393+
},
394+
)
395+
```
396+
397+
- `label` overrides display text only. The **canonical field id is unchanged** in the serialized config; sort, filter, and conditional-formatting rules still target the id. Empty / whitespace-only labels fall back to the id.
398+
- `help` renders as a native `title` tooltip on the dim/measure header.
399+
- `width` presets: `"small"`=100px, `"medium"`=120px, `"large"`=200px. Integers must be in `[20, 2000]`; out-of-range values warn once per field and are skipped. **Interactive resize drags override the configured width at runtime but are not persisted to config** (width returns to the configured value after rerun/remount).
400+
- `pinned=True` (or `"left"`) locks the field in the **config UI** — no drag, no remove. It does **not** create a visually sticky column. `"right"` warns and is ignored.
401+
- `alignment` accepts `"left"`, `"center"`, or `"right"`. It **unions** with the `column_alignment` kwarg; if both set a value for the same field, the explicit `column_alignment` kwarg wins. Invalid values warn once per field and are skipped (unlike the `column_alignment` kwarg, which still raises on invalid input).
402+
- Unknown keys in dict literals warn once per `(field, key)`; internal defaults on `st.column_config.*` objects (`disabled`, `required`, `default`) are silently ignored; recognized-but-unsupported column types (e.g. `line_chart`, `selectbox`) warn once per `(field, type)`.
403+
381404
### Conditional Formatting
382405

383406
```python

‎e2e_playwright/e2e_utils.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
INTERACTIONS_SCRIPT = Path(__file__).parent / "pivot_table_interactions_app.py"
4545
DATA_SCRIPT = Path(__file__).parent / "pivot_table_data_app.py"
4646
GOLDEN_SCRIPT = Path(__file__).parent / "pivot_table_golden_app.py"
47+
COLUMN_CONFIG_SCRIPT = Path(__file__).parent / "pivot_table_column_config_app.py"
4748

4849
PIVOT_KEYS = [
4950
"test_pivot",
@@ -149,6 +150,14 @@
149150
"golden_export",
150151
]
151152

153+
COLUMN_CONFIG_PIVOT_KEYS = [
154+
"test_pivot_cc_label",
155+
"test_pivot_cc_help",
156+
"test_pivot_cc_width_px",
157+
"test_pivot_cc_width_preset",
158+
"test_pivot_cc_pinned",
159+
]
160+
152161
APP_CONFIGS = {
153162
"default": {"script": SCRIPT, "pivot_keys": PIVOT_KEYS},
154163
"pivot_table_test.py": {"script": TOOLBAR_SCRIPT, "pivot_keys": TOOLBAR_PIVOT_KEYS},
@@ -161,6 +170,10 @@
161170
"script": GOLDEN_SCRIPT,
162171
"pivot_keys": GOLDEN_PIVOT_KEYS,
163172
},
173+
"pivot_table_column_config_test.py": {
174+
"script": COLUMN_CONFIG_SCRIPT,
175+
"pivot_keys": COLUMN_CONFIG_PIVOT_KEYS,
176+
},
164177
}
165178

166179

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Copyright 2025 Snowflake Inc.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Streamlit app backing the column_config Playwright suite."""
17+
18+
from __future__ import annotations
19+
20+
import streamlit as st
21+
22+
from streamlit_pivot import st_pivot_table
23+
24+
from pivot_table_app_support import init_page, load_data, noop
25+
26+
27+
def render_app(data):
28+
df = data["df"]
29+
30+
st.subheader("column_config.label")
31+
st_pivot_table(
32+
df,
33+
key="test_pivot_cc_label",
34+
rows=["Region"],
35+
columns=["Year"],
36+
values=["Revenue"],
37+
aggregation="sum",
38+
column_config={
39+
"Region": {"label": "Area"},
40+
"Revenue": {"label": "Rev"},
41+
},
42+
interactive=True,
43+
on_config_change=noop,
44+
)
45+
46+
st.subheader("column_config.help")
47+
st_pivot_table(
48+
df,
49+
key="test_pivot_cc_help",
50+
rows=["Region"],
51+
columns=["Year"],
52+
values=["Revenue"],
53+
aggregation="sum",
54+
column_config={
55+
"Region": {"help": "Geographic region"},
56+
"Revenue": {"help": "Revenue in USD"},
57+
},
58+
interactive=True,
59+
on_config_change=noop,
60+
)
61+
62+
st.subheader("column_config.width (pixel)")
63+
st_pivot_table(
64+
df,
65+
key="test_pivot_cc_width_px",
66+
rows=["Region"],
67+
columns=["Year"],
68+
values=["Revenue"],
69+
aggregation="sum",
70+
column_config={
71+
"Region": {"width": 180},
72+
"Revenue": {"width": 220},
73+
},
74+
interactive=True,
75+
on_config_change=noop,
76+
)
77+
78+
st.subheader("column_config.width (preset)")
79+
st_pivot_table(
80+
df,
81+
key="test_pivot_cc_width_preset",
82+
rows=["Region"],
83+
columns=["Year"],
84+
values=["Revenue"],
85+
aggregation="sum",
86+
column_config={
87+
"Region": {"width": "large"},
88+
"Revenue": {"width": "small"},
89+
},
90+
interactive=True,
91+
on_config_change=noop,
92+
)
93+
94+
st.subheader("column_config.pinned (config-UI lock)")
95+
st_pivot_table(
96+
df,
97+
key="test_pivot_cc_pinned",
98+
rows=["Region"],
99+
columns=["Year"],
100+
values=["Revenue"],
101+
aggregation="sum",
102+
column_config={
103+
"Region": {"pinned": True},
104+
},
105+
interactive=True,
106+
on_config_change=noop,
107+
)
108+
109+
110+
def main():
111+
init_page()
112+
render_app(load_data())
113+
114+
115+
if __name__ == "__main__":
116+
main()
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Copyright 2025 Snowflake Inc.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""End-to-end coverage for Tier 1 column_config keys: label, help, width, pinned."""
17+
18+
from __future__ import annotations
19+
20+
from playwright.sync_api import Page, expect
21+
22+
from e2e_utils import get_pivot
23+
24+
25+
def test_column_config_label_row_dim(page_at_app: Page):
26+
"""column_config.label renames a row dimension header without changing the
27+
underlying field id."""
28+
page = page_at_app
29+
container = get_pivot(page, "test_pivot_cc_label")
30+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
31+
32+
row_dim = container.get_by_test_id("pivot-row-dim-label-Region")
33+
expect(row_dim).to_contain_text("Area")
34+
expect(row_dim).not_to_contain_text("Region (")
35+
36+
37+
def test_column_config_label_measure(page_at_app: Page):
38+
"""column_config.label renames a measure header."""
39+
page = page_at_app
40+
container = get_pivot(page, "test_pivot_cc_label")
41+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
42+
43+
thead = container.locator("thead")
44+
expect(thead).to_contain_text("Rev")
45+
46+
47+
def test_column_config_help_tooltip_row_dim(page_at_app: Page):
48+
"""column_config.help flows through as a title attribute on the row dim header."""
49+
page = page_at_app
50+
container = get_pivot(page, "test_pivot_cc_help")
51+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
52+
53+
row_dim = container.get_by_test_id("pivot-row-dim-label-Region")
54+
expect(row_dim).to_have_attribute("title", "Geographic region")
55+
56+
57+
def test_column_config_help_tooltip_measure(page_at_app: Page):
58+
"""column_config.help flows through as a title attribute on a measure header."""
59+
page = page_at_app
60+
container = get_pivot(page, "test_pivot_cc_help")
61+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
62+
63+
measure_cell = container.locator(
64+
'[data-testid="pivot-header-cell"][title="Revenue in USD"]'
65+
).first
66+
expect(measure_cell).to_be_visible()
67+
68+
69+
def test_column_config_width_pixel(page_at_app: Page):
70+
"""column_config.width with a pixel integer applies to the row dim header."""
71+
page = page_at_app
72+
container = get_pivot(page, "test_pivot_cc_width_px")
73+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
74+
75+
row_dim = container.get_by_test_id("pivot-row-dim-label-Region")
76+
style = row_dim.get_attribute("style") or ""
77+
assert "width: 180px" in style, f"expected 180px width, got style={style!r}"
78+
79+
80+
def test_column_config_width_preset(page_at_app: Page):
81+
"""column_config.width with a preset string maps to documented pixel values."""
82+
page = page_at_app
83+
container = get_pivot(page, "test_pivot_cc_width_preset")
84+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
85+
86+
row_dim = container.get_by_test_id("pivot-row-dim-label-Region")
87+
style = row_dim.get_attribute("style") or ""
88+
assert "width: 200px" in style, f"expected 'large' -> 200px, got style={style!r}"
89+
90+
91+
def test_column_config_pinned_locks_in_config_ui(page_at_app: Page):
92+
"""column_config.pinned=True locks the field in the config UI (same behavior
93+
as frozen_columns): the chip renders but its remove (×) button is absent."""
94+
page = page_at_app
95+
container = get_pivot(page, "test_pivot_cc_pinned")
96+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
97+
98+
chip = container.get_by_test_id("toolbar-rows-chip-Region")
99+
expect(chip).to_be_visible(timeout=5000)
100+
remove_btn = container.get_by_test_id("toolbar-rows-remove-Region")
101+
expect(remove_btn).to_have_count(0)

0 commit comments

Comments
 (0)