PivotTable
Flat records turned sideways: rows by one field, columns by another, each cell the sum, count or average of a third, with totals.
Demo
| Region | Q1 | Q2 | Q3 | Q4 | Total |
|---|---|---|---|---|---|
| North | $12,400.00 | $17,300.00 | $9,800.00 | — | $39,500.00 |
| South | $8,300.50 | — | $11,250.00 | $14,000.00 | $33,550.50 |
| West | $5,600.00 | — | — | $7,700.00 | $13,300.00 |
| Total | $26,300.50 | $17,300.00 | $21,050.00 | $21,700.00 | $86,350.50 |
| assignee | Open | Blocked | Done | Total |
|---|---|---|---|---|
| Ada | 2 | — | 1 | 3 |
| Mika | 1 | 1 | — | 2 |
| Sol | — | — | 3 | 3 |
| Total | 3 | 1 | 4 | 8 |
| rep | Q1 | Q2 | Q3 | Q4 |
|---|---|---|---|---|
| Ada | 12,400 | 15,100 | – | 14,000 |
| Mika | 5,600 | 2,200 | 9,800 | – |
| Sol | 8,300.5 | – | 11,250 | 7,700 |
The same flat records, pivoted three ways: sums of money with totals, counts in a given column order, and averages.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/pivot_table.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/pivot_table.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/pivot_table.qwe
let pivot_table_demo_sales = [ { region: "North", quarter: "Q1", amount: 12400, rep: "Ada" }, { region: "North", quarter: "Q2", amount: 15100, rep: "Ada" }, { region: "North", quarter: "Q2", amount: 2200, rep: "Mika" }, { region: "North", quarter: "Q3", amount: 9800, rep: "Mika" }, { region: "South", quarter: "Q1", amount: 8300.5, rep: "Sol" }, { region: "South", quarter: "Q3", amount: 11250, rep: "Sol" }, { region: "South", quarter: "Q4", amount: 14000, rep: "Ada" }, { region: "West", quarter: "Q1", amount: 5600, rep: "Mika" }, { region: "West", quarter: "Q4", amount: 7700, rep: "Sol" },]let pivot_table_demo_tickets = [ { assignee: "Ada", status: "Open" }, { assignee: "Ada", status: "Open" }, { assignee: "Ada", status: "Done" }, { assignee: "Mika", status: "Open" }, { assignee: "Mika", status: "Blocked" }, { assignee: "Sol", status: "Done" }, { assignee: "Sol", status: "Done" }, { assignee: "Sol", status: "Done" },]view PivotTableDemo() = <div.demo-stack> <PivotTable id="pivot-table-demo-sales" label="Sales by region and quarter" rows={pivot_table_demo_sales} rows_by="region" cols_by="quarter" value="amount" format="money" totals={true} corner="Region"/> <PivotTable id="pivot-table-demo-tickets" label="Tickets by assignee and status" rows={pivot_table_demo_tickets} rows_by="assignee" cols_by="status" agg="count" col_order={["Open", "Blocked", "Done"]} totals={true}/> <PivotTable id="pivot-table-demo-avg" label="Average sale by rep and quarter" rows={pivot_table_demo_sales} rows_by="rep" cols_by="quarter" value="amount" agg="avg" empty="–"/> <p.demo-note>The same flat records, pivoted three ways: sums of money with totals, counts in a given column order, and averages.</p> </div>Source
ui/pivot_table.qwe
// PivotTable: flat records turned sideways: a row for each value of one field, a// column for each value of another, and in each cell the sum (or count, average,// lowest or highest) of a third: sales by region and quarter, hours by person and// project, tickets by assignee and status.//// <PivotTable label="Sales" rows={sales} rows_by="region" cols_by="quarter" value="amount"// format="money" totals={true}/>// <PivotTable rows={tickets} rows_by="assignee" cols_by="status" agg="count"/>//// rows: the records, maps. rows_by: the field whose values make the rows; cols_by:// the field whose values make the columns. Both come sorted; row_order and col_order// (lists of values) set the order instead, and leave out values not listed.// value: the field put together in each cell. agg: "sum" (default), "count" (the// records, so value is not needed), "avg", "min" or "max". format: "number"// (1,234.5, default), "money" ($1,234.50, or currency: "€") or "percent" (0.125 is// 12.5%). empty: what a cell with no records shows ("—").// totals: a Total column and a Total row, each from all the records of its row or// column (so an average is of all of them, not of the cells). corner: the text in// the top-left cell (rows_by by default). label: the table's name for screen// readers. id: the element's id. On a narrow screen it scrolls sideways.import ui/theme.qwe use ui_clslet pivot_table_css = css`.ui-pivot { min-width: 0; overflow: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-pivot table { width: 100%; border-collapse: separate; border-spacing: 0; }.ui-pivot th, .ui-pivot td { padding: 9px 14px; white-space: nowrap; border-bottom: 1px solid var(--ui-border); }.ui-pivot td { text-align: right; font-variant-numeric: tabular-nums; }.ui-pivot thead th { background: var(--ui-subtle); color: var(--ui-muted); font-size: 12px; font-weight: 600; letter-spacing: .03em; text-transform: uppercase; text-align: right; }.ui-pivot thead th:first-child, .ui-pivot tbody th { text-align: left; }.ui-pivot tbody th { font-weight: 500; background: color-mix(in srgb, var(--ui-subtle) 50%, transparent); }.ui-pivot tbody tr:last-child td, .ui-pivot tbody tr:last-child th { border-bottom: 0; }.ui-pivot tbody tr:hover td { background: color-mix(in srgb, var(--ui-subtle) 70%, transparent); }.ui-pivot-empty { color: var(--ui-muted); }.ui-pivot .ui-pivot-total { font-weight: 600; background: color-mix(in srgb, var(--ui-accent-soft) 45%, var(--ui-surface)); }.ui-pivot tr.ui-pivot-total th, .ui-pivot tr.ui-pivot-total td { border-top: 2px solid var(--ui-strong); border-bottom: 0; }.ui-pivot-none { padding: 28px 14px; text-align: center; color: var(--ui-muted); }`// What the records' values come to: null when there is nothing to count.fn pivot_table_agg(xs: list, agg: str) { if agg == "count" { return len(xs) } let ns = xs | filter((x) => type_of(x) == "int" || type_of(x) == "float") if len(ns) == 0 { return null } match agg { "avg" => sum(ns) / len(ns) "min" => min(ns) "max" => max(ns) _ => sum(ns) }}// A cell's text.fn pivot_table_fmt(v, format: str, currency: str, empty: str) -> str { if v == null { return empty } let sign = v < 0 ? "-" : "" match format { "money" => `${sign}${currency}${format_number(abs(v), "en-US", 2)}` "percent" => `${format_number(round(v * 1000) / 10, "en-US")}%` _ => format_number(round(v * 100) / 100, "en-US") }}view PivotTable(rows: list, rows_by: str, cols_by: str, value: str? = null, agg: str = "sum", format: str = "number", currency: str = "$", empty: str = "—", totals: bool = false, row_order: list? = null, col_order: list? = null, corner: str? = null, total_label: str = "Total", label: str? = null, id: str? = null, class: str? = null) { style global pivot_table_css let rvals = (row_order ?? sort(unique(rows | map((r) => r[rows_by])) | filter((v) => v != null))) | map((v) => str(v)) let cvals = (col_order ?? sort(unique(rows | map((r) => r[cols_by])) | filter((v) => v != null))) | map((v) => str(v)) let cell = (rs: list) => pivot_table_agg(agg == "count" || value == null ? rs : rs | map((r) => r[value]), agg) let text = (rs: list) => pivot_table_fmt(cell(rs), format, currency, empty) let of_row = (rv: str) => rows | filter((r) => str(r[rows_by]) == rv) let of_col = (rs: list, cv: str) => rs | filter((r) => str(r[cols_by]) == cv) <div class={ui_cls(["ui-pivot", class])} id={id}> <table aria-label={label}> <thead> <tr> <th scope="col">{corner ?? rows_by}</th> {for cv in cvals => <th scope="col">{cv}</th>} {totals && <th.ui-pivot-total scope="col">{total_label}</th>} </tr> </thead> <tbody> {len(rvals) == 0 && <tr><td.ui-pivot-none colspan={str(len(cvals) + 1 + (totals ? 1 : 0))}>No records.</td></tr>} {for rv in rvals => { let rs = of_row(rv) <tr> <th scope="row">{rv}</th> {for cv in cvals => { let here = of_col(rs, cv) <td class={len(here) == 0 ? "ui-pivot-empty" : null}>{len(here) == 0 ? empty : text(here)}</td> }} {totals && <td.ui-pivot-total>{text(rs)}</td>} </tr> }} {totals && len(rvals) > 0 && <tr.ui-pivot-total> <th scope="row">{total_label}</th> {for cv in cvals => <td>{text(of_col(rows, cv))}</td>} <td.ui-pivot-total>{text(rows)}</td> </tr>} </tbody> </table> </div>}Related
AuditLogWho did what to what and when, newest first, filtered by person and action, searched, and paged.5 views · 7 downloadsComparisonTableFeatures down the side and options across, with ticks, crosses and values in the cells, and one option picked out.5 views · 6 downloadsCsvTableA table made from CSV text, with quoted fields, a header row, numbers lined up on the right and a row limit.6 views · 7 downloadsDataGridSort, search, filter by a column's values, pick rows and page through them, in one table.5 views · 8 downloadsDataTableRows of data sorted by their headers, with search, pages, number, money, percent and date columns.5 views · 9 downloadsEditableTableA grid edited in place: text, number, select, checkbox and date cells, checked, sorted, with rows added and deleted.7 views · 5 downloads
Thread
Thread 0 posts
On PivotTable: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.