Skip to content

Commit d9bed7f

Browse files
committed
Add remaining aggregate funcs for hybrid mode
1 parent 4bb903a commit d9bed7f

14 files changed

Lines changed: 1621 additions & 45 deletions

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,10 +484,9 @@ st_pivot_table(
484484

485485
**Auto thresholds:** In `"auto"` mode, server-side pre-aggregation activates when the dataset has at least 100K rows (high-cardinality layouts) or 250K rows (moderate layouts) and the estimated pivot shape exceeds the client-side comfort budget.
486486

487-
**Supported aggregations:** `sum`, `count`, `min`, `max`, and `avg`. Configs using other aggregations (e.g. `median`, `count_distinct`) fall back to client-side computation automatically.
487+
**Supported aggregations:** All 10 aggregation types are supported in hybrid mode. `count` and `count_distinct` work on any column type; all other aggregations (`sum`, `avg`, `min`, `max`, `median`, `percentile_90`, `first`, `last`) coerce values to numeric and ignore non-numeric entries, consistent with client-only mode behavior. For non-decomposable aggregations (`avg`, `count_distinct`, `median`, `percentile_90`, `first`, `last`), the server computes correct totals and subtotals via a sidecar payload, ensuring accuracy that client-side re-aggregation alone cannot provide.
488488

489489
**Limitations:**
490-
- Drill-down is disabled in hybrid mode because values are pre-aggregated on the server rather than built from raw rows.
491490
- Synthetic measures are not supported in hybrid mode (falls back to client-side).
492491

493492
### Locked Mode

e2e_playwright/e2e_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@
117117
"test_pivot_null_separate",
118118
"test_pivot_null_zero",
119119
"test_pivot_sparse_drilldown",
120+
"test_pivot_hybrid_median",
121+
"test_pivot_hybrid_count_distinct",
120122
"test_pivot_synthetic",
121123
]
122124

e2e_playwright/pivot_table_data_app.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,35 @@ def render_app(data):
253253
on_config_change=noop,
254254
)
255255

256+
# --- Hybrid mode pivots with non-decomposable aggs ---
257+
df_large = df.loc[df.index.repeat(50)].reset_index(drop=True)
258+
259+
st.subheader("Hybrid Median Pivot")
260+
st_pivot_table(
261+
df_large,
262+
key="test_pivot_hybrid_median",
263+
rows=["Region"],
264+
columns=["Year"],
265+
values=["Revenue"],
266+
aggregation="median",
267+
show_totals=True,
268+
execution_mode="threshold_hybrid",
269+
on_config_change=noop,
270+
)
271+
272+
st.subheader("Hybrid Count Distinct Pivot")
273+
st_pivot_table(
274+
df_large,
275+
key="test_pivot_hybrid_count_distinct",
276+
rows=["Region"],
277+
columns=["Year"],
278+
values=["Category"],
279+
aggregation="count_distinct",
280+
show_totals=True,
281+
execution_mode="threshold_hybrid",
282+
on_config_change=noop,
283+
)
284+
256285
st.subheader("Synthetic Measures Pivot")
257286
st_pivot_table(
258287
df_synth,

e2e_playwright/pivot_table_data_test.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,3 +534,45 @@ def test_synthetic_menu_hides_show_values_as(page_at_app: Page):
534534
timeout=5000
535535
)
536536
expect(page.get_by_test_id("header-menu-display")).to_have_count(0)
537+
538+
539+
# ---------------------------------------------------------------------------
540+
# Hybrid mode tests (non-decomposable aggregations)
541+
# ---------------------------------------------------------------------------
542+
543+
544+
def test_hybrid_median_renders_cells(page_at_app: Page):
545+
"""Hybrid median pivot renders data cells and totals."""
546+
page = page_at_app
547+
container = get_pivot(page, "test_pivot_hybrid_median")
548+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
549+
cells = container.get_by_test_id("pivot-data-cell")
550+
expect(cells.first).to_be_visible(timeout=5000)
551+
cell_count = cells.count()
552+
assert cell_count > 0, "Hybrid median pivot should render data cells"
553+
554+
555+
def test_hybrid_median_grand_total_not_dash(page_at_app: Page):
556+
"""Grand total for hybrid median is not the empty cell value."""
557+
page = page_at_app
558+
container = get_pivot(page, "test_pivot_hybrid_median")
559+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
560+
grand_total_cells = container.get_by_test_id("pivot-grand-total-cell")
561+
if grand_total_cells.count() > 0:
562+
text = grand_total_cells.first.inner_text().strip()
563+
assert text != "-", f"Grand total should be a number, got '{text}'"
564+
565+
566+
def test_hybrid_count_distinct_renders_correct_values(page_at_app: Page):
567+
"""Hybrid count_distinct pivot shows actual distinct counts (not 1)."""
568+
page = page_at_app
569+
container = get_pivot(page, "test_pivot_hybrid_count_distinct")
570+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
571+
cells = container.get_by_test_id("pivot-data-cell")
572+
expect(cells.first).to_be_visible(timeout=5000)
573+
texts = [
574+
c.inner_text().strip() for c in cells.all() if c.inner_text().strip() != "-"
575+
]
576+
for t in texts:
577+
val = float(t.replace(",", ""))
578+
assert val >= 1, f"Count distinct value should be >= 1, got {val}"

