Clone the Reciprocal Tariffs Table Using Plotnine

python
polars
plotnine
Author

Jerry Wu

Published

April 16, 2025

This post presents a recreated visualization using Polars and plotnine, based on my earlier work.

Below is the final figure: Cloned Reciprocal Tariffs Table

Show full code
import matplotlib.pyplot as plt
import polars as pl
from highlight_text import ax_text
from matplotlib.axes import Axes
from matplotlib.figure import Figure

from plotnine import (
    aes,
    element_blank,
    element_rect,
    element_text,
    geom_segment,
    geom_text,
    ggplot,
    position_nudge,
    scale_color_identity,
    scale_y_discrete,
    theme,
    theme_void,
    watermark,
)


# source1: https://truthsocial.com/@realDonaldTrump/114270398531479278
# source2:
# "https://upload.wikimedia.org/wikipedia/commons/
# thumb/3/36/Seal_of_the_President_of_the_United_States.svg/
# 800px-Seal_of_the_President_of_the_United_States.svg.png"
logo_filename = "logo_resized.png"

data = {
    "country": [
        "China",
        "European Union",
        "Vietnam",
        "Taiwan",
        "Japan",
        "India",
        "South Korea",
        "Thailand",
        "Switzerland",
        "Indonesia",
        "Malaysia",
        "Cambodia",
        "United Kingdom",
        "South Africa",
        "Brazil",
        "Bangladesh",
        "Singapore",
        "Israel",
        "Philippines",
        "Chile",
        "Australia",
        "Pakistan",
        "Turkey",
        "Sri Lanka",
        "Colombia",
    ],
    "tariffs_charged": [
        "67%",
        "39%",
        "90%",
        "64%",
        "46%",
        "52%",
        "50%",
        "72%",
        "61%",
        "64%",
        "47%",
        "97%",
        "10%",
        "60%",
        "10%",
        "74%",
        "10%",
        "33%",
        "34%",
        "10%",
        "10%",
        "58%",
        "10%",
        "88%",
        "10%",
    ],
    "reciprocal_tariffs": [
        "34%",
        "20%",
        "46%",
        "32%",
        "24%",
        "26%",
        "25%",
        "36%",
        "31%",
        "32%",
        "24%",
        "49%",
        "10%",
        "30%",
        "10%",
        "37%",
        "10%",
        "17%",
        "17%",
        "10%",
        "10%",
        "29%",
        "10%",
        "44%",
        "10%",
    ],
}

country, tariffs_charged, reciprocal_tariffs = data.keys()

dark_navy_blue = "#0B162A"  # background
light_blue = "#B5D3E7"  # row
white = "#FFFFFF"  # row
yellow = "#F6D588"  # "reciprocal_tariffs" column
gold = "#FFF8DE"  # logo

fontname_georgia = "Georgia"  # title
fontname_roboto = "Roboto"  # body

# column width
x_col1_start, x_col1_end = 5, 52.5
x_col2_start, x_col2_end = 60, 75
x_col3_start, x_col3_end = 82.5, 97.5

# x-position for body text
x_col1_text = 5
x_col2_text = x_col2_start + (x_col2_end - x_col2_start) / 3 + 1
x_col3_text = x_col3_start + (x_col3_end - x_col3_start) / 3 + 1

geom_segment_props = {
    "size": 8,
    "lineend": "round",
}
geom_text_props = {
    "ha": "left",
    "va": "center",
    "position": position_nudge(y=-0.08),
    "size": 10,
    "fontweight": "bold",
}


def tweak_df() -> pl.DataFrame:
    return (
        pl.DataFrame(data)
        .with_row_index()
        .with_columns(
            pl.col(country).cast(pl.Categorical),
            pl.when(pl.col("index").mod(2).eq(0))
            .then(pl.lit(light_blue))
            .otherwise(pl.lit(white))
            .alias("color_mod"),
        )
    )


def plot_g() -> ggplot:
    return (
        ggplot(data=df, mapping=aes(y=country, yend=country))
        # col1 segment
        + geom_segment(
            mapping=aes(x=x_col1_start, xend=x_col1_end, color="color_mod"),
            **geom_segment_props,
        )
        # col2 segment
        + geom_segment(
            mapping=aes(x=x_col2_start, xend=x_col2_end, color="color_mod"),
            **geom_segment_props,
        )
        # col3 segment
        + geom_segment(
            mapping=aes(x=x_col3_start, xend=x_col3_end),
            color=yellow,
            **geom_segment_props,
        )
        # col1 text
        + geom_text(aes(x=x_col1_text, label=country), **geom_text_props)
        # col2 text
        + geom_text(aes(x=x_col2_text, label=tariffs_charged), **geom_text_props)
        # col3 text
        + geom_text(aes(x=x_col3_text, label=reciprocal_tariffs), **geom_text_props)
        # using "color_mod" column directly
        + scale_color_identity()
        # expand extra space
        + scale_y_discrete(
            limits=df.select(country).reverse().to_series().to_list(),
            expand=(0.02, 0, 0.14, 0),
        )
        # logo
        + watermark(logo_filename, 100, 2235)
    )


