---
title: "Great Tables `v0.18.0`: Easy Column Spanners and More!"
html-table-processing: none
author: Rich Iannone
date: 2025-07-18
freeze: true
jupyter: python3
---

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):

- `~~.GT.tab_spanner_delim()`: quick spanner creation
- `~~.GT.fmt_tf()`: easy boolean value formatting
- `~~.GT.cols_label_rotate()`: enables column label rotation
- `~~.GT.fmt_datetime()`: added `format_str=` parameter for extra customization

Let's explore each of these interesting new features!

### Quick spanner creation with `tab_spanner_delim()`

Working with data that has hierarchical column names can be tedious when manually creating spanners. The new `~~.GT.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:

```{python}
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)
```

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:

```{python}
(
    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")
)
```

The `~~.GT.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 `~~.GT.cols_label()` and `~~.GT.tab_spanner()` (which requires a separate invocation per spanner added).

### Beautiful boolean formatting with `fmt_tf()`

Boolean data is common in analytical tables, but raw `True`/`False` values can look unprofessional. The new `~~.GT.fmt_tf()` method provides elegant ways to display boolean data using symbols, words, or custom formatting.

Here's a simple example showing different `tf_style=` options:

```{python}
from great_tables import GT
import polars as pl

# Create a simple DF with boolean data
bool_df = pl.DataFrame({
    "feature": ["Premium Sound", "Leather Seats", "Sunroof", "Navigation"],
    "model_a": [True, False, True, True],
    "model_b": [True, True, False, True],
    "model_c": [False, True, True, False]
})

(
    GT(bool_df, rowname_col="feature")
    .fmt_tf(tf_style="check-mark", colors=["green", "red"])
    .tab_header(title="Car Features Comparison", subtitle="Using check-mark style")
)
```

You can also use different symbols and colors for a more distinctive look:

```{python}
(
    GT(bool_df, rowname_col="feature")
    .fmt_tf(tf_style="circles", colors=["#4CAF50", "#F44336"])
    .tab_header(title="Car Features Comparison", subtitle="Using circles style")
)
```

The `~~.GT.fmt_tf()` method transforms boolean values into visually appealing symbols that make it easy to quickly scan and compare data across rows and columns.

### Rotating column labels with `cols_label_rotate()`

When dealing with many columns or long column names, horizontal space becomes precious. The `~~.GT.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:

```{python}
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()
    )
)
```

This example demonstrates how both the `~~.GT.fmt_tf()` and `~~.GT.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.

### Enhanced datetime formatting with `fmt_datetime()`

The `~~.GT.fmt_datetime()` method now supports custom format strings through the new `format_str=` parameter, giving you complete control over how datetime values appear in your tables.

Here's an example using the included `gibraltar` weather dataset:

```{python}
from great_tables import GT
from great_tables.data import gibraltar
import polars as pl

# Prepare the meteorological data
gibraltar_mini = (
    pl.from_pandas(gibraltar)
    .with_columns(
        [
            pl.concat_str([pl.col("date"), pl.lit(" "), pl.col("time")])
            .str.strptime(pl.Datetime, format="%Y-%m-%d %H:%M")
            .alias("datetime")
        ]
    )
    .filter(pl.col("datetime").dt.hour().is_in([6, 12, 18]))
    .select(["datetime", "temp", "humidity", "condition"])
    .sort("datetime")
    .head(10)
)

(
    GT(gibraltar_mini)
    .fmt_datetime(
        columns="datetime",
        format_str="%b %d %Y (%a) - %I:%M %p",
    )
    .fmt_number(columns="temp", decimals=1, pattern="{x}°C")
    .fmt_percent(columns="humidity", scale_values=False, decimals=0)
    .cols_label(
        datetime="Time",
        temp="Temperature",
        humidity="Humidity",
        condition="Conditions",
    )
    .tab_header(
        title="Gibraltar Temperature and Humidity Conditions",
        subtitle="Morning, Noon, and Evening Readings"
    )
    .opt_stylize(style=1, color="cyan")
)
```

The custom datetime formatting string in `format_str="%b %d %Y (%a) - %I:%M %p"` creates a readable datetime format that's perfect for weather reporting, showing the day of week, month, day, year, and the time in 12-hour 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:

1. [GitHub Issues](https://github.com/posit-dev/great-tables/issues)
2. [GitHub Discussions](https://github.com/posit-dev/great-tables/discussions)
3. [Discord](https://discord.com/invite/Ux7nrcXHVV)

Keep building those beautiful tables!
