The development of Great Tables continues! We’re excited to announce the release of v0.18.0, which brings several powerful new features. These features make it even easier to create beautiful, informative tables. The key additions in this release include new methods (and a tweak to an existing one):
Let’s explore each of these interesting new features!
Working with data that has hierarchical column names can be tedious when manually creating spanners. The new .tab_spanner_delim() method automates this process by intelligently splitting column names based on a delimiter and creating the appropriate spanner structure.
Here’s a practical example using the towny dataset, which contains population data for a collection of municipalities across multiple census years. Let’s start by looking at the most populated cities and examining their column structure:
from great_tables import GT
from great_tables.data import towny
import polars as pl
import polars.selectors as cs
# Create a smaller version of the `towny` dataset
towny_mini = (
pl.from_pandas(towny)
.filter(pl.col("csd_type") == "city")
.sort("population_2021", descending=True)
.select(
"name",
cs.starts_with("population_"),
cs.starts_with("density_")
)
.head(5)
)
# Let's look at the column names
print(towny_mini.columns)
['name', 'population_1996', 'population_2001', 'population_2006', 'population_2011', 'population_2016', 'population_2021', 'density_1996', 'density_2001', 'density_2006', 'density_2011', 'density_2016', 'density_2021']
Notice how the column names have a clear hierarchical structure with underscores as delimiters. Let’s now create a table that takes advantage of this structure:
(
GT(towny_mini, rowname_col="name")
.tab_spanner_delim(delim="_")
.fmt_integer(columns=cs.contains("population"))
.fmt_number(columns=cs.contains("density"), decimals=1)
.tab_header(title="Population and Density Trends from Census Data")
.opt_align_table_header(align="left")
)
| Population and Density Trends from Census Data |
|
population
|
density
|
| 1996 |
2001 |
2006 |
2011 |
2016 |
2021 |
1996 |
2001 |
2006 |
2011 |
2016 |
2021 |
| Toronto |
2,385,421 |
2,481,494 |
2,503,281 |
2,615,060 |
2,731,571 |
2,794,356 |
3,779.8 |
3,932.0 |
3,966.5 |
4,143.6 |
4,328.3 |
4,427.8 |
| Ottawa |
721,136 |
774,072 |
812,129 |
883,391 |
934,243 |
1,017,449 |
258.6 |
277.6 |
291.3 |
316.8 |
335.1 |
364.9 |
| Mississauga |
544,382 |
612,925 |
668,599 |
713,443 |
721,599 |
717,961 |
1,859.6 |
2,093.8 |
2,283.9 |
2,437.1 |
2,465.0 |
2,452.6 |
| Brampton |
268,251 |
325,428 |
433,806 |
523,906 |
593,638 |
656,480 |
1,008.9 |
1,223.9 |
1,631.5 |
1,970.4 |
2,232.7 |
2,469.0 |
| Hamilton |
467,799 |
490,268 |
504,559 |
519,949 |
536,917 |
569,353 |
418.3 |
438.4 |
451.2 |
464.9 |
480.1 |
509.1 |
The .tab_spanner_delim() method recognizes the underscore delimiter and creates a hierarchical structure: "population" and "density" become top-level spanners, with the years (1996, 2001, 2021) as the final column labels. This creates a clean, organized appearance that clearly groups related metrics together. And, this one method can be used instead of a combination of .cols_label() and .tab_spanner() (which requires a separate invocation per spanner added).
When dealing with many columns or long column names, horizontal space becomes precious. The .cols_label_rotate() method solves this by rotating column labels vertically, allowing for more compact table layouts.
Here’s an example where we use the gtcars dataset to create a table which communicates a feature matrix:
from great_tables import GT, style, loc
from great_tables.data import gtcars
import polars as pl
import polars.selectors as cs
# Manipulate dataset to create a feature comparison table
gtcars_mini = (
pl.from_pandas(gtcars)
.filter(pl.col("year") == 2017)
.filter(pl.col("ctry_origin").is_in(["Germany", "Italy", "United Kingdom"]))
.with_columns([
(pl.col("hp") > 500).alias("High Power"),
(pl.col("mpg_h") > 25).alias("Fuel Efficient"),
(pl.col("drivetrain") == "awd").alias("All Wheel Drive"),
(pl.col("msrp") > 100000).alias("Premium Price"),
(pl.col("trsmn").str.contains("manual")).alias("Manual Transmission")
])
.select([
"mfr", "model", "trim",
"High Power",
"Fuel Efficient",
"All Wheel Drive",
"Premium Price",
"Manual Transmission"
])
.head(10)
)
(
GT(gtcars_mini)
.fmt_tf(
columns=cs.by_dtype(pl.Boolean),
tf_style="check-mark",
colors=["#2E8B57", "#DC143C"]
)
.cols_label_rotate(
columns=cs.by_dtype(pl.Boolean),
dir="sideways-lr"
)
.tab_header(
title="European Luxury Cars Feature Matrix",
subtitle="2017 Models with Performance & Luxury Features"
)
.opt_stylize(style=1)
.tab_style(
style=style.text(size="11px"),
locations=loc.body()
)
)
| European Luxury Cars Feature Matrix |
| 2017 Models with Performance & Luxury Features |
| mfr |
model |
trim |
High Power |
Fuel Efficient |
All Wheel Drive |
Premium Price |
Manual Transmission |
| Ferrari |
GTC4Lusso |
Base Coupe |
✔ |
✘ |
✔ |
✔ |
✘ |
| Aston Martin |
DB11 |
Base Coupe |
✔ |
✘ |
✘ |
✔ |
✘ |
| Lotus |
Evora |
2+2 Coupe |
✘ |
✘ |
✘ |
✘ |
✘ |
| Porsche |
718 Boxster |
Base Convertible |
✘ |
✔ |
✘ |
✘ |
✘ |
| Porsche |
718 Cayman |
Base Coupe |
✘ |
✔ |
✘ |
✘ |
✘ |
This example demonstrates how both the .fmt_tf() and .cols_label_rotate() methods can work well together. The boolean columns use checkmarks (✓/✗) with custom colors=, while the rotated labels save horizontal space in this dense feature matrix. The combination allows you to put more information into a compact and still readable format.
Acknowledgements and what’s next
We’re grateful to all the contributors who made this release possible. These new features represent significant improvements for creating space-efficient tables while also maximizing visual appeal.
The combination of these features lets you now create complex, professional tables with hierarchical column structures, boolean indicators, space-saving labels, and nicely formatted datetime displays.
We’re always happy to get feedback and hear about how you’re using Great Tables:
- GitHub Issues
- GitHub Discussions
- Discord
Keep building those beautiful tables!