def themify(p: ggplot) -> Figure:
    return (
        p
        + theme_void()
        + theme(
            legend_position="none",  # turns off the legend
            axis_text_x=element_blank(),
            axis_text_y=element_blank(),
            axis_title_x=element_blank(),
            axis_title_y=element_blank(),
            panel_background=element_rect(fill=dark_navy_blue),
            plot_background=element_rect(fill=dark_navy_blue),
            text=element_text(family=fontname_roboto),
            dpi=300,
            figure_size=(6, 8),
        )
    ).draw(False)


def add_ax_text(ax: Axes) -> Axes:
    title_fontsize = 12
    title_fontweight = "bold"
    heading_fontsize = 8
    heading_fontweight = "bold"
    subheading_fontsize = 6
    subheading_fontweight = "normal"

    title_props = {
        "color": gold,
        "fontsize": title_fontsize,
        "fontweight": title_fontweight,
        "fontname": fontname_georgia,
    }
    heading_props = {
        "color": white,
        "fontsize": heading_fontsize,
        "fontweight": heading_fontweight,
        "fontname": fontname_georgia,
    }
    subheading_props = {
        "color": white,
        "fontsize": subheading_fontsize,
        "fontweight": subheading_fontweight,
        "fontname": fontname_georgia,
    }

    x_ref, y_ref = 25.5, 6  # ref point

    # title
    ax_text(
        s="<Reciprocal Tariffs>",
        x=x_ref + 7,
        y=y_ref + 20.9,
        fontsize=heading_fontsize,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[title_props],
    )
    # col1
    ax_text(
        s="<Country>",
        x=x_ref + 4,
        y=y_ref + 19.5,
        fontsize=heading_fontsize,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[heading_props],
    )
    # col2
    ax_text(
        s="<Tariffs Charged>",
        x=x_ref + 42,
        y=y_ref + 20.8,
        fontsize=heading_fontsize,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[heading_props],
    )
    ax_text(
        s="<to the U.S.A.>",
        x=x_ref + 42,
        y=y_ref + 20.4,
        fontsize=heading_fontsize,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[heading_props],
    )
    ax_text(
        s="<Including>",
        x=x_ref + 42,
        y=y_ref + 20.1,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[subheading_props],
    )
    ax_text(
        s="<Currency Manipulation>",
        x=x_ref + 42,
        y=y_ref + 19.8,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[subheading_props],
    )
    ax_text(
        s="<and Trade Barriers>",
        x=x_ref + 42,
        y=y_ref + 19.5,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[subheading_props],
    )
    # col3
    ax_text(
        s="<U.S.A. Discounted>",
        x=x_ref + 64,
        y=y_ref + 20,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[heading_props],
    )
    ax_text(
        s="<Reciprocal Tariffs>",
        x=x_ref + 64,
        y=y_ref + 19.6,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[heading_props],
    )
    return ax


df = tweak_df()
p = plot_g()
fig = themify(p)
ax = fig.axes[0]
ax = add_ax_text(ax)
fig

Preparing the DataFrame

We start by extracting the data from a previous project. The country column is converted to a categorical type, which simplifies handling in plotnine. To enable alternating row colors in the final visualization, we also create a new column called color_mod.

def tweak_df() -> pl.DataFrame:
    return (
        pl.DataFrame(data)
        .with_row_index()
        .with_columns(
            pl.col(country).cast(pl.Categorical),
            pl.when(pl.col("index").mod(2).eq(0))
            .then(pl.lit(light_blue))
            .otherwise(pl.lit(white))
            .alias("color_mod"),
        )
    )

df = tweak_df()

Constructing the ggplot Object

With the processed DataFrame ready, we can now build the ggplot() object:

def plot_g() -> ggplot:
    return (
        ggplot(data=df, mapping=aes(y=country, yend=country))
        # col1 segment
        + geom_segment(
            mapping=aes(x=x_col1_start, xend=x_col1_end, color="color_mod"),
            **geom_segment_props,
        )
        # col2 segment
        + geom_segment(
            mapping=aes(x=x_col2_start, xend=x_col2_end, color="color_mod"),
            **geom_segment_props,
        )
        # col3 segment
        + geom_segment(
            mapping=aes(x=x_col3_start, xend=x_col3_end),
            color=yellow,
            **geom_segment_props,
        )
        # col1 text
        + geom_text(aes(x=x_col1_text, label=country), **geom_text_props)
        # col2 text
        + geom_text(aes(x=x_col2_text, label=tariffs_charged), **geom_text_props)
        # col3 text
        + geom_text(aes(x=x_col3_text, label=reciprocal_tariffs), **geom_text_props)
        # using "color_mod" column directly
        + scale_color_identity()
        # expand extra space
        + scale_y_discrete(
            limits=df.select(country).reverse().to_series().to_list(),
            expand=(0.02, 0, 0.14, 0),
        )
        # logo
        + watermark(logo_filename, 100, 2235)
    )

