b

biaoge

@preview

表哥 — Table toolkit for CSV grouping, cell merging, column splitting, and summary tables.

v0.1.1
MIT

Package Information

Last Updated
Minimum Typst Version
0.13.1
Authors
songwupei
Categories
components

1. Get the package

Download the package using the TPIX CLI:

tpix get @preview/biaoge:0.1.1

2. Import in your Typst file

Add this to your .typ file:

#import "@preview/biaoge:0.1.1": *

Version History

0.1.10.13.1
0d8bd6064875...
0.1.00.13.1
71540be7b855...

biaoge (表哥)

A Typst table toolkit for CSV grouping & summarization, cell merging, and column splitting.

Functions

Module Function Description
group-csv summarize Group and summarize by column
group-csv summary-table Generate grouped summary table (with total row)
group-csv summary-table-style Styled summary table
group-csv summary-table-style-no-total Styled summary table without total row
group-csv grouped-sum-table Two-level grouped, sorted summary table
group-csv-tools merge-selected-columns Merge selected columns into one
group-csv-tools split-column-in-rows Split rows by separator within a column
group-csv-tools trim-string Trim leading/trailing whitespace
merge-cells merge-col Extract values from a single column
merge-cells merge-table-data Vertically merge identical adjacent cells (returns data)
merge-cells merge-table-data-value-col Vertically merge and sum a value column (returns data)
merge-cells merge-table Vertically merge identical adjacent cells (returns table)
merge-cells merge-table-value-col Vertically merge and sum a value column (returns table)
table-style stroke-light Light border stroke
table-style inner-frame Inner frame stroke pattern (three-line table style)
conf conf Paper/report layout with author grid + abstract
conf-table group-tables Split a master table into sub-tables by group column

Usage

#import "@preview/biaoge:0.1.1": *

Data format

All table functions use a uniform 2D array format: the first row is the header, followed by data rows.

#let data = (
  ("Name", "Category", "Amount"),
  ("Apple", "Fruit", "120"),
  ("Banana", "Fruit", "80"),
  ("Milk", "Drink", "150"),
)

summarize — Group and summarize by column

Groups data by a specified column, computing the sum and count for each group. Returns an array of dictionaries with group, total, and count.

#let data = (
  ("Apple", "Fruit", "120"),
  ("Banana", "Fruit", "80"),
  ("Milk", "Drink", "150"),
)

#let result = summarize(data, group-index: 1, value-index: 2)
// result = (
//   (group: "Fruit", total: 200, count: 2),
//   (group: "Drink", total: 150, count: 1),
// )

Parameters:

  • group-index — Column index for grouping (0-based)
  • value-index — Column index for the numeric values to sum (0-based)

summary-table — Grouped summary table with total row

Generates a summary table with Category, Count, Total, and Percentage columns, sorted by total in descending order. A grand total row is appended automatically.

#let data = (
  ("Name", "Category", "Amount"),
  ("Apple", "Fruit", "120"),
  ("Banana", "Fruit", "80"),
  ("Milk", "Drink", "150"),
  ("Cola", "Drink", "60"),
  ("Cake", "Bakery", "200"),
)

#summary-table(data, group-index: 1, value-index: 2, title: "Sales by Category")

Output table:

Category Count Total Percentage
Bakery 1 200 32.79%
Drink 2 210 34.43%
Fruit 2 200 32.79%
Total 5 610 100%

Parameters:

  • group-index — Column index for grouping
  • value-index — Column index for numeric values
  • title — Table title

summary-table-style — Styled summary table

Similar to summary-table, but accepts pre-computed header and rows. Use when data has already been processed.

#let header = ([*Category*], [*Count*], [*Total*], [*Percentage*])
#let rows = (
  ("Bakery", "1", "200", "40.82%"),
  ("Fruit",  "2", "200", "40.82%"),
  ("Drink",  "2", "150", "30.61%"),
)

#summary-table-style(header, rows, value-index: 2, title: "Sales Statistics")

Parameters:

  • header — Header row array
  • rows — Data rows (2D array)
  • value-index — Column index for the total calculation
  • title — Table title

summary-table-style-no-total — Styled summary table without total row

Same as summary-table-style, but omits the total row. Useful when a grand total is not needed.

#let header = ([*Department*], [*Headcount*], [*Budget*])
#let rows = (
  ("Engineering", "15", "500"),
  ("Marketing",   "8",  "300"),
  ("Finance",     "5",  "200"),
)

#summary-table-style-no-total(header, rows, value-index: 2, title: "Department Budgets")

Parameters: Same as summary-table-style.


grouped-sum-table — Two-level grouped, sorted summary table

Groups by group-index-1 (sorted by total descending), sub-groups by group-index-2, and vertically merges identical cells in the first column. The value column cells are also merged and summed.

#let data = (
  ("Department", "Project", "Budget", "Executed", "Rate"),
  ("Engineering", "ERP System", "300", "270", "90%"),
  ("Engineering", "OA System",  "200", "150", "75%"),
  ("Marketing",   "Online Ads", "150", "120", "80%"),
  ("Marketing",   "Offline",    "100", "80",  "80%"),
)

