Data

PivotTable

Flat records turned sideways: rows by one field, columns by another, each cell the sum, count or average of a third, with totals.

#data#table#no-js

Demo

Preview
RegionQ1Q2Q3Q4Total
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
assigneeOpenBlockedDoneTotal
Ada2—13
Mika11—2
Sol——33
Total3148
repQ1Q2Q3Q4
Ada12,40015,100–14,000
Mika5,6002,2009,800–
Sol8,300.5–11,2507,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.qwe
main.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>}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

On PivotTable: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.