streamlit_app.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,71 @@
11831183
"forced to threshold_hybrid mode for demonstration."
11841184
)
11851185

1186+
st.markdown(
1187+
"""
1188+
**Non-decomposable aggregations in hybrid mode:** All 10 aggregation types are
1189+
supported in hybrid mode, including `median`, `count_distinct`, `percentile_90`,
1190+
`first`, and `last`. The server computes correct totals via a sidecar payload.
1191+
`count` and `count_distinct` work on any column type; all other aggregations
1192+
coerce values to numeric and ignore non-numeric entries.
1193+
"""
1194+
)
1195+
1196+
col_a, col_b = st.columns(2)
1197+
with col_a:
1198+
st.markdown("**Hybrid Median Pivot**")
1199+
st_pivot_table(
1200+
df_hybrid,
1201+
key="hybrid_median_demo",
1202+
rows=["Region"],
1203+
columns=["Year"],
1204+
values=["Revenue"],
1205+
aggregation="median",
1206+
number_format={"Revenue": "$,.2f"},
1207+
show_totals=True,
1208+
execution_mode="threshold_hybrid",
1209+
)
1210+
with col_b:
1211+
st.markdown("**Hybrid Count Distinct Pivot**")
1212+
st_pivot_table(
1213+
df_hybrid,
1214+
key="hybrid_count_distinct_demo",
1215+
rows=["Region"],
1216+
columns=["Year"],
1217+
values=["Category"],
1218+
aggregation="count_distinct",
1219+
show_totals=True,
1220+
execution_mode="threshold_hybrid",
1221+
)
1222+
1223+
with st.expander("View Code — Non-decomposable Aggregations"):
1224+
st.code(
1225+
"""
1226+
# Median: server computes correct grand/row/col totals
1227+
st_pivot_table(
1228+
df_hybrid,
1229+
rows=["Region"],
1230+
columns=["Year"],
1231+
values=["Revenue"],
1232+
aggregation="median",
1233+
show_totals=True,
1234+
execution_mode="threshold_hybrid",
1235+
)
1236+
1237+
# Count Distinct: works on any column type (e.g. Category strings)
1238+
st_pivot_table(
1239+
df_hybrid,
1240+
rows=["Region"],
1241+
columns=["Year"],
1242+
values=["Category"],
1243+
aggregation="count_distinct",
1244+
show_totals=True,
1245+
execution_mode="threshold_hybrid",
1246+
)
1247+
""",
1248+
language="python",
1249+
)
1250+
11861251

11871252
# ---------------------------------------------------------------------------
11881253
# Section 15: Drag-and-Drop Field Configuration

0 commit comments

Comments
 (0)