Pivot Table
Dynamic row/column grouping with multi-level aggregation, drag-and-drop axis configuration, and expand/collapse group rows.
#Preview
Pivot Table
| Region / Product | Q1 | Q2 | Q3 | Q4 | Total |
|---|---|---|---|---|---|
East | 15,800 | 9,800 | 16,200 | 10,500 | 52,300 |
Gadget | — | 9,800 | — | 10,500 | 20,300 |
Widget | 15,800 | — | 16,200 | — | 32,000 |
North | 20,200 | 14,500 | 9,100 | — | 43,800 |
Gadget | 8,200 | — | 9,100 | — | 17,300 |
Widget | 12,000 | 14,500 | — | — | 26,500 |
South | 6,700 | 11,500 | 7,400 | 13,200 | 38,800 |
Gizmo | 6,700 | — | 7,400 | — | 14,100 |
Widget | — | 11,500 | — | 13,200 | 24,700 |
West | 5,500 | 6,200 | 17,100 | 11,300 | 40,100 |
Gadget | — | — | — | 11,300 | 11,300 |
Gizmo | 5,500 | 6,200 | — | — | 11,700 |
Widget | — | — | 17,100 | — | 17,100 |
| Grand Total | 48,200 | 42,000 | 49,800 | 35,000 | 175,000 |
1"use client"23import { createSignal, createMemo } from '@barefootjs/client'45const SAMPLE_DATA = [6 { region: 'North', product: 'Widget', quarter: 'Q1', amount: 12000 },7 // ...16 records total8]910function PivotTable() {11 const [rowFields, setRowFields] = createSignal(['region', 'product'])12 const [columnField, setColumnField] = createSignal('quarter')13 const [aggregationFn, setAggregationFn] = createSignal('sum')14 const [expandedGroups, setExpandedGroups] = createSignal(new Set(['North', 'South', 'East', 'West']))1516 // L2: group data by row fields17 const groupedData = createMemo(() =>18 buildGroupTree(SAMPLE_DATA, rowFields(), 0)19 )2021 // L3: unique column split values22 const columnValues = createMemo(() => {23 const col = columnField()24 return [...new Set(SAMPLE_DATA.map(r => r[col]))].sort()25 })2627 // L4: aggregate each (groupPath, columnValue) pair28 const aggregatedCells = createMemo(() => {29 const fn = aggregationFn()30 const col = columnField()31 const colVals = columnValues()32 // ... computes cells for all (row × column) intersections33 return computeCells(groupedData(), col, colVals, fn)34 })3536 // L5: flatten tree respecting expand/collapse state37 const visibleRows = createMemo(() =>38 flattenTree(groupedData(), expandedGroups(), '')39 )4041 // L6: grand totals per column42 const grandTotals = createMemo(() =>43 computeTotals(SAMPLE_DATA, columnField(), columnValues(), aggregationFn())44 )4546 return (47 <table>48 <thead>49 <tr>50 <th>Groups</th>51 {columnValues().map(cv => <th key={cv}>{cv}</th>)}52 <th>Total</th>53 </tr>54 </thead>55 <tbody>56 {visibleRows().map(row => (57 <tr key={row.id}>58 <td style={`padding-left: ${12 + row.depth * 16}px`}>59 {row.isGroup && (60 <button onClick={() => toggleExpand(row.id)}>▶</button>61 )}62 {row.label}63 </td>64 {columnValues().map(cv => (65 <td key={cv}>{aggregatedCells()[row.id]?.[cv] ?? '—'}</td>66 ))}67 <td>{aggregatedCells()[row.id]?.['__total__']}</td>68 </tr>69 ))}70 </tbody>71 <tfoot>72 <tr>73 <td>Grand Total</td>74 {columnValues().map(cv => <td key={cv}>{grandTotals()[cv]}</td>)}75 <td>{grandTotals()['__total__']}</td>76 </tr>77 </tfoot>78 </table>79 )80}#Features
6-Level Memo Dependency Chain
rowFields signal → groupedData memo → aggregatedCells memo (also depends on columnValues, valueField, aggregationFn) → visibleRows memo (also depends on expandedGroups) → render. Changing aggregationFn only recomputes aggregatedCells and downstream — groupedData is not re-evaluated, demonstrating memo caching.
Nested Group Loops
Row fields create a hierarchical tree (e.g., Region → Product). Expand/collapse state is tracked in a Set signal. The visibleRows memo flattens the tree respecting expansion state, producing a dynamic flat list for rendering. Each group row shows aggregated values computed from all descendant records.
Dynamic Loop Structure via Axis Reconfiguration
Drag fields between Available, Rows, Columns, and Values zones to restructure the table. Removing a row field collapses a grouping level; removing the column field eliminates the column split and shows a single Total column. These changes propagate through the entire 6-level memo chain, testing dynamic loop reconstruction.
Multi-Input Memo Convergence
aggregatedCells depends on groupedData (L2), columnValues (L3), valueField signal, and aggregationFn signal — four upstream sources converging on one memo. This tests that the compiler correctly batches multi-source updates without redundant recomputation.
Drag-and-Drop Axis Configuration
Fields are draggable between axis zones using the HTML5 Drag and Drop API. Each zone enforces type constraints (dimensions only in Rows/Columns, measures only in Values). X buttons provide a non-drag fallback for removing fields from an axis.