├── .gitignore ├── README.pdf ├── doc ├── example-snippets │ ├── import-csv-easy.typ │ ├── example-data │ │ ├── supplies.typ │ │ └── titanic.typ │ ├── aggregation.typ │ ├── index.typ │ ├── import-csv-raw.typ │ ├── only-cells.typ │ ├── format.typ │ ├── styling.typ │ ├── group.typ │ ├── rearrange.typ │ ├── index-alternate.typ │ ├── basic.typ │ ├── no-headers.typ │ ├── calculation.typ │ ├── title.typ │ ├── transpose.typ │ ├── combine.typ │ ├── align.typ │ ├── filter.typ │ ├── align-manual.typ │ ├── sort.typ │ ├── group-aggregation.typ │ ├── width-manual.typ │ ├── width.typ │ ├── slice.typ │ ├── import-csv.typ │ ├── usd.typ │ └── tablex.typ └── compiled-snippets │ ├── usd.svg │ ├── import-csv-raw.svg │ ├── import-csv.svg │ ├── import-csv-easy.svg │ ├── example-data │ ├── supplies.svg │ └── titanic.svg │ ├── aggregation.svg │ ├── no-headers.svg │ └── index.svg ├── justfile ├── typst.toml ├── local-pub.bash ├── LICENSE ├── .github └── workflows │ └── compile-readme.yml ├── compile-snippets.bash ├── lib.typ ├── README.typ └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | doc/compiled-snippets/* 2 | README.md 3 | README.pdf -------------------------------------------------------------------------------- /README.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amelia-Mowers/typst-tabut/HEAD/README.pdf -------------------------------------------------------------------------------- /doc/example-snippets/import-csv-easy.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": records-from-csv 2 | 3 | #let titanic = records-from-csv(csv("example-data/titanic.csv")); -------------------------------------------------------------------------------- /doc/example-snippets/example-data/supplies.typ: -------------------------------------------------------------------------------- 1 | #let supplies = ( 2 | (name: "Notebook", price: 3.49, quantity: 5), 3 | (name: "Ballpoint Pens", price: 5.99, quantity: 2), 4 | (name: "Printer Paper", price: 6.99, quantity: 3), 5 | ) 6 | -------------------------------------------------------------------------------- /doc/example-snippets/example-data/titanic.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": records-from-csv 2 | 3 | #let titanic = records-from-csv(csv("titanic.csv")); 4 | 5 | #let classes = ( 6 | "N/A", 7 | "First", 8 | "Second", 9 | "Third" 10 | ); -------------------------------------------------------------------------------- /doc/example-snippets/aggregation.typ: -------------------------------------------------------------------------------- 1 | #import "usd.typ": usd 2 | #import "example-data/titanic.typ": titanic, classes 3 | 4 | #table( 5 | columns: (auto, auto), 6 | [*Fare, Total:*], [#usd(titanic.map(r => r.Fare).sum())], 7 | [*Fare, Avg:*], [#usd(titanic.map(r => r.Fare).sum() / titanic.len())], 8 | stroke: none 9 | ) -------------------------------------------------------------------------------- /doc/example-snippets/index.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "example-data/supplies.typ": supplies 3 | 4 | #tabut( 5 | supplies, 6 | ( 7 | (header: [*\#*], func: r => r._index), 8 | (header: [*Name*], func: r => r.name ), 9 | ), 10 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 11 | stroke: none 12 | ) -------------------------------------------------------------------------------- /doc/example-snippets/import-csv-raw.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut, rows-to-records 2 | #import "example-data/supplies.typ": supplies 3 | 4 | #let titanic = { 5 | let titanic-raw = csv("example-data/titanic.csv"); 6 | rows-to-records( 7 | titanic-raw.first(), // The header row 8 | titanic-raw.slice(1, -1), // The rest of the rows 9 | ) 10 | } -------------------------------------------------------------------------------- /doc/example-snippets/only-cells.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut-cells 2 | #import "usd.typ": usd 3 | #import "example-data/supplies.typ": supplies 4 | 5 | #tabut-cells( 6 | supplies, 7 | ( 8 | (header: [Name], func: r => r.name), 9 | (header: [Price], func: r => usd(r.price)), 10 | (header: [Quantity], func: r => r.quantity), 11 | ) 12 | ) -------------------------------------------------------------------------------- /doc/example-snippets/format.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "usd.typ": usd 3 | #import "example-data/supplies.typ": supplies 4 | 5 | #tabut( 6 | supplies, 7 | ( 8 | (header: [*Name*], func: r => r.name ), 9 | (header: [*Price*], func: r => usd(r.price)), 10 | ), 11 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 12 | stroke: none 13 | ) -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | build: local-pub compile-snippets 2 | typst compile README.typ README.pdf 3 | pandoc -t gfm -o README.md README.typ 4 | 5 | build-github-action: local-pub compile-snippets 6 | typst compile README.typ README.pdf 7 | pandoc -t gfm -o README.md README.typ 8 | 9 | local-pub: 10 | sh local-pub.bash 11 | 12 | compile-snippets: 13 | sh compile-snippets.bash 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /doc/example-snippets/styling.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "example-data/supplies.typ": supplies 3 | 4 | #tabut( 5 | supplies, 6 | ( 7 | (header: [Name], func: r => r.name), 8 | (header: [Price], func: r => r.price), 9 | (header: [Quantity], func: r => r.quantity), 10 | ), 11 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 12 | stroke: none 13 | ) -------------------------------------------------------------------------------- /doc/example-snippets/group.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut, group 2 | #import "example-data/titanic.typ": titanic, classes 3 | 4 | #tabut( 5 | group(titanic, r => r.Pclass), 6 | ( 7 | (header: [*Class*], func: r => classes.at(r.value)), 8 | (header: [*Passengers*], func: r => r.group.len()), 9 | ), 10 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 11 | stroke: none 12 | ) -------------------------------------------------------------------------------- /typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tabut" 3 | version = "1.0.2" 4 | authors = ["Amelia Mowers "] 5 | license = "MIT" 6 | description = "Display data as tables." 7 | entrypoint = "lib.typ" 8 | repository = "https://github.com/Amelia-Mowers/typst-tabut" 9 | keywords = ["formatting", "table", "tables", "data", "display"] 10 | exclude = ["README.pdf", "doc/compiled-snippets/*", "doc/example-snippets/*", "*.bash"] 11 | -------------------------------------------------------------------------------- /doc/example-snippets/rearrange.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "example-data/supplies.typ": supplies 3 | 4 | #tabut( 5 | supplies, 6 | ( 7 | (header: [Price], func: r => r.price), // This column is moved to the front 8 | (header: [Name], func: r => r.name), 9 | (header: [Name 2], func: r => r.name), // copied 10 | // (header: [Quantity], func: r => r.quantity), // removed via comment 11 | ) 12 | ) -------------------------------------------------------------------------------- /doc/example-snippets/index-alternate.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "example-data/supplies.typ": supplies 3 | 4 | #tabut( 5 | supplies, 6 | ( 7 | (header: [*\#*], func: r => r.index-alt ), 8 | (header: [*Name*], func: r => r.name ), 9 | ), 10 | index: "index-alt", // set an aternate name for the automatically generated index property. 11 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 12 | stroke: none 13 | ) -------------------------------------------------------------------------------- /doc/example-snippets/basic.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "example-data/supplies.typ": supplies 3 | 4 | #tabut( 5 | supplies, // the source of the data used to generate the table 6 | ( // column definitions 7 | ( 8 | header: [Name], // label, takes content. 9 | func: r => r.name // generates the cell content. 10 | ), 11 | (header: [Price], func: r => r.price), 12 | (header: [Quantity], func: r => r.quantity), 13 | ) 14 | ) -------------------------------------------------------------------------------- /doc/example-snippets/no-headers.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "example-data/supplies.typ": supplies 3 | 4 | #tabut( 5 | supplies, 6 | ( 7 | (header: [*Name*], func: r => r.name), 8 | (header: [*Price*], func: r => r.price), 9 | (header: [*Quantity*], func: r => r.quantity), 10 | ), 11 | headers: false, // Prevents Headers from being generated 12 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 13 | stroke: none, 14 | ) -------------------------------------------------------------------------------- /doc/example-snippets/calculation.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "usd.typ": usd 3 | #import "example-data/supplies.typ": supplies 4 | 5 | #tabut( 6 | supplies, 7 | ( 8 | (header: [*Name*], func: r => r.name ), 9 | (header: [*Price*], func: r => usd(r.price)), 10 | (header: [*Tax*], func: r => usd(r.price * .2)), 11 | (header: [*Total*], func: r => usd(r.price * 1.2)), 12 | ), 13 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 14 | stroke: none 15 | ) -------------------------------------------------------------------------------- /doc/example-snippets/title.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "example-data/supplies.typ": supplies 3 | 4 | #let fmt(it) = { 5 | heading( 6 | outlined: false, 7 | upper(it) 8 | ) 9 | } 10 | 11 | #tabut( 12 | supplies, 13 | ( 14 | (header: fmt([Name]), func: r => r.name ), 15 | (header: fmt([Price]), func: r => r.price), 16 | (header: fmt([Quantity]), func: r => r.quantity), 17 | ), 18 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 19 | stroke: none 20 | ) -------------------------------------------------------------------------------- /doc/example-snippets/transpose.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "usd.typ": usd 3 | #import "example-data/supplies.typ": supplies 4 | 5 | #tabut( 6 | supplies, 7 | ( 8 | (header: [*\#*], func: r => r._index), 9 | (header: [*Name*], func: r => r.name), 10 | (header: [*Price*], func: r => usd(r.price)), 11 | (header: [*Quantity*], func: r => r.quantity), 12 | ), 13 | transpose: true, // set optional name arg `transpose` to `true` 14 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 15 | stroke: none 16 | ) -------------------------------------------------------------------------------- /doc/example-snippets/combine.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | 3 | #let employees = ( 4 | (id: 3251, first: "Alice", last: "Smith", middle: "Jane"), 5 | (id: 4872, first: "Carlos", last: "Garcia", middle: "Luis"), 6 | (id: 5639, first: "Evelyn", last: "Chen", middle: "Ming") 7 | ); 8 | 9 | #tabut( 10 | employees, 11 | ( 12 | (header: [*ID*], func: r => r.id ), 13 | (header: [*Full Name*], func: r => [#r.first #r.middle.first(), #r.last] ), 14 | ), 15 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 16 | stroke: none 17 | ) -------------------------------------------------------------------------------- /doc/example-snippets/align.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "usd.typ": usd 3 | #import "example-data/supplies.typ": supplies 4 | 5 | #tabut( 6 | supplies, 7 | ( // Include `align` as an optional arg to a column def 8 | (header: [*\#*], func: r => r._index), 9 | (header: [*Name*], align: right, func: r => r.name), 10 | (header: [*Price*], align: right, func: r => usd(r.price)), 11 | (header: [*Quantity*], align: right, func: r => r.quantity), 12 | ), 13 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 14 | stroke: none 15 | ) -------------------------------------------------------------------------------- /doc/example-snippets/filter.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "usd.typ": usd 3 | #import "example-data/titanic.typ": titanic, classes 4 | 5 | #tabut( 6 | titanic 7 | .filter(r => r.Pclass == 1) 8 | .slice(0, 5), 9 | ( 10 | (header: [*Name*], func: r => r.Name), 11 | (header: [*Class*], func: r => classes.at(r.Pclass)), 12 | (header: [*Fare*], func: r => usd(r.Fare)), 13 | (header: [*Survived?*], func: r => ("No", "Yes").at(r.Survived)), 14 | ), 15 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 16 | stroke: none 17 | ) -------------------------------------------------------------------------------- /doc/compiled-snippets/usd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /doc/example-snippets/align-manual.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "usd.typ": usd 3 | #import "example-data/supplies.typ": supplies 4 | 5 | #tabut( 6 | supplies, 7 | ( 8 | (header: [*\#*], func: r => r._index), 9 | (header: [*Name*], func: r => r.name), 10 | (header: [*Price*], func: r => usd(r.price)), 11 | (header: [*Quantity*], func: r => r.quantity), 12 | ), 13 | align: (auto, right, right, right), // Alignment defined as in standard table function 14 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 15 | stroke: none 16 | ) -------------------------------------------------------------------------------- /doc/example-snippets/sort.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "usd.typ": usd 3 | #import "example-data/titanic.typ": titanic, classes 4 | 5 | #tabut( 6 | titanic 7 | .sorted(key: r => r.Fare) 8 | .rev() 9 | .slice(0, 5), 10 | ( 11 | (header: [*Name*], func: r => r.Name), 12 | (header: [*Class*], func: r => classes.at(r.Pclass)), 13 | (header: [*Fare*], func: r => usd(r.Fare)), 14 | (header: [*Survived?*], func: r => ("No", "Yes").at(r.Survived)), 15 | ), 16 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 17 | stroke: none 18 | ) -------------------------------------------------------------------------------- /doc/compiled-snippets/import-csv-raw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /doc/compiled-snippets/import-csv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /doc/compiled-snippets/import-csv-easy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /doc/example-snippets/group-aggregation.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut, group 2 | #import "usd.typ": usd 3 | #import "example-data/titanic.typ": titanic, classes 4 | 5 | #tabut( 6 | group(titanic, r => r.Pclass), 7 | ( 8 | (header: [*Class*], func: r => classes.at(r.value)), 9 | (header: [*Total Fare*], func: r => usd(r.group.map(r => r.Fare).sum())), 10 | ( 11 | header: [*Avg Fare*], 12 | func: r => usd(r.group.map(r => r.Fare).sum() / r.group.len()) 13 | ), 14 | ), 15 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 16 | stroke: none 17 | ) -------------------------------------------------------------------------------- /doc/compiled-snippets/example-data/supplies.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /doc/compiled-snippets/example-data/titanic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /doc/example-snippets/width-manual.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "usd.typ": usd 3 | #import "example-data/supplies.typ": supplies 4 | 5 | #box( 6 | width: 300pt, 7 | tabut( 8 | supplies, 9 | ( 10 | (header: [*\#*], func: r => r._index), 11 | (header: [*Name*], func: r => r.name), 12 | (header: [*Price*], func: r => usd(r.price)), 13 | (header: [*Quantity*], func: r => r.quantity), 14 | ), 15 | columns: (auto, 1fr, 20%, 1.5in), // Columns defined as in standard table 16 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 17 | stroke: none, 18 | ) 19 | ) 20 | 21 | -------------------------------------------------------------------------------- /doc/example-snippets/width.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut 2 | #import "usd.typ": usd 3 | #import "example-data/supplies.typ": supplies 4 | 5 | #box( 6 | width: 300pt, 7 | tabut( 8 | supplies, 9 | ( // Include `width` as an optional arg to a column def 10 | (header: [*\#*], func: r => r._index), 11 | (header: [*Name*], width: 1fr, func: r => r.name), 12 | (header: [*Price*], width: 20%, func: r => usd(r.price)), 13 | (header: [*Quantity*], width: 1.5in, func: r => r.quantity), 14 | ), 15 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 16 | stroke: none, 17 | ) 18 | ) 19 | 20 | -------------------------------------------------------------------------------- /doc/example-snippets/slice.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut, records-from-csv 2 | #import "usd.typ": usd 3 | #import "example-data/titanic.typ": titanic 4 | 5 | #let classes = ( 6 | "N/A", 7 | "First", 8 | "Second", 9 | "Third" 10 | ); 11 | 12 | #let titanic-head = titanic.slice(0, 5); 13 | 14 | #tabut( 15 | titanic-head, 16 | ( 17 | (header: [*Name*], func: r => r.Name), 18 | (header: [*Class*], func: r => classes.at(r.Pclass)), 19 | (header: [*Fare*], func: r => usd(r.Fare)), 20 | (header: [*Survived?*], func: r => ("No", "Yes").at(r.Survived)), 21 | ), 22 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 23 | stroke: none 24 | ) -------------------------------------------------------------------------------- /doc/example-snippets/import-csv.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut, rows-to-records 2 | #import "example-data/supplies.typ": supplies 3 | 4 | #let auto-type(input) = { 5 | let is-int = (input.match(regex("^-?\d+$")) != none); 6 | if is-int { return int(input); } 7 | let is-float = (input.match(regex("^-?(inf|nan|\d+|\d*(\.\d+))$")) != none); 8 | if is-float { return float(input) } 9 | input 10 | } 11 | 12 | #let titanic = { 13 | let titanic-raw = csv("example-data/titanic.csv"); 14 | rows-to-records( titanic-raw.first(), titanic-raw.slice(1, -1) ) 15 | .map( r => { 16 | let new-record = (:); 17 | for (k, v) in r.pairs() { new-record.insert(k, auto-type(v)); } 18 | new-record 19 | }) 20 | } -------------------------------------------------------------------------------- /doc/example-snippets/usd.typ: -------------------------------------------------------------------------------- 1 | #let usd(input) = { 2 | set align(right); 3 | 4 | let dollars-int = int(input); 5 | let cents-int = int((input - dollars-int) * 100); 6 | 7 | let dollars-str = str(dollars-int); 8 | let cents-str = str(cents-int); 9 | 10 | if cents-str.len() < 2 { 11 | cents-str = "0" + cents-str; 12 | } 13 | 14 | let dollars-chunks = (); 15 | let dollars-to-chunk = dollars-str.rev(); 16 | 17 | while dollars-to-chunk.len() > 3 { 18 | dollars-chunks.push( 19 | dollars-to-chunk.slice(0, 3).rev() 20 | ); 21 | 22 | dollars-to-chunk = dollars-to-chunk.slice(3); 23 | } 24 | 25 | dollars-chunks.push(dollars-to-chunk.rev()); 26 | 27 | [\$#{dollars-chunks.rev().join(",")}.#cents-str] 28 | } -------------------------------------------------------------------------------- /doc/example-snippets/tablex.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/tabut:<>": tabut-cells 2 | #import "usd.typ": usd 3 | #import "example-data/supplies.typ": supplies 4 | 5 | #import "@preview/tablex:0.0.8": tablex, rowspanx, colspanx 6 | 7 | #tablex( 8 | auto-vlines: false, 9 | header-rows: 2, 10 | 11 | /* --- header --- */ 12 | rowspanx(2)[*Name*], colspanx(2)[*Price*], (), rowspanx(2)[*Quantity*], 13 | (), [*Base*], [*W/Tax*], (), 14 | /* -------------- */ 15 | 16 | ..tabut-cells( 17 | supplies, 18 | ( 19 | (header: [], func: r => r.name), 20 | (header: [], func: r => usd(r.price)), 21 | (header: [], func: r => usd(r.price * 1.3)), 22 | (header: [], func: r => r.quantity), 23 | ), 24 | headers: false 25 | ) 26 | ) -------------------------------------------------------------------------------- /local-pub.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define the base directory 4 | BASE_DIR="$HOME/.local/share/typst/packages/preview" 5 | 6 | # Extract the name and version from typst.toml 7 | PACKAGE_NAME=$(awk -F'"' '/name/ {print $2; exit}' typst.toml) 8 | PACKAGE_VERSION=$(awk -F'"' '/version/ {print $2; exit}' typst.toml) 9 | 10 | # Construct the destination directory path 11 | DEST_DIR="$BASE_DIR/$PACKAGE_NAME/$PACKAGE_VERSION" 12 | 13 | # Create the destination directory if it doesn't exist 14 | mkdir -p "$DEST_DIR" 15 | 16 | # Clear the contents of the destination directory 17 | rm -rf "${DEST_DIR:?}"/* 18 | 19 | # Copy all contents of the current directory to the destination directory, excluding .git 20 | rsync -a --exclude='.git' ./ "$DEST_DIR/" 21 | 22 | # Output the operation completion 23 | echo "Contents copied to $DEST_DIR" 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Amelia Brenda Mowers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/compile-readme.yml: -------------------------------------------------------------------------------- 1 | name: Generate README 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the "main" branch 6 | push: 7 | branches: [ "main" ] 8 | paths-ignore: 9 | - 'README.md' 10 | - 'README.pdf' 11 | - 'doc/compiled-snippets/**' 12 | pull_request: 13 | branches: [ "main" ] 14 | paths-ignore: 15 | - 'README.md' 16 | - 'README.pdf' 17 | - 'doc/compiled-snippets/**' 18 | 19 | # Allows you to run this workflow manually from the Actions tab 20 | workflow_dispatch: 21 | 22 | permissions: 23 | contents: write 24 | 25 | jobs: 26 | build: 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 31 | - uses: actions/checkout@v3 32 | - uses: extractions/setup-just@v1 33 | - uses: typst-community/setup-typst@v3 34 | 35 | - name: Install Pandoc 36 | run: | 37 | wget https://github.com/jgm/pandoc/releases/download/3.1.11/pandoc-3.1.11-linux-amd64.tar.gz 38 | tar xvzf pandoc-3.1.11-linux-amd64.tar.gz 39 | sudo mv pandoc-3.1.11/bin/pandoc /usr/local/bin 40 | 41 | - name: Compile Readme 42 | run: just build-github-action 43 | 44 | - name: Commit and Push README 45 | run: | 46 | git config --local user.email "github-actions[bot]@github.com" 47 | git config --local user.name "github-actions[bot]" 48 | git add --force README.md 49 | git add --force README.pdf 50 | git add --force doc/compiled-snippets/ 51 | git commit -m "Update README by GitHub Action" 52 | git push 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | -------------------------------------------------------------------------------- /compile-snippets.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define the absolute paths for the source, destination, and temporary directories 4 | SOURCE_DIR="$PWD/doc/example-snippets" 5 | DEST_DIR="$PWD/doc/compiled-snippets" 6 | TMP_DIR="/dev/shm/my_temp_dir" 7 | #DEST_DIR="$PWD/doc/tmp" 8 | 9 | # Read the version number from typst.toml 10 | VERSION=$(grep '^version' "$PWD/typst.toml" | cut -d '"' -f2) 11 | 12 | # Check if VERSION is empty 13 | if [ -z "$VERSION" ]; then 14 | echo "Version not found in typst.toml" 15 | exit 1 16 | fi 17 | 18 | # Create and setup the temporary directory 19 | mkdir -p "$TMP_DIR" 20 | cp -r "$SOURCE_DIR/"* "$TMP_DIR" 21 | 22 | # Check if the destination directory exists, if not, create it 23 | if [ ! -d "$DEST_DIR" ]; then 24 | mkdir -p "$DEST_DIR" 25 | else 26 | # If the directory exists, clear its contents 27 | rm -rf "${DEST_DIR:?}"/* 28 | fi 29 | 30 | # Preprocessing step 31 | find "$SOURCE_DIR" -type f -name '*.typ' | while read -r file; do 32 | # Get the relative path from SOURCE_DIR 33 | relative_path="${file#$SOURCE_DIR/}" 34 | relative_dir=$(dirname "$relative_path") 35 | filename=$(basename "${file%.*}") 36 | 37 | # Create corresponding subdirectory in temporary directory 38 | mkdir -p "$TMP_DIR/$relative_dir" 39 | 40 | # Define the path for the temporary file 41 | temp_file="$TMP_DIR/$relative_path" 42 | 43 | echo "Pre-processing file: $relative_path" 44 | 45 | # Prepend the required string to the new temp file 46 | echo "#set page(background: box(width: 100%, height: 100%, fill: luma(97%)), width: auto, height: auto, margin: 2pt)" > "$temp_file" 47 | 48 | # Append the contents of the original file to the new temp file, replacing "<>" with the actual version number 49 | sed "s/<>/$VERSION/g" "$file" >> "$temp_file" 50 | done 51 | 52 | # Compilation step 53 | find "$TMP_DIR" -type f -name '*.typ' | while read -r temp_file; do 54 | # Get the relative path from TMP_DIR 55 | relative_path="${temp_file#$TMP_DIR/}" 56 | relative_dir=$(dirname "$relative_path") 57 | filename=$(basename "${temp_file%.*}") 58 | 59 | # Ensure the destination subdirectory exists 60 | mkdir -p "$DEST_DIR/$relative_dir" 61 | 62 | echo "Compiling file: $relative_path" 63 | 64 | # Compile the temp file to SVG format 65 | typst compile "$temp_file" "$DEST_DIR/$relative_dir/$filename.svg" 66 | done 67 | 68 | # Clean up: remove the temporary directory and its contents 69 | # rm -rf "$TMP_DIR" 70 | 71 | echo "All files have been processed." 72 | -------------------------------------------------------------------------------- /lib.typ: -------------------------------------------------------------------------------- 1 | #let col( 2 | header, 3 | function, 4 | width: auto, 5 | align: auto, 6 | ) = { 7 | ( 8 | header: header, 9 | func: function, 10 | width: width, 11 | align: align, 12 | ) 13 | } 14 | 15 | #let tabut-cells( 16 | data-raw, 17 | colDefs, 18 | columns: auto, 19 | align: auto, 20 | index: "_index", 21 | transpose: false, 22 | headers: true, 23 | ) = { 24 | 25 | let data = (); 26 | let i = 0; 27 | for record in data-raw { 28 | let new-record = record; 29 | if index != none { new-record.insert(index, i); } 30 | data.push(new-record); 31 | i = i + 1; 32 | } 33 | 34 | let entries = (); 35 | let colWidths = (); 36 | let colAlignments = (); 37 | 38 | if transpose { 39 | colWidths.push(auto); 40 | colAlignments.push(auto); 41 | 42 | for record in data { 43 | colWidths.push(auto); 44 | colAlignments.push(auto); 45 | } 46 | 47 | for colDef in colDefs { 48 | if headers { entries.push(colDef.header); } 49 | for record in data { 50 | entries.push([#(colDef.func)(record)]) 51 | } 52 | } 53 | } else { 54 | for colDef in colDefs { 55 | if colDef.keys().contains("width") { 56 | colWidths.push(colDef.width); 57 | } else { 58 | colWidths.push(auto); 59 | } 60 | if colDef.keys().contains("align") { 61 | colAlignments.push(colDef.align); 62 | } else { 63 | colAlignments.push(auto); 64 | } 65 | } 66 | 67 | for colDef in colDefs { 68 | if headers { entries.push(colDef.header); } 69 | } 70 | 71 | for record in data { 72 | for colDef in colDefs { 73 | entries.push([#(colDef.func)(record)]) 74 | } 75 | } 76 | } 77 | 78 | let output-named = (:) 79 | 80 | if columns == auto { 81 | output-named.columns = colWidths; 82 | } else if columns == none { 83 | // Do nothing 84 | } else { 85 | output-named.columns = columns; 86 | } 87 | 88 | if align == auto { 89 | output-named.align = colAlignments; 90 | } else if align == none { 91 | // Do nothing 92 | } else { 93 | output-named.align = align; 94 | } 95 | 96 | arguments( 97 | ..output-named, 98 | ..entries 99 | ); 100 | } 101 | 102 | #let tabut( 103 | data-raw, 104 | colDefs, 105 | columns: auto, 106 | align: auto, 107 | index: "_index", 108 | transpose: false, 109 | headers: true, 110 | ..tableArgs 111 | ) = { 112 | table( 113 | ..tabut-cells( 114 | data-raw, 115 | colDefs, 116 | columns: columns, 117 | align: align, 118 | index: index, 119 | headers: headers, 120 | transpose: transpose, 121 | ), 122 | ..tableArgs 123 | ) 124 | } 125 | 126 | #let rows-to-records(headers, rows, default: none) = { 127 | rows.map(r => { 128 | let record = (:); 129 | let i = 0; 130 | for header in headers { 131 | record.insert(header, r.at(i, default: default)); 132 | i = i + 1; 133 | } 134 | record 135 | }) 136 | } 137 | 138 | #let group(data, function) = { 139 | let groups = (); 140 | for record in data { 141 | let value = function(record); 142 | let group-pos = groups.position(g => g.value == value); 143 | if group-pos == none { 144 | let new-group = (value: value, group: ()); 145 | new-group.group.push(record); 146 | groups.push(new-group); 147 | } else { 148 | groups.at(group-pos).group.push(record) 149 | } 150 | } 151 | groups.sorted(key: r => r.value) 152 | } 153 | 154 | #let auto-type(input) = { 155 | 156 | let is-int = (input.match(regex("^-?\d+$")) != none); 157 | if is-int { return int(input); } 158 | 159 | let is-float = (input.match(regex("^-?(inf|nan|\d+|\d*(\.\d+))$")) != none); 160 | if is-float { return float(input) } 161 | 162 | input 163 | } 164 | 165 | #let records-from-csv(input) = { 166 | let data = { 167 | let data-raw = input; 168 | rows-to-records(data-raw.first(), data-raw.slice(1)) 169 | } 170 | data.map( r => { 171 | let new-record = (:); 172 | for (k, v) in r.pairs() { 173 | new-record.insert(k, auto-type(v)); 174 | } 175 | new-record 176 | }) 177 | } -------------------------------------------------------------------------------- /README.typ: -------------------------------------------------------------------------------- 1 | #let ex(input) = { 2 | set align(center); 3 | block( 4 | fill: luma(97%), 5 | inset: 8pt, 6 | radius: 4pt, 7 | breakable: false, 8 | [ 9 | #set align(left); 10 | #input 11 | ]) 12 | } 13 | 14 | #let snippet(filename) = {[ 15 | #let version = toml("typst.toml").package.version; 16 | #let snippet-code-path = "doc/example-snippets/" + filename + ".typ" 17 | #let snippet-image-path = "doc/compiled-snippets/" + filename + ".svg" 18 | 19 | #let data = xml(snippet-image-path).first(); 20 | #let size = ( 21 | height: float(data.attrs.height) * 1pt , 22 | width: float(data.attrs.width) * 1pt, 23 | ); 24 | 25 | #let content = read(snippet-code-path).replace("\r", "").replace("<>", version) 26 | #ex(raw(content, block: true, lang: "typ")) 27 | #ex(image(snippet-image-path, ..size)) 28 | ]} 29 | 30 | #let snippet-quiet(filename) = {[ 31 | #let version = toml("typst.toml").package.version; 32 | #let snippet-code-path = "doc/example-snippets/" + filename + ".typ" 33 | #let content = read(snippet-code-path).replace("\r", "").replace("<>", version) 34 | #ex(raw(content, block: true, lang: "typ")) 35 | ]} 36 | 37 | #let no-break(content) = { 38 | block(breakable: false, width:100%, content) 39 | } 40 | 41 | #let subsection(name) = { 42 | [== #name ] 43 | } 44 | 45 | #let label-text(content) = { 46 | label(lower(content.text.replace(" ", "-"))) 47 | } 48 | 49 | #let section(name) = { 50 | [= #name #label-text(name)] 51 | } 52 | 53 | #let subsection(name) = { 54 | [== #name #label-text(name)] 55 | } 56 | 57 | #no-break([ 58 | 59 | = Tabut 60 | 61 | _Powerful, Simple, Concise_ 62 | 63 | A Typst plugin for turning data into tables. 64 | 65 | == Outline 66 | 67 | #set list(marker: ([•], [◦])) 68 | 69 | #let sections = ( 70 | (name: [Examples], subs: ( 71 | [Input Format and Creation], 72 | [Basic Table], 73 | [Table Styling], 74 | [Header Formatting], 75 | [Remove Headers], 76 | [Cell Expressions and Formatting], 77 | [Index], 78 | [Transpose], 79 | [Alignment], 80 | [Column Width], 81 | [Get Cells Only], 82 | [Use with Tablex], 83 | )), 84 | (name: [Data Operation Examples], subs: ( 85 | [CSV Data], 86 | [Slice], 87 | [Sorting and Reversing], 88 | [Filter], 89 | [Aggregation using Map and Sum], 90 | [Grouping] 91 | )), 92 | (name: [Function Definitions], subs: ( 93 | [`tabut`], 94 | [`tabut-cells`], 95 | [`rows-to-records`], 96 | [`records-from-csv`], 97 | [`group`], 98 | )), 99 | ) 100 | 101 | #{ 102 | let items = (); 103 | for s in sections { 104 | let s-items = (); 105 | for ss in s.subs { 106 | s-items.push(link(label-text(ss), ss)); 107 | } 108 | items.push([ 109 | #link(label-text(s.name), s.name) 110 | #list(..s-items) 111 | ]) 112 | } 113 | list(..items, tight: true) 114 | } 115 | 116 | ]) 117 | 118 | #pagebreak(weak: true) 119 | 120 | #no-break([ 121 | 122 | #section([Examples]) 123 | 124 | ]) #no-break([ 125 | 126 | #subsection([Input Format and Creation]) 127 | 128 | The `tabut` function takes input in "record" format, an array of dictionaries, with each dictionary representing a single "object" or "record". 129 | 130 | In the example below, each record is a listing for an office supply product. 131 | 132 | #snippet-quiet("example-data/supplies") 133 | 134 | ]) #no-break([ 135 | 136 | #subsection([Basic Table]) 137 | 138 | Now create a basic table from the data. 139 | 140 | #snippet("basic") 141 | 142 | `funct` takes a function which generates content for a given cell corrosponding to the defined column for each record. 143 | `r` is the record, so `r => r.name` returns the `name` property of each record in the input data if it has one. 144 | 145 | ]) #no-break([ 146 | 147 | The philosphy of `tabut` is that the display of data should be simple and clearly defined, 148 | therefore each column and it's content and formatting should be defined within a single clear column defintion. 149 | One consequence is you can comment out, remove or move, any column easily, for example: 150 | 151 | #snippet("rearrange") 152 | 153 | ]) #no-break([ 154 | 155 | #subsection([Table Styling]) 156 | 157 | Any default Table style options can be tacked on and are passed to the final table function. 158 | 159 | #snippet("styling") 160 | 161 | ]) #no-break([ 162 | 163 | #subsection([Header Formatting]) 164 | 165 | You can pass any content or expression into the header property. 166 | 167 | #snippet("title") 168 | 169 | ]) #no-break([ 170 | 171 | #subsection([Remove Headers]) 172 | 173 | You can prevent from being generated with the `headers` paramater. This is useful with the `tabut-cells` function as demonstrated in it's section. 174 | 175 | #snippet("no-headers") 176 | 177 | ]) #no-break([ 178 | 179 | #subsection([Cell Expressions and Formatting]) 180 | 181 | Just like the headers, cell contents can be modified and formatted like any content in Typst. 182 | 183 | #snippet("format") 184 | 185 | ]) #no-break([ 186 | 187 | You can have the cell content function do calculations on a record property. 188 | 189 | #snippet("calculation") 190 | 191 | ]) #no-break([ 192 | 193 | Or even combine multiple record properties, go wild. 194 | 195 | #snippet("combine") 196 | 197 | ]) #no-break([ 198 | 199 | #subsection([Index]) 200 | 201 | `tabut` automatically adds an `_index` property to each record. 202 | 203 | #snippet("index") 204 | 205 | You can also prevent the `index` property being generated by setting it to `none`, 206 | or you can also set an alternate name of the index property as shown below. 207 | 208 | #snippet("index-alternate") 209 | 210 | ]) #no-break([ 211 | 212 | #subsection([Transpose]) 213 | 214 | This was annoying to implement, and I don't know when you'd actually use this, but here. 215 | 216 | #snippet("transpose") 217 | 218 | ]) #no-break([ 219 | 220 | #subsection([Alignment]) 221 | 222 | #snippet("align") 223 | 224 | You can also define Alignment manually as in the the standard Table Function. 225 | 226 | #snippet("align-manual") 227 | 228 | ]) #no-break([ 229 | 230 | #subsection([Column Width]) 231 | 232 | #snippet("width") 233 | 234 | You can also define Columns manually as in the the standard Table Function. 235 | 236 | #snippet("width-manual") 237 | 238 | ]) #no-break([ 239 | 240 | #subsection([Get Cells Only]) 241 | 242 | #snippet("only-cells") 243 | 244 | ]) #no-break([ 245 | 246 | #subsection([Use with Tablex]) 247 | 248 | #snippet("tablex") 249 | 250 | ]) 251 | 252 | #pagebreak(weak: true) 253 | 254 | #no-break([ 255 | 256 | #section([Data Operation Examples]) 257 | 258 | While technically seperate from table display, the following are examples of how to perform operations on data before it is displayed with `tabut`. 259 | 260 | Since `tabut` assumes an "array of dictionaries" format, then most data operations can be performed easily with Typst's native array functions. `tabut` also provides several functions to provide additional functionality. 261 | 262 | ]) #no-break([ 263 | 264 | #subsection([CSV Data]) 265 | 266 | By default, imported CSV gives a "rows" or "array of arrays" data format, which can not be directly used by `tabut`. 267 | To convert, `tabut` includes a function `rows-to-records` demonstrated below. 268 | 269 | #snippet-quiet("import-csv-raw") 270 | 271 | Imported CSV data are all strings, so it's usefull to convert them to `int` or `float` when possible. 272 | 273 | #snippet-quiet("import-csv") 274 | 275 | `tabut` includes a function, `records-from-csv`, to automatically perform this process. 276 | 277 | #snippet-quiet("import-csv-easy") 278 | 279 | ]) #no-break([ 280 | 281 | #subsection([Slice]) 282 | 283 | #snippet("slice") 284 | 285 | ]) #no-break([ 286 | 287 | #subsection([Sorting and Reversing]) 288 | 289 | #snippet("sort") 290 | 291 | ]) #no-break([ 292 | 293 | #subsection([Filter]) 294 | 295 | #snippet("filter") 296 | 297 | ]) #no-break([ 298 | 299 | #subsection([Aggregation using Map and Sum]) 300 | 301 | #snippet("aggregation") 302 | 303 | ]) #no-break([ 304 | 305 | #subsection([Grouping]) 306 | 307 | #snippet("group") 308 | 309 | #snippet("group-aggregation") 310 | 311 | ]) 312 | 313 | #pagebreak(weak: true) 314 | 315 | #no-break([ 316 | 317 | #section([Function Definitions]) 318 | 319 | ]) #no-break([ 320 | 321 | #subsection([`tabut`]) 322 | 323 | Takes data and column definitions and outputs a table. 324 | 325 | #ex(```typc 326 | tabut( 327 | data-raw, 328 | colDefs, 329 | columns: auto, 330 | align: auto, 331 | index: "_index", 332 | transpose: false, 333 | headers: true, 334 | ..tableArgs 335 | ) -> content 336 | ```) 337 | 338 | === Parameters 339 | / `data-raw`: This is the raw data that will be used to generate the table. The data is expected to be in an array of dictionaries, where each dictionary represents a single record or object. 340 | 341 | / `colDefs`: These are the column definitions. An array of dictionaries, each representing column definition. Must include the properties `header` and a `func`. `header` expects content, and specifies the label of the column. `func` expects a function, the function takes a record dictionary as input and returns the value to be displayed in the cell corresponding to that record and column. There are also two optional properties; `align` sets the alignment of the content within the cells of the column, `width` sets the width of the column. 342 | 343 | / `columns`: (optional, default: `auto`) Specifies the column widths. If set to `auto`, the function automatically generates column widths by each column's column definition. Otherwise functions exactly the `columns` paramater of the standard Typst `table` function. Unlike the `tabut-cells` setting this to `none` will break. 344 | 345 | / `align`: (optional, default: `auto`) Specifies the column alignment. If set to `auto`, the function automatically generates column alignment by each column's column definition. If set to `none` no `align` property is added to the output arg. Otherwise functions exactly the `align` paramater of the standard Typst `table` function. 346 | 347 | / `index`: (optional, default: `"_index"`) Specifies the property name for the index of each record. By default, an `_index` property is automatically added to each record. If set to `none`, no index property is added. 348 | 349 | / `transpose`: (optional, default: `false`) If set to `true`, transposes the table, swapping rows and columns. 350 | 351 | / `headers`: (optional, default: `true`) Determines whether headers should be included in the output. If set to `false`, headers are not generated. 352 | 353 | / `tableArgs`: (optional) Any additional arguments are passed to the `table` function, can be used for styling or anything else. 354 | 355 | ]) #no-break([ 356 | 357 | #subsection([`tabut-cells`]) 358 | 359 | The `tabut-cells` function functions as `tabut`, but returns `arguments` for use in either the standard `table` function or other tools such as `tablex`. If you just want the array of cells, use the `pos` function on the returned value, ex `tabut-cells(...).pos`. 360 | 361 | `tabut-cells` is particularly useful when you need to generate only the cell contents of a table or when these cells need to be passed to another function for further processing or customization. 362 | 363 | === Function Signature 364 | #ex(```typc 365 | tabut-cells( 366 | data-raw, 367 | colDefs, 368 | columns: auto, 369 | align: auto, 370 | index: "_index", 371 | transpose: false, 372 | headers: true, 373 | ) -> arguments 374 | ```) 375 | 376 | === Parameters 377 | / `data-raw`: This is the raw data that will be used to generate the table. The data is expected to be in an array of dictionaries, where each dictionary represents a single record or object. 378 | 379 | / `colDefs`: These are the column definitions. An array of dictionaries, each representing column definition. Must include the properties `header` and a `func`. `header` expects content, and specifies the label of the column. `func` expects a function, the function takes a record dictionary as input and returns the value to be displayed in the cell corresponding to that record and column. There are also two optional properties; `align` sets the alignment of the content within the cells of the column, `width` sets the width of the column. 380 | 381 | / `columns`: (optional, default: `auto`) Specifies the column widths. If set to `auto`, the function automatically generates column widths by each column's column definition. If set to `none` no `column` property is added to the output arg. Otherwise functions exactly the `columns` paramater of the standard typst `table` function. 382 | 383 | / `align`: (optional, default: `auto`) Specifies the column alignment. If set to `auto`, the function automatically generates column alignment by each column's column definition. If set to `none` no `align` property is added to the output arg. Otherwise functions exactly the `align` paramater of the standard typst `table` function. 384 | 385 | / `index`: (optional, default: `"_index"`) Specifies the property name for the index of each record. By default, an `_index` property is automatically added to each record. If set to `none`, no index property is added. 386 | 387 | / `transpose`: (optional, default: `false`) If set to `true`, transposes the table, swapping rows and columns. 388 | 389 | / `headers`: (optional, default: `true`) Determines whether headers should be included in the output. If set to `false`, headers are not generated. 390 | 391 | ]) #no-break([ 392 | 393 | #subsection([`records-from-csv`]) 394 | 395 | Automatically converts a CSV data into an array of records. 396 | 397 | #ex(```typc 398 | records-from-csv( 399 | data 400 | ) -> array 401 | ```) 402 | 403 | === Parameters 404 | / `data`: The CSV data that needs to be converted, this can be obtained using the native `csv` function, like `records-from-csv(csv(file-path))`. 405 | 406 | This function simplifies the process of converting CSV data into a format compatible with `tabut`. It reads the CSV data, extracts the headers, and converts each row into a dictionary, using the headers as keys. 407 | 408 | It also automatically converts data into floats or integers when possible. 409 | 410 | ]) #no-break([ 411 | 412 | #subsection([`rows-to-records`]) 413 | 414 | Converts rows of data into an array of records based on specified headers. 415 | 416 | This function is useful for converting data in a "rows" format (commonly found in CSV files) into an array of dictionaries format, which is required for `tabut` and allows easy data processing using the built in array functions. 417 | 418 | #ex(```typc 419 | rows-to-records( 420 | headers, 421 | rows, 422 | default: none 423 | ) -> array 424 | ```) 425 | 426 | === Parameters 427 | / `headers`: An array representing the headers of the table. Each item in this array corresponds to a column header. 428 | 429 | / `rows`: An array of arrays, each representing a row of data. Each sub-array contains the cell data for a corresponding row. 430 | 431 | / `default`: (optional, default: `none`) A default value to use when a cell is empty or there is an error. 432 | 433 | ]) #no-break([ 434 | 435 | #subsection([`group`]) 436 | 437 | Groups data based on a specified function and returns an array of grouped records. 438 | 439 | #ex(```typc 440 | group( 441 | data, 442 | function 443 | ) -> array 444 | ```) 445 | 446 | === Parameters 447 | / `data`: An array of dictionaries. Each dictionary represents a single record or object. 448 | 449 | / `function`: A function that takes a record as input and returns a value based on which the grouping is to be performed. 450 | 451 | This function iterates over each record in the `data`, applies the `function` to determine the grouping value, and organizes the records into groups based on this value. Each group record is represented as a dictionary with two properties: `value` (the result of the grouping function) and `group` (an array of records belonging to this group). 452 | 453 | In the context of `tabut`, the `group` function is particularly useful for creating summary tables where records need to be categorized and aggregated based on certain criteria, such as calculating total or average values for each group. 454 | 455 | ]) 456 | 457 | 458 | 459 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Tabut 4 | 5 | *Powerful, Simple, Concise* 6 | 7 | A Typst plugin for turning data into tables. 8 | 9 | ## Outline 10 | 11 | - [Examples](#examples) 12 | 13 | - [Input Format and Creation](#input-format-and-creation) 14 | 15 | - [Basic Table](#basic-table) 16 | 17 | - [Table Styling](#table-styling) 18 | 19 | - [Header Formatting](#header-formatting) 20 | 21 | - [Remove Headers](#remove-headers) 22 | 23 | - [Cell Expressions and Formatting](#cell-expressions-and-formatting) 24 | 25 | - [Index](#index) 26 | 27 | - [Transpose](#transpose) 28 | 29 | - [Alignment](#alignment) 30 | 31 | - [Column Width](#column-width) 32 | 33 | - [Get Cells Only](#get-cells-only) 34 | 35 | - [Use with Tablex](#use-with-tablex) 36 | 37 | - [Data Operation Examples](#data-operation-examples) 38 | 39 | - [CSV Data](#csv-data) 40 | 41 | - [Slice](#slice) 42 | 43 | - [Sorting and Reversing](#sorting-and-reversing) 44 | 45 | - [Filter](#filter) 46 | 47 | - [Aggregation using Map and Sum](#aggregation-using-map-and-sum) 48 | 49 | - [Grouping](#grouping) 50 | 51 | - [Function Definitions](#function-definitions) 52 | 53 | - [`tabut`](#tabut) 54 | 55 | - [`tabut-cells`](#tabut-cells) 56 | 57 | - [`rows-to-records`](#rows-to-records) 58 | 59 | - [`records-from-csv`](#records-from-csv) 60 | 61 | - [`group`](#group) 62 | 63 |
64 | 65 |
66 | 67 | # Examples 68 | 69 |
70 | 71 |
72 | 73 | ## Input Format and Creation 74 | 75 | The `tabut` function takes input in “record” format, an array of 76 | dictionaries, with each dictionary representing a single “object” or 77 | “record”. 78 | 79 | In the example below, each record is a listing for an office supply 80 | product. 81 | 82 |
83 | 84 | ``` typ 85 | #let supplies = ( 86 | (name: "Notebook", price: 3.49, quantity: 5), 87 | (name: "Ballpoint Pens", price: 5.99, quantity: 2), 88 | (name: "Printer Paper", price: 6.99, quantity: 3), 89 | ) 90 | ``` 91 | 92 |
93 | 94 |
95 | 96 |
97 | 98 | ## Basic Table 99 | 100 | Now create a basic table from the data. 101 | 102 |
103 | 104 | ``` typ 105 | #import "@preview/tabut:1.0.2": tabut 106 | #import "example-data/supplies.typ": supplies 107 | 108 | #tabut( 109 | supplies, // the source of the data used to generate the table 110 | ( // column definitions 111 | ( 112 | header: [Name], // label, takes content. 113 | func: r => r.name // generates the cell content. 114 | ), 115 | (header: [Price], func: r => r.price), 116 | (header: [Quantity], func: r => r.quantity), 117 | ) 118 | ) 119 | ``` 120 | 121 |
122 | 123 |
124 | 125 | 127 | 128 |
129 | 130 | `funct` takes a function which generates content for a given cell 131 | corrosponding to the defined column for each record. `r` is the record, 132 | so `r => r.name` returns the `name` property of each record in the input 133 | data if it has one. 134 | 135 |
136 | 137 |
138 | 139 | The philosphy of `tabut` is that the display of data should be simple 140 | and clearly defined, therefore each column and it’s content and 141 | formatting should be defined within a single clear column defintion. One 142 | consequence is you can comment out, remove or move, any column easily, 143 | for example: 144 | 145 |
146 | 147 | ``` typ 148 | #import "@preview/tabut:1.0.2": tabut 149 | #import "example-data/supplies.typ": supplies 150 | 151 | #tabut( 152 | supplies, 153 | ( 154 | (header: [Price], func: r => r.price), // This column is moved to the front 155 | (header: [Name], func: r => r.name), 156 | (header: [Name 2], func: r => r.name), // copied 157 | // (header: [Quantity], func: r => r.quantity), // removed via comment 158 | ) 159 | ) 160 | ``` 161 | 162 |
163 | 164 |
165 | 166 | 168 | 169 |
170 | 171 |
172 | 173 |
174 | 175 | ## Table Styling 176 | 177 | Any default Table style options can be tacked on and are passed to the 178 | final table function. 179 | 180 |
181 | 182 | ``` typ 183 | #import "@preview/tabut:1.0.2": tabut 184 | #import "example-data/supplies.typ": supplies 185 | 186 | #tabut( 187 | supplies, 188 | ( 189 | (header: [Name], func: r => r.name), 190 | (header: [Price], func: r => r.price), 191 | (header: [Quantity], func: r => r.quantity), 192 | ), 193 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 194 | stroke: none 195 | ) 196 | ``` 197 | 198 |
199 | 200 |
201 | 202 | 204 | 205 |
206 | 207 |
208 | 209 |
210 | 211 | ## Header Formatting 212 | 213 | You can pass any content or expression into the header property. 214 | 215 |
216 | 217 | ``` typ 218 | #import "@preview/tabut:1.0.2": tabut 219 | #import "example-data/supplies.typ": supplies 220 | 221 | #let fmt(it) = { 222 | heading( 223 | outlined: false, 224 | upper(it) 225 | ) 226 | } 227 | 228 | #tabut( 229 | supplies, 230 | ( 231 | (header: fmt([Name]), func: r => r.name ), 232 | (header: fmt([Price]), func: r => r.price), 233 | (header: fmt([Quantity]), func: r => r.quantity), 234 | ), 235 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 236 | stroke: none 237 | ) 238 | ``` 239 | 240 |
241 | 242 |
243 | 244 | 246 | 247 |
248 | 249 |
250 | 251 |
252 | 253 | ## Remove Headers 254 | 255 | You can prevent from being generated with the `headers` paramater. This 256 | is useful with the `tabut-cells` function as demonstrated in it’s 257 | section. 258 | 259 |
260 | 261 | ``` typ 262 | #import "@preview/tabut:1.0.2": tabut 263 | #import "example-data/supplies.typ": supplies 264 | 265 | #tabut( 266 | supplies, 267 | ( 268 | (header: [*Name*], func: r => r.name), 269 | (header: [*Price*], func: r => r.price), 270 | (header: [*Quantity*], func: r => r.quantity), 271 | ), 272 | headers: false, // Prevents Headers from being generated 273 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 274 | stroke: none, 275 | ) 276 | ``` 277 | 278 |
279 | 280 |
281 | 282 | 284 | 285 |
286 | 287 |
288 | 289 |
290 | 291 | ## Cell Expressions and Formatting 292 | 293 | Just like the headers, cell contents can be modified and formatted like 294 | any content in Typst. 295 | 296 |
297 | 298 | ``` typ 299 | #import "@preview/tabut:1.0.2": tabut 300 | #import "usd.typ": usd 301 | #import "example-data/supplies.typ": supplies 302 | 303 | #tabut( 304 | supplies, 305 | ( 306 | (header: [*Name*], func: r => r.name ), 307 | (header: [*Price*], func: r => usd(r.price)), 308 | ), 309 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 310 | stroke: none 311 | ) 312 | ``` 313 | 314 |
315 | 316 |
317 | 318 | 320 | 321 |
322 | 323 |
324 | 325 |
326 | 327 | You can have the cell content function do calculations on a record 328 | property. 329 | 330 |
331 | 332 | ``` typ 333 | #import "@preview/tabut:1.0.2": tabut 334 | #import "usd.typ": usd 335 | #import "example-data/supplies.typ": supplies 336 | 337 | #tabut( 338 | supplies, 339 | ( 340 | (header: [*Name*], func: r => r.name ), 341 | (header: [*Price*], func: r => usd(r.price)), 342 | (header: [*Tax*], func: r => usd(r.price * .2)), 343 | (header: [*Total*], func: r => usd(r.price * 1.2)), 344 | ), 345 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 346 | stroke: none 347 | ) 348 | ``` 349 | 350 |
351 | 352 |
353 | 354 | 356 | 357 |
358 | 359 |
360 | 361 |
362 | 363 | Or even combine multiple record properties, go wild. 364 | 365 |
366 | 367 | ``` typ 368 | #import "@preview/tabut:1.0.2": tabut 369 | 370 | #let employees = ( 371 | (id: 3251, first: "Alice", last: "Smith", middle: "Jane"), 372 | (id: 4872, first: "Carlos", last: "Garcia", middle: "Luis"), 373 | (id: 5639, first: "Evelyn", last: "Chen", middle: "Ming") 374 | ); 375 | 376 | #tabut( 377 | employees, 378 | ( 379 | (header: [*ID*], func: r => r.id ), 380 | (header: [*Full Name*], func: r => [#r.first #r.middle.first(), #r.last] ), 381 | ), 382 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 383 | stroke: none 384 | ) 385 | ``` 386 | 387 |
388 | 389 |
390 | 391 | 393 | 394 |
395 | 396 |
397 | 398 |
399 | 400 | ## Index 401 | 402 | `tabut` automatically adds an `_index` property to each record. 403 | 404 |
405 | 406 | ``` typ 407 | #import "@preview/tabut:1.0.2": tabut 408 | #import "example-data/supplies.typ": supplies 409 | 410 | #tabut( 411 | supplies, 412 | ( 413 | (header: [*\#*], func: r => r._index), 414 | (header: [*Name*], func: r => r.name ), 415 | ), 416 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 417 | stroke: none 418 | ) 419 | ``` 420 | 421 |
422 | 423 |
424 | 425 | 427 | 428 |
429 | 430 | You can also prevent the `index` property being generated by setting it 431 | to `none`, or you can also set an alternate name of the index property 432 | as shown below. 433 | 434 |
435 | 436 | ``` typ 437 | #import "@preview/tabut:1.0.2": tabut 438 | #import "example-data/supplies.typ": supplies 439 | 440 | #tabut( 441 | supplies, 442 | ( 443 | (header: [*\#*], func: r => r.index-alt ), 444 | (header: [*Name*], func: r => r.name ), 445 | ), 446 | index: "index-alt", // set an aternate name for the automatically generated index property. 447 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 448 | stroke: none 449 | ) 450 | ``` 451 | 452 |
453 | 454 |
455 | 456 | 458 | 459 |
460 | 461 |
462 | 463 |
464 | 465 | ## Transpose 466 | 467 | This was annoying to implement, and I don’t know when you’d actually use 468 | this, but here. 469 | 470 |
471 | 472 | ``` typ 473 | #import "@preview/tabut:1.0.2": tabut 474 | #import "usd.typ": usd 475 | #import "example-data/supplies.typ": supplies 476 | 477 | #tabut( 478 | supplies, 479 | ( 480 | (header: [*\#*], func: r => r._index), 481 | (header: [*Name*], func: r => r.name), 482 | (header: [*Price*], func: r => usd(r.price)), 483 | (header: [*Quantity*], func: r => r.quantity), 484 | ), 485 | transpose: true, // set optional name arg `transpose` to `true` 486 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 487 | stroke: none 488 | ) 489 | ``` 490 | 491 |
492 | 493 |
494 | 495 | 497 | 498 |
499 | 500 |
501 | 502 |
503 | 504 | ## Alignment 505 | 506 |
507 | 508 | ``` typ 509 | #import "@preview/tabut:1.0.2": tabut 510 | #import "usd.typ": usd 511 | #import "example-data/supplies.typ": supplies 512 | 513 | #tabut( 514 | supplies, 515 | ( // Include `align` as an optional arg to a column def 516 | (header: [*\#*], func: r => r._index), 517 | (header: [*Name*], align: right, func: r => r.name), 518 | (header: [*Price*], align: right, func: r => usd(r.price)), 519 | (header: [*Quantity*], align: right, func: r => r.quantity), 520 | ), 521 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 522 | stroke: none 523 | ) 524 | ``` 525 | 526 |
527 | 528 |
529 | 530 | 532 | 533 |
534 | 535 | You can also define Alignment manually as in the the standard Table 536 | Function. 537 | 538 |
539 | 540 | ``` typ 541 | #import "@preview/tabut:1.0.2": tabut 542 | #import "usd.typ": usd 543 | #import "example-data/supplies.typ": supplies 544 | 545 | #tabut( 546 | supplies, 547 | ( 548 | (header: [*\#*], func: r => r._index), 549 | (header: [*Name*], func: r => r.name), 550 | (header: [*Price*], func: r => usd(r.price)), 551 | (header: [*Quantity*], func: r => r.quantity), 552 | ), 553 | align: (auto, right, right, right), // Alignment defined as in standard table function 554 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 555 | stroke: none 556 | ) 557 | ``` 558 | 559 |
560 | 561 |
562 | 563 | 565 | 566 |
567 | 568 |
569 | 570 |
571 | 572 | ## Column Width 573 | 574 |
575 | 576 | ``` typ 577 | #import "@preview/tabut:1.0.2": tabut 578 | #import "usd.typ": usd 579 | #import "example-data/supplies.typ": supplies 580 | 581 | #box( 582 | width: 300pt, 583 | tabut( 584 | supplies, 585 | ( // Include `width` as an optional arg to a column def 586 | (header: [*\#*], func: r => r._index), 587 | (header: [*Name*], width: 1fr, func: r => r.name), 588 | (header: [*Price*], width: 20%, func: r => usd(r.price)), 589 | (header: [*Quantity*], width: 1.5in, func: r => r.quantity), 590 | ), 591 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 592 | stroke: none, 593 | ) 594 | ) 595 | 596 | ``` 597 | 598 |
599 | 600 |
601 | 602 | 604 | 605 |
606 | 607 | You can also define Columns manually as in the the standard Table 608 | Function. 609 | 610 |
611 | 612 | ``` typ 613 | #import "@preview/tabut:1.0.2": tabut 614 | #import "usd.typ": usd 615 | #import "example-data/supplies.typ": supplies 616 | 617 | #box( 618 | width: 300pt, 619 | tabut( 620 | supplies, 621 | ( 622 | (header: [*\#*], func: r => r._index), 623 | (header: [*Name*], func: r => r.name), 624 | (header: [*Price*], func: r => usd(r.price)), 625 | (header: [*Quantity*], func: r => r.quantity), 626 | ), 627 | columns: (auto, 1fr, 20%, 1.5in), // Columns defined as in standard table 628 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 629 | stroke: none, 630 | ) 631 | ) 632 | 633 | ``` 634 | 635 |
636 | 637 |
638 | 639 | 641 | 642 |
643 | 644 |
645 | 646 |
647 | 648 | ## Get Cells Only 649 | 650 |
651 | 652 | ``` typ 653 | #import "@preview/tabut:1.0.2": tabut-cells 654 | #import "usd.typ": usd 655 | #import "example-data/supplies.typ": supplies 656 | 657 | #tabut-cells( 658 | supplies, 659 | ( 660 | (header: [Name], func: r => r.name), 661 | (header: [Price], func: r => usd(r.price)), 662 | (header: [Quantity], func: r => r.quantity), 663 | ) 664 | ) 665 | ``` 666 | 667 |
668 | 669 |
670 | 671 | 673 | 674 |
675 | 676 |
677 | 678 |
679 | 680 | ## Use with Tablex 681 | 682 |
683 | 684 | ``` typ 685 | #import "@preview/tabut:1.0.2": tabut-cells 686 | #import "usd.typ": usd 687 | #import "example-data/supplies.typ": supplies 688 | 689 | #import "@preview/tablex:0.0.8": tablex, rowspanx, colspanx 690 | 691 | #tablex( 692 | auto-vlines: false, 693 | header-rows: 2, 694 | 695 | /* --- header --- */ 696 | rowspanx(2)[*Name*], colspanx(2)[*Price*], (), rowspanx(2)[*Quantity*], 697 | (), [*Base*], [*W/Tax*], (), 698 | /* -------------- */ 699 | 700 | ..tabut-cells( 701 | supplies, 702 | ( 703 | (header: [], func: r => r.name), 704 | (header: [], func: r => usd(r.price)), 705 | (header: [], func: r => usd(r.price * 1.3)), 706 | (header: [], func: r => r.quantity), 707 | ), 708 | headers: false 709 | ) 710 | ) 711 | ``` 712 | 713 |
714 | 715 |
716 | 717 | 719 | 720 |
721 | 722 |
723 | 724 |
725 | 726 | # Data Operation Examples 727 | 728 | While technically seperate from table display, the following are 729 | examples of how to perform operations on data before it is displayed 730 | with `tabut`. 731 | 732 | Since `tabut` assumes an “array of dictionaries” format, then most data 733 | operations can be performed easily with Typst’s native array functions. 734 | `tabut` also provides several functions to provide additional 735 | functionality. 736 | 737 |
738 | 739 |
740 | 741 | ## CSV Data 742 | 743 | By default, imported CSV gives a “rows” or “array of arrays” data 744 | format, which can not be directly used by `tabut`. To convert, `tabut` 745 | includes a function `rows-to-records` demonstrated below. 746 | 747 |
748 | 749 | ``` typ 750 | #import "@preview/tabut:1.0.2": tabut, rows-to-records 751 | #import "example-data/supplies.typ": supplies 752 | 753 | #let titanic = { 754 | let titanic-raw = csv("example-data/titanic.csv"); 755 | rows-to-records( 756 | titanic-raw.first(), // The header row 757 | titanic-raw.slice(1, -1), // The rest of the rows 758 | ) 759 | } 760 | ``` 761 | 762 |
763 | 764 | Imported CSV data are all strings, so it’s usefull to convert them to 765 | `int` or `float` when possible. 766 | 767 |
768 | 769 | ``` typ 770 | #import "@preview/tabut:1.0.2": tabut, rows-to-records 771 | #import "example-data/supplies.typ": supplies 772 | 773 | #let auto-type(input) = { 774 | let is-int = (input.match(regex("^-?\d+$")) != none); 775 | if is-int { return int(input); } 776 | let is-float = (input.match(regex("^-?(inf|nan|\d+|\d*(\.\d+))$")) != none); 777 | if is-float { return float(input) } 778 | input 779 | } 780 | 781 | #let titanic = { 782 | let titanic-raw = csv("example-data/titanic.csv"); 783 | rows-to-records( titanic-raw.first(), titanic-raw.slice(1, -1) ) 784 | .map( r => { 785 | let new-record = (:); 786 | for (k, v) in r.pairs() { new-record.insert(k, auto-type(v)); } 787 | new-record 788 | }) 789 | } 790 | ``` 791 | 792 |
793 | 794 | `tabut` includes a function, `records-from-csv`, to automatically 795 | perform this process. 796 | 797 |
798 | 799 | ``` typ 800 | #import "@preview/tabut:1.0.2": records-from-csv 801 | 802 | #let titanic = records-from-csv(csv("example-data/titanic.csv")); 803 | ``` 804 | 805 |
806 | 807 |
808 | 809 |
810 | 811 | ## Slice 812 | 813 |
814 | 815 | ``` typ 816 | #import "@preview/tabut:1.0.2": tabut, records-from-csv 817 | #import "usd.typ": usd 818 | #import "example-data/titanic.typ": titanic 819 | 820 | #let classes = ( 821 | "N/A", 822 | "First", 823 | "Second", 824 | "Third" 825 | ); 826 | 827 | #let titanic-head = titanic.slice(0, 5); 828 | 829 | #tabut( 830 | titanic-head, 831 | ( 832 | (header: [*Name*], func: r => r.Name), 833 | (header: [*Class*], func: r => classes.at(r.Pclass)), 834 | (header: [*Fare*], func: r => usd(r.Fare)), 835 | (header: [*Survived?*], func: r => ("No", "Yes").at(r.Survived)), 836 | ), 837 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 838 | stroke: none 839 | ) 840 | ``` 841 | 842 |
843 | 844 |
845 | 846 | 848 | 849 |
850 | 851 |
852 | 853 |
854 | 855 | ## Sorting and Reversing 856 | 857 |
858 | 859 | ``` typ 860 | #import "@preview/tabut:1.0.2": tabut 861 | #import "usd.typ": usd 862 | #import "example-data/titanic.typ": titanic, classes 863 | 864 | #tabut( 865 | titanic 866 | .sorted(key: r => r.Fare) 867 | .rev() 868 | .slice(0, 5), 869 | ( 870 | (header: [*Name*], func: r => r.Name), 871 | (header: [*Class*], func: r => classes.at(r.Pclass)), 872 | (header: [*Fare*], func: r => usd(r.Fare)), 873 | (header: [*Survived?*], func: r => ("No", "Yes").at(r.Survived)), 874 | ), 875 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 876 | stroke: none 877 | ) 878 | ``` 879 | 880 |
881 | 882 |
883 | 884 | 886 | 887 |
888 | 889 |
890 | 891 |
892 | 893 | ## Filter 894 | 895 |
896 | 897 | ``` typ 898 | #import "@preview/tabut:1.0.2": tabut 899 | #import "usd.typ": usd 900 | #import "example-data/titanic.typ": titanic, classes 901 | 902 | #tabut( 903 | titanic 904 | .filter(r => r.Pclass == 1) 905 | .slice(0, 5), 906 | ( 907 | (header: [*Name*], func: r => r.Name), 908 | (header: [*Class*], func: r => classes.at(r.Pclass)), 909 | (header: [*Fare*], func: r => usd(r.Fare)), 910 | (header: [*Survived?*], func: r => ("No", "Yes").at(r.Survived)), 911 | ), 912 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 913 | stroke: none 914 | ) 915 | ``` 916 | 917 |
918 | 919 |
920 | 921 | 923 | 924 |
925 | 926 |
927 | 928 |
929 | 930 | ## Aggregation using Map and Sum 931 | 932 |
933 | 934 | ``` typ 935 | #import "usd.typ": usd 936 | #import "example-data/titanic.typ": titanic, classes 937 | 938 | #table( 939 | columns: (auto, auto), 940 | [*Fare, Total:*], [#usd(titanic.map(r => r.Fare).sum())], 941 | [*Fare, Avg:*], [#usd(titanic.map(r => r.Fare).sum() / titanic.len())], 942 | stroke: none 943 | ) 944 | ``` 945 | 946 |
947 | 948 |
949 | 950 | 952 | 953 |
954 | 955 |
956 | 957 |
958 | 959 | ## Grouping 960 | 961 |
962 | 963 | ``` typ 964 | #import "@preview/tabut:1.0.2": tabut, group 965 | #import "example-data/titanic.typ": titanic, classes 966 | 967 | #tabut( 968 | group(titanic, r => r.Pclass), 969 | ( 970 | (header: [*Class*], func: r => classes.at(r.value)), 971 | (header: [*Passengers*], func: r => r.group.len()), 972 | ), 973 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 974 | stroke: none 975 | ) 976 | ``` 977 | 978 |
979 | 980 |
981 | 982 | 984 | 985 |
986 | 987 |
988 | 989 | ``` typ 990 | #import "@preview/tabut:1.0.2": tabut, group 991 | #import "usd.typ": usd 992 | #import "example-data/titanic.typ": titanic, classes 993 | 994 | #tabut( 995 | group(titanic, r => r.Pclass), 996 | ( 997 | (header: [*Class*], func: r => classes.at(r.value)), 998 | (header: [*Total Fare*], func: r => usd(r.group.map(r => r.Fare).sum())), 999 | ( 1000 | header: [*Avg Fare*], 1001 | func: r => usd(r.group.map(r => r.Fare).sum() / r.group.len()) 1002 | ), 1003 | ), 1004 | fill: (_, row) => if calc.odd(row) { luma(240) } else { luma(220) }, 1005 | stroke: none 1006 | ) 1007 | ``` 1008 | 1009 |
1010 | 1011 |
1012 | 1013 | 1015 | 1016 |
1017 | 1018 |
1019 | 1020 |
1021 | 1022 | # Function Definitions 1023 | 1024 |
1025 | 1026 |
1027 | 1028 | ## `tabut` 1029 | 1030 | Takes data and column definitions and outputs a table. 1031 | 1032 |
1033 | 1034 | ``` typc 1035 | tabut( 1036 | data-raw, 1037 | colDefs, 1038 | columns: auto, 1039 | align: auto, 1040 | index: "_index", 1041 | transpose: false, 1042 | headers: true, 1043 | ..tableArgs 1044 | ) -> content 1045 | ``` 1046 | 1047 |
1048 | 1049 | ### Parameters 1050 | 1051 | `data-raw` 1052 | This is the raw data that will be used to generate the table. The data 1053 | is expected to be in an array of dictionaries, where each dictionary 1054 | represents a single record or object. 1055 | 1056 | `colDefs` 1057 | These are the column definitions. An array of dictionaries, each 1058 | representing column definition. Must include the properties `header` and 1059 | a `func`. `header` expects content, and specifies the label of the 1060 | column. `func` expects a function, the function takes a record 1061 | dictionary as input and returns the value to be displayed in the cell 1062 | corresponding to that record and column. There are also two optional 1063 | properties; `align` sets the alignment of the content within the cells 1064 | of the column, `width` sets the width of the column. 1065 | 1066 | `columns` 1067 | (optional, default: `auto`) Specifies the column widths. If set to 1068 | `auto`, the function automatically generates column widths by each 1069 | column’s column definition. Otherwise functions exactly the `columns` 1070 | paramater of the standard Typst `table` function. Unlike the 1071 | `tabut-cells` setting this to `none` will break. 1072 | 1073 | `align` 1074 | (optional, default: `auto`) Specifies the column alignment. If set to 1075 | `auto`, the function automatically generates column alignment by each 1076 | column’s column definition. If set to `none` no `align` property is 1077 | added to the output arg. Otherwise functions exactly the `align` 1078 | paramater of the standard Typst `table` function. 1079 | 1080 | `index` 1081 | (optional, default: `"_index"`) Specifies the property name for the 1082 | index of each record. By default, an `_index` property is automatically 1083 | added to each record. If set to `none`, no index property is added. 1084 | 1085 | `transpose` 1086 | (optional, default: `false`) If set to `true`, transposes the table, 1087 | swapping rows and columns. 1088 | 1089 | `headers` 1090 | (optional, default: `true`) Determines whether headers should be 1091 | included in the output. If set to `false`, headers are not generated. 1092 | 1093 | `tableArgs` 1094 | (optional) Any additional arguments are passed to the `table` function, 1095 | can be used for styling or anything else. 1096 | 1097 |
1098 | 1099 |
1100 | 1101 | ## `tabut-cells` 1102 | 1103 | The `tabut-cells` function functions as `tabut`, but returns `arguments` 1104 | for use in either the standard `table` function or other tools such as 1105 | `tablex`. If you just want the array of cells, use the `pos` function on 1106 | the returned value, ex `tabut-cells(...).pos`. 1107 | 1108 | `tabut-cells` is particularly useful when you need to generate only the 1109 | cell contents of a table or when these cells need to be passed to 1110 | another function for further processing or customization. 1111 | 1112 | ### Function Signature 1113 | 1114 |
1115 | 1116 | ``` typc 1117 | tabut-cells( 1118 | data-raw, 1119 | colDefs, 1120 | columns: auto, 1121 | align: auto, 1122 | index: "_index", 1123 | transpose: false, 1124 | headers: true, 1125 | ) -> arguments 1126 | ``` 1127 | 1128 |
1129 | 1130 | ### Parameters 1131 | 1132 | `data-raw` 1133 | This is the raw data that will be used to generate the table. The data 1134 | is expected to be in an array of dictionaries, where each dictionary 1135 | represents a single record or object. 1136 | 1137 | `colDefs` 1138 | These are the column definitions. An array of dictionaries, each 1139 | representing column definition. Must include the properties `header` and 1140 | a `func`. `header` expects content, and specifies the label of the 1141 | column. `func` expects a function, the function takes a record 1142 | dictionary as input and returns the value to be displayed in the cell 1143 | corresponding to that record and column. There are also two optional 1144 | properties; `align` sets the alignment of the content within the cells 1145 | of the column, `width` sets the width of the column. 1146 | 1147 | `columns` 1148 | (optional, default: `auto`) Specifies the column widths. If set to 1149 | `auto`, the function automatically generates column widths by each 1150 | column’s column definition. If set to `none` no `column` property is 1151 | added to the output arg. Otherwise functions exactly the `columns` 1152 | paramater of the standard typst `table` function. 1153 | 1154 | `align` 1155 | (optional, default: `auto`) Specifies the column alignment. If set to 1156 | `auto`, the function automatically generates column alignment by each 1157 | column’s column definition. If set to `none` no `align` property is 1158 | added to the output arg. Otherwise functions exactly the `align` 1159 | paramater of the standard typst `table` function. 1160 | 1161 | `index` 1162 | (optional, default: `"_index"`) Specifies the property name for the 1163 | index of each record. By default, an `_index` property is automatically 1164 | added to each record. If set to `none`, no index property is added. 1165 | 1166 | `transpose` 1167 | (optional, default: `false`) If set to `true`, transposes the table, 1168 | swapping rows and columns. 1169 | 1170 | `headers` 1171 | (optional, default: `true`) Determines whether headers should be 1172 | included in the output. If set to `false`, headers are not generated. 1173 | 1174 |
1175 | 1176 |
1177 | 1178 | ## `records-from-csv` 1179 | 1180 | Automatically converts a CSV data into an array of records. 1181 | 1182 |
1183 | 1184 | ``` typc 1185 | records-from-csv( 1186 | data 1187 | ) -> array 1188 | ``` 1189 | 1190 |
1191 | 1192 | ### Parameters 1193 | 1194 | `data` 1195 | The CSV data that needs to be converted, this can be obtained using the 1196 | native `csv` function, like `records-from-csv(csv(file-path))`. 1197 | 1198 | This function simplifies the process of converting CSV data into a 1199 | format compatible with `tabut`. It reads the CSV data, extracts the 1200 | headers, and converts each row into a dictionary, using the headers as 1201 | keys. 1202 | 1203 | It also automatically converts data into floats or integers when 1204 | possible. 1205 | 1206 |
1207 | 1208 |
1209 | 1210 | ## `rows-to-records` 1211 | 1212 | Converts rows of data into an array of records based on specified 1213 | headers. 1214 | 1215 | This function is useful for converting data in a “rows” format (commonly 1216 | found in CSV files) into an array of dictionaries format, which is 1217 | required for `tabut` and allows easy data processing using the built in 1218 | array functions. 1219 | 1220 |
1221 | 1222 | ``` typc 1223 | rows-to-records( 1224 | headers, 1225 | rows, 1226 | default: none 1227 | ) -> array 1228 | ``` 1229 | 1230 |
1231 | 1232 | ### Parameters 1233 | 1234 | `headers` 1235 | An array representing the headers of the table. Each item in this array 1236 | corresponds to a column header. 1237 | 1238 | `rows` 1239 | An array of arrays, each representing a row of data. Each sub-array 1240 | contains the cell data for a corresponding row. 1241 | 1242 | `default` 1243 | (optional, default: `none`) A default value to use when a cell is empty 1244 | or there is an error. 1245 | 1246 |
1247 | 1248 |
1249 | 1250 | ## `group` 1251 | 1252 | Groups data based on a specified function and returns an array of 1253 | grouped records. 1254 | 1255 |
1256 | 1257 | ``` typc 1258 | group( 1259 | data, 1260 | function 1261 | ) -> array 1262 | ``` 1263 | 1264 |
1265 | 1266 | ### Parameters 1267 | 1268 | `data` 1269 | An array of dictionaries. Each dictionary represents a single record or 1270 | object. 1271 | 1272 | `function` 1273 | A function that takes a record as input and returns a value based on 1274 | which the grouping is to be performed. 1275 | 1276 | This function iterates over each record in the `data`, applies the 1277 | `function` to determine the grouping value, and organizes the records 1278 | into groups based on this value. Each group record is represented as a 1279 | dictionary with two properties: `value` (the result of the grouping 1280 | function) and `group` (an array of records belonging to this group). 1281 | 1282 | In the context of `tabut`, the `group` function is particularly useful 1283 | for creating summary tables where records need to be categorized and 1284 | aggregated based on certain criteria, such as calculating total or 1285 | average values for each group. 1286 | 1287 |
1288 | -------------------------------------------------------------------------------- /doc/compiled-snippets/aggregation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /doc/compiled-snippets/no-headers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /doc/compiled-snippets/index.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | --------------------------------------------------------------------------------