#let tb-header = ([*Department*], [*Project*], [*Budget*], [*Executed*], [*Rate*])

#grouped-sum-table(
  data,
  group-index-1: 0,
  group-index-2: 1,
  value-index: 3,
  tb-header: tb-header,
  tb-title: "Project Execution Summary",
  col-nums: 5,
)

Output: departments are sorted by total executed amount, identical department names are vertically merged, and execution rates are summed for each department group.

Parameters:

  • group-index-1 — Primary grouping column index
  • group-index-2 — Secondary grouping column index
  • value-index — Numeric column index for sorting and summation
  • tb-header — Table header row
  • tb-title — Table title
  • col-nums — Total number of columns (default: 6)

merge-selected-columns — Merge selected columns

Merges multiple columns into one, joining values with a custom separator. The merged column replaces the first selected column; remaining selected columns are removed.

#let data = (
  ("First", "Last", "Age"),
  ("San",   "Zhang", "28"),
  ("Si",    "Li",    "35"),
  ("Wu",    "Wang",  "42"),
)

// Default separator " - "
#let merged = merge-selected-columns(data, (0, 1))
// merged = (
//   ("First", "Age"),
//   ("San - Zhang", "28"),
//   ("Si - Li",     "35"),
//   ("Wu - Wang",   "42"),
// )

// Custom separator
#let merged2 = merge-selected-columns(data, (0, 1), transform: values => values.join(" "))
// merged2 = (
//   ("First", "Age"),
//   ("San Zhang", "28"),
//   ("Si Li",     "35"),
//   ("Wu Wang",   "42"),
// )

Parameters:

  • data — 2D data array
  • indices — Array of column indices to merge
  • transform — Custom merge function, default values.join(" - ")

split-column-in-rows — Split a column into multiple rows

Splits a column value by a separator, creating one row per split part. Useful for expanding multi-value fields like "Apple - Banana - Orange".

#let data = (
  ("Category", "Name"),
  ("Fruit", "Apple - Banana - Orange"),
  ("Drink", "Milk - Cola"),
)

#let result = split-column-in-rows(data, split-column-index: 1, separator: " - ")
// result = (
//   ("Category", "Name"),
//   ("Fruit", "Apple"),
//   ("Fruit", "Banana"),
//   ("Fruit", "Orange"),
//   ("Drink", "Milk"),
//   ("Drink", "Cola"),
// )

Parameters:

  • data — 2D data array
  • split-column-index — Column index to split
  • separator — Separator string, default " - "

trim-string — Trim whitespace

Removes leading and trailing whitespace from a string.

#let result = trim-string("   hello world   ")
// result = "hello world"

#let cleaned = (" apple ", " banana ").map(trim-string)
// cleaned = ("apple", "banana")

Parameters:

  • str — String to trim

merge-col — Extract a column

Extracts all values from a single column. Mostly used internally by other merge functions.

#let data = (
  ("Name", "Category", "Amount"),
  ("Apple", "Fruit", "120"),
  ("Banana", "Fruit", "80"),
  ("Milk", "Drink", "150"),
)

#let categories = merge-col(data, merge-col-index: 1)
// categories = ("Name", "Category", "Amount", "Fruit", "Fruit", "Drink")

Parameters:

  • data — 2D data array
  • merge-col-index — Column index to extract (0-based)

merge-table-data — Vertically merge identical cells (returns data)

Merges consecutive identical cells in a specified column using table.cell(rowspan:). Returns a dictionary (header:, rows:) without rendering a table — use this when you need to post-process the data.

#let header = ("Category", "Name", "Amount")
#let rows = (
  ("Fruit", "Apple",  "120"),
  ("Fruit", "Banana", "80"),
  ("Drink", "Milk",   "150"),
  ("Drink", "Cola",   "60"),
)

#let merged-data = merge-table-data(header, rows, merge-col-index: 0, table-col-count: 3)
// merged-data = (
//   header: ("Category", "Name", "Amount"),
//   rows: (
//     (table.cell(rowspan: 2, "Fruit"), "Apple", "120"),
//     ("Banana", "80"),
//     (table.cell(rowspan: 2, "Drink"), "Milk", "150"),
//     ("Cola", "60"),
//   ),
// )

Parameters:

  • header — Header row array
  • rows — Data rows (without header)
  • merge-col-index — Column index to merge identical cells in
  • table-col-count — Total number of table columns

merge-table-data-value-col — Vertically merge and sum values (returns data)

Combines merge-table-data cell merging with numeric summation on a value column. Merged cells show the summed value with a % suffix.

#let header = ("Category", "Name", "Amount", "Share")
#let rows = (
  ("Fruit", "Apple",  "120", "30%"),
  ("Fruit", "Banana", "80",  "20%"),
  ("Drink", "Milk",   "150", "37.5%"),
  ("Drink", "Cola",   "60",  "15%"),
)

#let merged-data = merge-table-data-value-col(
  header, rows, merge-col-index: 0, table-col-count: 4, value-col-index: 3
)
// Share column summed per group: Fruit "30%" + "20%" → "50%"