What’s happening here?

  1. geom_segment(): Since I couldn’t find a way to apply border radius, I used geom_segment() with lineend="round" as the best available workaround. Thick lines serve as cell backgrounds.
  2. geom_text(): Adds text for each column.
  3. scale_color_identity(): Uses the color values directly from the color_mod column, without applying a scale.
  4. scale_x_discrete(): Reorders the country axis and tweaks padding to add space above and below the table.
  5. watermark(): Embeds a logo. Since there’s no native figure size parameter in plotnine, I manually scaled the output.

Custom Theme

We apply a tailored theme with themify() to refine the figure’s appearance:

def themify(p: ggplot) -> Figure:
    return (
        p
        + theme_void()
        + theme(
            legend_position="none",  # turns off the legend
            axis_text_x=element_blank(),
            axis_text_y=element_blank(),
            axis_title_x=element_blank(),
            axis_title_y=element_blank(),
            panel_background=element_rect(fill=dark_navy_blue),
            plot_background=element_rect(fill=dark_navy_blue),
            text=element_text(family=fontname_roboto),
            dpi=300,
            figure_size=(6, 8),
        )
    ).draw(False)

Adding the Title and Column Headers

We use ax_text() from HighlightText to manually position and style each line of text.

def add_ax_text(ax: Axes) -> Axes:
    title_fontsize = 12
    title_fontweight = "bold"
    heading_fontsize = 8
    heading_fontweight = "bold"
    subheading_fontsize = 6
    subheading_fontweight = "normal"

    title_props = {
        "color": gold,
        "fontsize": title_fontsize,
        "fontweight": title_fontweight,
        "fontname": fontname_georgia,
    }
    heading_props = {
        "color": white,
        "fontsize": heading_fontsize,
        "fontweight": heading_fontweight,
        "fontname": fontname_georgia,
    }
    subheading_props = {
        "color": white,
        "fontsize": subheading_fontsize,
        "fontweight": subheading_fontweight,
        "fontname": fontname_georgia,
    }

    x_ref, y_ref = 25.5, 6  # ref point

    # title
    ax_text(
        s="<Reciprocal Tariffs>",
        x=x_ref + 7,
        y=y_ref + 20.9,
        fontsize=heading_fontsize,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[title_props],
    )
    # col1
    ax_text(
        s="<Country>",
        x=x_ref + 4,
        y=y_ref + 19.5,
        fontsize=heading_fontsize,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[heading_props],
    )
    # col2
    ax_text(
        s="<Tariffs Charged>",
        x=x_ref + 42,
        y=y_ref + 20.8,
        fontsize=heading_fontsize,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[heading_props],
    )
    ax_text(
        s="<to the U.S.A.>",
        x=x_ref + 42,
        y=y_ref + 20.4,
        fontsize=heading_fontsize,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[heading_props],
    )
    ax_text(
        s="<Including>",
        x=x_ref + 42,
        y=y_ref + 20.1,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[subheading_props],
    )
    ax_text(
        s="<Currency Manipulation>",
        x=x_ref + 42,
        y=y_ref + 19.8,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[subheading_props],
    )
    ax_text(
        s="<and Trade Barriers>",
        x=x_ref + 42,
        y=y_ref + 19.5,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[subheading_props],
    )
    # col3
    ax_text(
        s="<U.S.A. Discounted>",
        x=x_ref + 64,
        y=y_ref + 20,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[heading_props],
    )
    ax_text(
        s="<Reciprocal Tariffs>",
        x=x_ref + 64,
        y=y_ref + 19.6,
        ax=ax,
        va="bottom",
        ha="center",
        highlight_textprops=[heading_props],
    )
    return ax

Each line is added with a separate ax_text() call, which gives me more flexibility during alignment and styling. That said, I suspect there’s a cleaner way to do this.

Final Rendering

Now, let’s tie it all together:

p = plot_g()
fig = themify(p)
ax = fig.axes[0]
ax = add_ax_text(ax)
fig

Closing Thoughts

This post showcases how plotnine can be used to create table-like visualizations. I’m genuinely impressed by its capabilities — it’s surprisingly fun to approach a table as a figure.

That said, fine-tuning the layout and text positioning still feels a bit clunky at this stage. I hope to find a more structured and reliable way to handle that moving forward.

It would be exciting to explore how plotnine and Great Tables might work together to enable even richer visual storytelling — I’m looking forward to diving into that next.

Disclaimer
  1. This table is intended as a self-practice project, and the data in the table may not be 100% accurate. Please refer to the original source if you require verified data.
  2. This post was drafted by me, with AI assistance to refine the content.