Typ-Tables vs Pure Typst¶
Typst can load CSV files directly with #csv or JSON files with #json. So
you might be asking, why bother with typ-table? This explainer aims to
persuade you that typ-tables is more ergonomic when you are already
summarising the data in Python anyway.
Data Preparation¶
Suppose the source data is transaction-level sales data and the report needs a summary by region and customer segment. The data prep belongs in Python regardless:
from pathlib import Path
import polars as pl
sales = pl.DataFrame(
{
"region": ["North", "North", "North", "South", "South", "South"],
"segment": ["Retail", "Retail", "Enterprise", "Retail", "Enterprise", "Enterprise"],
"orders": [42, 35, 18, 31, 24, 29],
"revenue": [12_600, 10_850, 21_400, 9_920, 28_600, 33_350],
"cost": [7_900, 6_820, 13_200, 6_300, 16_700, 19_100],
}
)
summary = (
sales.group_by(["region", "segment"])
.agg(
pl.sum("orders"),
pl.sum("revenue"),
pl.sum("cost"),
)
.with_columns(
avg_order=pl.col("revenue") / pl.col("orders"),
margin=(pl.col("revenue") - pl.col("cost")) / pl.col("revenue"),
)
.sort(["region", "revenue"], descending=[False, True])
)
With typ-tables, the remaining code transforms the DataFrame into a Typst
table string. Formatting, labels, row grouping, and data-dependent styling stay
beside the DataFrame that created the result:
import narwhals as nw
from typ_tables import Typst, TypTable, locators, style
margin_threshold = 0.4
typst = (
TypTable(summary, rowname_col="segment", groupname_col="region")
.tab_header("Sales Summary", subtitle="Grouped by region and segment")
.tab_stubhead(Typst("*Segment*"))
.cols_label_with(
lambda x: Typst("*" + x.title().replace("_", ". ") + "*"),
)
.cols_hide("cost")
.fmt_currency(columns=["revenue", "avg_order"], currency="USD", decimals=0)
.fmt_percentage(columns="margin", decimals=1)
.tab_style(
locator=locators.LocBody(columns="margin", rows=nw.col("margin") >= margin_threshold),
text=style.TextStyle(weight="bold"),
cell=style.CellStyle(fill="rgb(232, 246, 239)"),
)
.to_typst()
)
Path("sales-summary.typ").write_text(typst)
Then the Typst document only needs to include the generated table:
On the other hand, if the presentation logic is done in Typst, we first need to save the CSV:
Then load and format it in the Typst document:
#let data = csv("sales-summary.csv", row-type: dictionary)
#let currency(val) = "$" + str(calc.round(float(val)))
#let percent(val) = str(calc.round(float(val) * 100, digits: 1)) + "%"
#table(
columns: (auto, auto, auto, auto, auto),
inset: 5pt,
align: (left, right, right, right, right),
table.header(
table.cell(colspan: 5, align: center)[
#set text(1.2em)
*Sales Summary* \
#text(0.8em, weight: "regular")[Grouped by region and segment]
],
[*Segment*], [*Orders*], [*Revenue*], [*Avg. order*], [*Margin*],
),
..{
let last-region = none
let rows = ()
for row in data {
if row.region != last-region {
rows.push(table.cell(colspan: 5)[#row.region])
last-region = row.region
}
let m = float(row.margin)
let highlight = m >= 0.4
rows.push([#row.segment])
rows.push([#row.orders])
rows.push([#currency(row.revenue)])
rows.push([#currency(row.avg_order)])
rows.push(table.cell(fill: if highlight { rgb("#e8f6ef") })[
#if highlight { strong(percent(row.margin)) } else { percent(row.margin) }
])
}
rows
},
)
Ergonomics¶
Which approach you find most ergonomic will probably depend on whether you are a better Python or Typst programmer. That being said...
Advantages of Typ-Tables¶
Code Locality
With Python, the code preparing, formatting, and styling the data is all in one place. If the data changes at all, you only have to update the Python script rather than a Python export script and Typst script.
Declarative API
The fluent API used by typ-tables allows you to describe what you want and
the library handles the how. In Typst you need to write the logic for how to
build the table including defining the helper functions. Of course, if
typ-tables doesn't support the formatting or styling you need, this doesn't
help.
Expressions
Conditional styling and formatting can be done using expressions rather than manual if else logic. Expressions can be constructed programmatically so complex selection logic can easily be used.
Narwhal column selectors are particularly powerful.
Helpers
- Row grouping is handled automatically using
typ-tables, no manual tracking. - Easily transform column labels programmatically.
- Full power of the Python standard library and ecosystem.
Disadvantages of Typ-Tables¶
Stringy Types
Many of the types used for styling are just strings. So you won't know if the
stroke you defined is valid Typst until the snippet produced by typ-tables
has been compiled. Whereas if you a using a Typst LSP, you will know
straight away.
This is something we hope to improve in the future.
No Instant Preview
If you notice an issue with the style, you need to re-run the Python and then recompile the PDF.
Context Awareness
In Typst, you can reference any global variables directly and if you want to change the name you can use the LSP to do so. With Python, you have to be careful to reference the global correctly and then keep them in sync.
Summary¶
If you are already doing a bunch of data crunching in Python typ-tables might
work well for you. If you already have clean data you just want to present in
your report, it is probably overkill.