Parameters:

  • header — Header row array
  • rows — Data rows
  • merge-col-index — Column index to merge cells in
  • table-col-count — Total number of table columns
  • value-col-index — Value column index (values must be in "xx%" format)

merge-table — Vertically merge identical cells (returns table)

Same as merge-table-data, but directly returns a rendered Typst table.

#let header = ("Category", "Name", "Amount")
#let rows = (
  ("Fruit", "Apple",  "120"),
  ("Fruit", "Banana", "80"),
  ("Drink", "Milk",   "150"),
)

#merge-table(header, rows, merge-col-index: 0, table-col-count: 3)

Output: consecutive identical values ("Fruit", "Drink") in the category column are vertically merged with rowspan.

Parameters: Same as merge-table-data.


merge-table-value-col — Vertically merge and sum values (returns table)

Same as merge-table-data-value-col, but directly returns a rendered Typst table.

#let header = ("Category", "Name", "Amount", "Share")
#let rows = (
  ("Fruit", "Apple",  "120", "30%"),
  ("Fruit", "Banana", "80",  "20%"),
  ("Drink", "Milk",   "150", "37.5%"),
)

#merge-table-value-col(header, rows, merge-col-index: 0, table-col-count: 4, value-col-index: 3)

Output: category column cells are vertically merged; share column shows summed percentages per group.

Parameters: Same as merge-table-data-value-col.


stroke-light — Light border stroke

A 0.5pt + black stroke value, typically used with inner-frame for table borders.

#table(
  columns: 3,
  stroke: inner-frame(stroke-light),
  [A], [B], [C],
  [1], [2], [3],
)

Value: 0.5pt + black


inner-frame — Inner frame stroke pattern

A stroke generator that draws separators only between inner cells — hides the left border on the first column and the top border on header rows, producing a clean "three-line table" look.

#import "table-style.typ": stroke-light, inner-frame

#table(
  columns: 4,
  stroke: inner-frame(stroke-light),
  inset: 10pt,
  table.header([*Name*], [*Qty*], [*Price*], [*Total*]),
  [Apple],  [10], [5.0], [50.0],
  [Banana], [20], [3.0], [60.0],
  table.hline(stroke: stroke-light),
  [*Total*], [], [], [110.0],
)

Parameters:

  • stroke — Stroke style (e.g., stroke-light)

Behavior: Left border hidden on column 0, top border hidden on rows 0 and 1 (header area), giving the table an open, clean style.


conf — Paper / report layout

Sets up a paper or report page layout with margins, an author grid, and an abstract block at the top.

#show: conf.with(
  authors: (
    (name: "Alice", affiliation: "University of Example", email: "alice@example.com"),
    (name: "Bob",   affiliation: "Research Institute",   email: "bob@example.com"),
  ),
  abstract: [This paper presents a novel approach to table generation in Typst...],
)

= Introduction

#lorem(500) // body content

Output: title centered at the top, authors arranged in a grid (max 3 columns), abstract below, followed by the document body.

Parameters:

  • authors — Array of author dictionaries with name (required), plus optional affiliation and email
  • abstract — Abstract content block
  • doc — Document body (passed as the content block after #show:)

group-tables — Split master table into sub-tables by group

Splits a single table into multiple independent sub-tables based on unique values in a group column. Each sub-table is titled with the group value, and the group column is removed from display.

#let data = (
  ("System", "Department", "Module", "Status"),
  ("ERP", "Engineering", "Procurement", "Running"),
  ("ERP", "Engineering", "Inventory",   "Running"),
  ("OA",  "Admin",       "Documents",   "Maintenance"),
  ("OA",  "Admin",       "Meetings",    "Stopped"),
)

#group-tables(data, group-col: "Department")

Output:

== Engineering Required Fields

System Module Status
ERP Procurement Running
ERP Inventory Running

== Admin Required Fields

System Module Status
OA Documents Maintenance
OA Meetings Stopped

Parameters:

  • data — 2D data array (first row is the header)
  • group-col — Group column name (string) or column index (integer)

Complete example: from raw data to report

#import "@preview/biaoge:0.1.1": *

// Raw sales data
#let sales-data = (
  ("Product", "Region", "Quarter", "Revenue"),
  ("Phone",   "East",   "Q1",      "5000"),
  ("Phone",   "East",   "Q2",      "6200"),
  ("Phone",   "South",  "Q1",      "3800"),
  ("Laptop",  "East",   "Q1",      "12000"),
  ("Laptop",  "South",  "Q1",      "8500"),
  ("Laptop",  "South",  "Q2",      "9200"),
)

// 1. Summary by product
#summary-table(sales-data, group-index: 0, value-index: 3, title: "Revenue by Product")

// 2. Split into sub-tables by region
#group-tables(sales-data, group-col: "Region")

// 3. Merge identical product names vertically
#let header = ("Product", "Region", "Quarter", "Revenue")
#let rows = sales-data.slice(1)
#merge-table(header, rows, merge-col-index: 0, table-col-count: 4)

Dependencies

No external dependencies — pure Typst standard library.

License

MIT — Copyright (c) 2025 songwupei