Migrating from greater_tables 5.x
The import name does not change, and the common call still works:
from greater_tables import GT
GT(df, caption='Exhibit 1', ratio_cols=['lr'], year_cols='year')
GT is a thin compatibility facade over the new
engine — constructor-compatible where that still makes sense, not bug-for-bug.
It is deliberately thin: options that were really renderer opinions, or that
the new architecture makes meaningless, are gone.
This page is the complete map. Every 5.x option, what it did, and where it
went, compiled from 5.3’s config.py Configurator (47 fields, with its own
Field(description=...) text as the source for “what it did”) and core.py’s
GT.__init__ signature.
Staying on 5.x
5.3 is frozen, complete, and stays installable from PyPI forever. Pin it:
pip install "greater-tables<6"
That is what the major version is for. Its source and issue archive remain at mynl/greater_tables_project.
What carries over
caption, label, aligners, formatters, unbreakable, ratio_cols,
year_cols, date_cols, raw_cols, show_index, config_path, sparsify,
sparsify_columns, header_row, pef_lower, pef_upper. Any other
TableSpec field can also be passed through the
facade as a keyword.
formatters accepts the 5.x shapes: a callable, a format string
('{x:.1%}'), or an int meaning “this many decimals”.
Unrecognized keywords warn and are ignored rather than raising, so an old call site keeps working while you clean it up.
Warning
Three silent default changes. Everything else either works or warns.
A bare int now groups.
formatters={'paid': 2}gives1,234.57where 5.x gave1234.57, because 6.0 groups whenever it chooses the format for you. Write'.2f'for the ungrouped form.show_indexdefaults toNone, which auto-hides a trivial unnamedRangeIndex. PassTrueto force the index to show.header_rowdefaults toFalse. Passheader_row=Truefor list-of-lists input that carries a header row.
The two objects
The split is the whole design: semantics (what the table means) stay in Python and travel in the IR; geometry and style (what it looks like) belong to each renderer. Most deletions below are options that crossed that line.
Marker |
Meaning |
|---|---|
TableSpec |
Semantics that ride with the table. |
HouseStyle |
Renderer opinions — rule depth, weights, TikZ geometry. Never enters the IR. Pass it to any renderer or to the facade: |
CSS |
A custom property on the |
engine |
No longer a knob: the engine decides per column from dtype and column statistics. Override per column with |
gone |
Deleted, with no replacement. The “Notes” column says what to do instead, if anything. |
HouseStyle in full
Everything on HouseStyle, and who actually reads it:
field |
default |
read by |
controls |
|---|---|---|---|
|
|
all four |
which stub depths get a horizontal rule |
|
|
HTML, TikZ, walker |
which column-group depths get a vertical |
|
|
HTML, TikZ |
how wide one wrapping column may get (HTML: |
|
|
HTML, TikZ |
total width to aim for (HTML: |
|
|
text |
|
|
|
TikZ |
weight per depth slot (HTML: |
|
|
TikZ |
weight per vrule slot (HTML: |
|
|
TikZ |
table top and bottom ( |
|
|
TikZ |
under the header block ( |
|
|
TikZ |
the stub/body vertical divider ( |
|
|
TikZ |
above rows flagged |
|
|
TikZ |
cmidrule under a column-group label ( |
|
|
TikZ |
|
|
|
TikZ |
column gap, em |
|
|
TikZ |
row gap, em |
|
|
TikZ |
|
|
|
TikZ |
float placement, e.g. |
|
|
TikZ |
TeX injected into the environment |
Weight scale: 0 don’t draw · 0.25 ultra thin · 0.5 very thin · 0.75
thin · 1 semithick · 1.5 thick · 2 very thick · 3 ultra thick.
So the honest summary is: four fields are portable, one is text-only, the
other thirteen are TikZ. HTML’s equivalents are CSS custom properties, and it
has four more with no HouseStyle counterpart at all — --gt-pad-v,
--gt-pad-h, --gt-neg-color, --gt-caption-align.
tikz_scale deserves its footnote. It rides on the matrix nodes, so node
contents scale and the matrix column separations do not — a table at
scale=0.5 is about 0.60 of its full width, not half. Measured, the emitted
width is exactly
width(s) = s · (sum of column widths + 2 × tikz_column_sep) + ncols × tikz_column_sep + ~0.7em
which is the model max_table_width_em uses to pick an autoscale step. The
~0.7em is the rules overhanging the matrix bounding box; it is flat in both
column count and table width.
Vertical rules
There are only two kinds of vertical in a 6.0 table, and 6.0 never draws a grid line between every column.
1. The stub/body divider. Always present when there is a stub — the │ in
text, gt-stub-end in HTML, stub_rule in TikZ. Not depth-driven, no on/off
switch beyond setting its weight to 0 (TikZ) or --gt-rule-stub: none
(HTML).
2. Column-group verticals — the max_vrule_depth family. Same two-part
split as the horizontals:
GT(df, style=HouseStyle(max_vrule_depth=0)) # outermost group boundaries only
GT(df, style=HouseStyle(max_vrule_depth=None)) # none at all
weight from vrule_widths (TikZ) or --gt-vrule-0/1 (HTML). Those got their
own properties in a3 precisely so you can do this:
<style>.gt { --gt-vrule-0: none; --gt-vrule-1: none; }</style>
— verticals off, row rules untouched. Before a3 they shared --gt-rule-0/1,
so you could not have one without the other.
The constraint worth knowing: these need a MultiIndex on the columns.
The engine reports a boundary only at depth ≤ n_levels − 2, so a flat column
header produces no verticals whatever you set max_vrule_depth to. That is
the asymmetry with rows — max_rule_depth=0 on a single-level index rules
every changed row, but there is no vertical equivalent, because the filter
sits in the engine rather than the render policy. And only two slots exist, so
a third-level group boundary reuses the lighter weight.
Text has no group verticals at all — only the stub divider. If you are
using render_text as the check, verticals are the one thing it will not show
you.
GT.__init__ arguments that were not Configurator fields
5.x argument |
Default |
What it did |
6.0 home |
Notes |
|---|---|---|---|---|
|
— |
DataFrame, Series, list of lists, markdown table string, or namedtuple |
|
Same inputs except namedtuple, which is gone. Markdown ingest still parses the caption, |
|
|
Table caption |
TableSpec |
5.x also picked up a |
|
|
Quarto cross-reference id |
TableSpec |
Emits |
|
|
dict col → |
TableSpec |
Same two shapes. |
|
|
dict col → callable, |
TableSpec |
Same name, now a real spec field rather than a shim. Also takes csv-grid sugar ( |
|
|
Column widths in characters or ems |
gone |
Width machinery deleted; see the width block below. The |
|
|
Columns that must not wrap |
TableSpec |
Sets |
|
|
Columns to format as percentages |
TableSpec |
All four |
|
|
Integer years — no thousands separator |
TableSpec |
Also forces center alignment. |
|
|
Force date treatment |
TableSpec |
|
|
|
Pass through unformatted |
TableSpec |
Distinct from |
|
|
Show the index as stub columns |
TableSpec |
⚠️ Default changed to |
|
|
A |
gone |
6.0’s equivalent object is |
|
|
YAML config file |
TableSpec via |
Still a keyword on the |
|
— |
Any |
facade only |
Names matching a |
Configurator fields
Default formats
5.x option |
Default |
What it did |
6.0 home |
Notes |
|---|---|---|---|---|
|
|
Format f-string for integers |
TableSpec |
The direct equivalent, and YAML-able. Unset, the built-in is |
|
|
Format f-string for floats |
TableSpec |
The direct equivalent. Left unset, decimals come from each column’s own magnitude — better for one table, inconsistent across a document, which is exactly what this field is for. Setting it also turns off the automatic SI/engineering switch. |
|
|
strftime pattern for dates |
TableSpec |
Covers the date tag and both the date and datetime dtypes, so it also drops the time component from datetimes. ISO if unset. |
|
|
Format for ratio columns |
TableSpec |
|
|
|
Fallback formatter f-string |
gone |
There is no single fallback across dtypes — the four named defaults above are per kind, which is what makes them predictable. |
|
|
One float format for the whole table, overriding per-column |
TableSpec |
Same meaning and the same precedence: it beats an explicit |
The resolution order per column is its kind (semantic tag if it has one,
else dtype), then within that kind: formatters[col] → the house default →
the built-in. Setting float_format turns float inference off entirely,
including the automatic SI/engineering switch.
Rules and lines
All five became renderer opinions. Nothing about a rule weight enters the IR;
the semantics that drive them (break_depth per row and column, total row
flags) are computed by the engine and each renderer maps them to weights.
5.x option |
Default |
What it did |
6.0 home |
Notes |
|---|---|---|---|---|
|
|
Top, bottom and header rules |
HouseStyle |
|
|
|
Rule under the header block |
HouseStyle |
|
|
|
Vertical rule between index and body |
HouseStyle |
|
|
|
Three weights for MultiIndex row-group rules |
HouseStyle |
Now driven by row |
|
|
Three weights for column-group verticals |
HouseStyle |
Driven by column |
Structure
5.x option |
Default |
What it did |
6.0 home |
Notes |
|---|---|---|---|---|
|
|
Merge repeated index values |
TableSpec |
Unchanged, same default. Emits real |
|
|
Merge repeated header prefixes |
TableSpec |
Unchanged. Emits |
|
|
For list input, promote row 0 to headers |
TableSpec |
⚠️ Default changed to |
Spacing, fonts, caption
5.x option |
Default |
What it did |
6.0 home |
Notes |
|---|---|---|---|---|
|
|
Padding preset: tight / medium / wide |
CSS |
The three presets are gone; set the two properties. |
|
|
Manual padding, four sides |
CSS same two properties |
Reduced to vertical/horizontal — per-side padding on a table cell was never worth the API. |
|
|
Body font size (em) |
gone |
The fragment now inherits the host page’s font entirely — no size opinion at all, which is what stopped it fighting JupyterLab and Quarto. Set |
|
|
Header font size (em) |
gone |
Headers are |
|
|
Caption font size (em) |
gone |
Caption is |
|
|
Bold the index columns |
gone |
Style |
|
|
Caption alignment |
CSS |
⚠️ Default changed to |
|
|
Hook mapping non-math TeX/markdown to HTML |
gone |
Math is auto-detected ( |
Engineering format
5.x option |
Default |
What it did |
6.0 home |
Notes |
|---|---|---|---|---|
|
|
Digits after the decimal in engineering format |
TableSpec |
Plus |
|
|
Use engineering format below |
TableSpec |
Unchanged, same default. Now a column-level decision (the column’s mean magnitude), not per cell. |
|
|
Use engineering format above |
TableSpec |
Unchanged, same default. Default display is SI suffixes (µ m k M G T…); aligned |
Ingest and limits
5.x option |
Default |
What it did |
6.0 home |
Notes |
|---|---|---|---|---|
|
|
Cast non-int, non-date columns to float where possible |
gone |
The engine trusts the dtypes it is given — 5.3 had already backed away from this for PyArrow. |
|
|
Truncate stringified objects |
gone |
No truncation: cutting content out of a display table is the wrong trade. Long prose wraps at |
|
|
Allow rendering tables over the limit |
gone |
Replaced by |
|
|
Raise above this many rows |
TableSpec |
⚠️ It no longer raises: it truncates and appends a note saying so. |
|
|
Extra reporting; internal id in the caption |
gone |
|
Width machinery — rebuilt, smaller
5.x negotiated column widths in Python for every backend. 6.0 does not, and the division of labor is the point: TikZ estimates, because TeX has no browser to ask; HTML states a limit and lets the browser measure, because it measures the real font and we can only guess at it; the JS walker can run a canvas-measured solver for a constrained container.
max_table_width_em is the heir to max_table_inch_width, in ems rather than
inches — every other geometry field is ems, and inches mean nothing without a
font size. It is a target, not a guarantee. Columns that cannot wrap —
numbers, dates, anything tagged or listed in TableSpec.unbreakable — keep
their natural width whatever the budget says. What the budget narrows is the
prose, and:
TikZ allocates with one shared quantile, so every prose column carries the same chance of wrapping, then distributes the remainder proportionally so the budget is met rather than merely respected. If even the floors do not fit,
tikz_scalesteps down a coarse ladder (1.0, 0.95 … 0.6) and says so throughlogging; past 0.6 it warns and suggestssidewaysfigureinstead of shrinking into illegibility. A prose column is never narrowed past its widest single word — TeX will not hyphenate inside one.HTML emits
--gt-max-widthand stops there.gt-nowrapalready pins the columns that must not be narrowed, so the browser narrows exactly the prose ones. No solver runs on that path and none should.
One caveat with teeth: the TikZ estimate is Times metrics, exact against
newtxtext (what Etcher compiles with) and about 11% tight against Latin
Modern, the LaTeX default a plain Quarto PDF uses — where a long heading may
hyphenate. Load a Times-metric serif alongside the tables, or expect slightly
tight columns. Use newtxtext alone; newtxmath collides with Quarto’s math
setup.
5.x option |
Default |
What it did |
6.0 home |
Notes |
|---|---|---|---|---|
|
|
Force equal column widths |
gone |
|
|
|
Target table width in inches |
HouseStyle |
Ems, not inches: at 11pt, 8in is about 52em. Advisory — see above. |
|
|
explicit / natural / breakable / minimum |
partly |
“natural” is the default (no budget) and the browser’s |
|
|
Share of width given to headers |
gone |
Headers no longer compete for width: a heading floors its column at its widest word and wraps, so there is nothing to apportion. |
|
|
Extra characters allowed per heading |
gone |
Same reason. |
|
|
Point size, used for width estimation |
Etcher |
Survives only where it is real: the standalone LaTeX document Etcher compiles. |
|
|
few / center |
gone |
Was marked |
TikZ
5.x option |
Default |
What it did |
6.0 home |
Notes |
|---|---|---|---|---|
|
|
Compute TikZ output at all |
gone |
Every rendering is a lazily cached property, so the HTML path never pays for TeX. This option existed to work around that; the waste is gone by construction. |
|
|
Scale factor |
HouseStyle |
|
|
|
Column separation |
HouseStyle |
em units. |
|
|
Row separation |
HouseStyle |
em units. |
|
|
table / figure / sidewaysfigure |
HouseStyle |
|
|
|
Placement args for |
HouseStyle |
|
|
|
Extra commands at the bottom of the picture |
HouseStyle |
|
|
|
Explicit list of row indices to rule |
gone |
Rules follow row |
|
|
Explicit list of column indices to rule |
gone |
Follows column |
|
|
Escape |
gone |
Cells carry plain unicode in the IR and each renderer escapes for its own target; math rides in a separate |
New in 6.0 — no 5.x equivalent
Option |
What it does |
|---|---|
|
Footnotes under the table, in every renderer. |
|
Columns whose cells carry raw HTML (renderers still gate on their own allow-html). |
|
Mark rows |
|
Same per cell. The |
|
|
|
Ship machine values alongside formatted text, for clients. Data columns only. |
|
How deep the hierarchy is ruled. |
|
Weights for the rule above a total row and under a column-group label. |
|
How wide one wrapping column may get (18em). TikZ reads it; HTML emits it as |
|
Total table width to aim for — advisory, since unbreakable columns hold their natural width. TikZ narrows prose then autoscales; HTML hands it to the browser as |
|
Draw the text rendering without box-drawing characters, for LaTeX verbatim blocks and legacy consoles. |
|
|
Target-aware display bundle |
A Quarto PDF render gets TikZ, everything else gets HTML, and |
|
A document you can hash, cache, store, and send over HTTP. See The table document (IR v1). |
Porting recipes
The common call is unchanged.
from greater_tables import GT
GT(df, caption='Exhibit 1', ratio_cols=['lr'], year_cols='year')
A table that used to raise on 50 rows now truncates at 200 with a note:
GT(df, max_rows=None) # or any int
Rule weights are no longer per table. For TikZ:
from greater_tables import build, render_tikz, HouseStyle, TableSpec
render_tikz(build(df, TableSpec()), style=HouseStyle(hrule_widths=(1, 0.5, 0)))
For HTML, set the properties once for the document:
.gt { --gt-rule-0: 1pt solid; --gt-pad-v: 0.15em; --gt-caption-align: center; }
Checking a port — the text renderer strips styling and shows only decisions, which makes it the fastest diff between the two engines:
print(GT(df, caption='Check'))
dev/tools/compare_gt5.py in the repository does this side by side, running
5.3 in an isolated interpreter (both generations share the import name, so
they cannot coexist in one process) and writing an HTML comparison sheet.