Pie and Donut Charts: Showing Part-to-Whole (and When Not To)

BaseballBeginnerPython~5 min read

What you'll build

A pie and a donut of each division's share of league-wide runs, plus when a bar beats both.

A pie and a donut of each division's share of league-wide runs, plus when a bar beats both.
Data: Bundled sample (2023 MLB standings), retrieved June 2026

I'll defend the pie chart, but only just. They earn their bad reputation most of the time — yet they do one job genuinely well: showing what share of a whole each part takes. Give a pie (or its hollow cousin, the donut) a few categories that sum to a meaningful total and it reads instantly. I'll build both for each division's share of league-wide runs, then show you the case where a plain bar is the honest choice instead.

This builds on Your First Visualization. The data is the bundled sample_standings.csv (real 2023 MLB standings), so it runs offline.

  1. A real part-to-whole

    Pies only make sense when the slices genuinely sum to a whole. "Each division's share of all runs scored" qualifies: every run belongs to exactly one division, and the six shares add to 100%.

    python
    import pandas as pd
    
    df = pd.read_csv("sample_standings.csv")
    runs_by_div = df.groupby("Division")["RS"].sum().sort_values(ascending=False)
    shares = (runs_by_div / runs_by_div.sum() * 100).round(1)
    print(shares.to_string())
    Each division's share of league-wide runs
    Share of league-wide runs scored, by division:
    Division
    AL East       17.2
    NL East       17.1
    NL West       16.9
    AL West       16.9
    NL Central    16.7
    AL Central    15.2

    Already a warning sign: the shares are all bunched between ~15% and ~17%. Hold that thought — it's exactly the pattern pies handle badly, and we'll come back to it.

  2. Pie and donut, side by side

    ax.pie() takes the values and draws the slices; autopct prints each percentage. A donut is just a pie with a hole, set by wedgeprops=dict(width=...) — the hole is handy for a title or total in the center, and many find it easier to read.

    python
    import matplotlib.pyplot as plt
    
    colors = plt.cm.Set2(range(len(runs_by_div)))
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 5.5))
    
    ax1.pie(runs_by_div, labels=runs_by_div.index, colors=colors,
            autopct="%1.1f%%", startangle=90, counterclock=False)
    
    ax2.pie(runs_by_div, labels=runs_by_div.index, colors=colors,
            autopct="%1.1f%%", startangle=90, counterclock=False,
            wedgeprops=dict(width=0.42))          # the hole makes it a donut
    fig.savefig("pie_donut.png", dpi=144, bbox_inches="tight")
    A pie chart and a donut chart side by side, each showing six divisions' near-equal shares of league-wide runs scored, all slices close to one-sixth
    Data: Bundled sample (2023 MLB standings), retrieved June 2026

    startangle=90, counterclock=False starts the first slice at the top and goes clockwise — the order people instinctively read. Without it, pies start at 3 o'clock and run counterclockwise, which feels subtly wrong.

  3. When a bar wins

    Look at the chart: six slices all near one-sixth. Can you tell, by eye, whether the AL East or NL East slice is bigger? Almost nobody can — the human eye judges lengths far better than angles or areas. That's the pie's fatal flaw: when values are close, or when there are many of them, the slices are indistinguishable and the reader leans entirely on the printed numbers (at which point you've drawn a table, not a chart). A pie earns its place only with few categories and clearly different sizes (think 60/30/10). For ranking near-equal values like these, a sorted bar chart shows the differences honestly. Choose the pie for "it's roughly two-thirds versus one-third," and a bar for everything else.

Troubleshooting

My pie is an oval, not a circle

The axes aren't square. Call ax.set_aspect("equal") (or ax.axis("equal")) so the pie renders as a true circle regardless of figure shape.

The slices start in a weird place

Add startangle=90, counterclock=False to begin at the top and read clockwise. Matplotlib's default (start at 3 o'clock, go counterclockwise) confuses most readers.

The percent labels overlap on small slices

Too many tiny slices is the pie telling you it's the wrong chart. Group small categories into an "Other" slice, or switch to a bar. You can also push labels out with pctdistance/labeldistance, but fixing the chart choice is better.

Challenge yourself

Make the honest version: plot the same six division shares as a horizontal bar chart sorted high to low. Now the ranking the pie hid is obvious. Then build a pie where a pie actually works — collapse the data to just American League vs National League total runs (two clearly comparable slices) and see how the chart type finally fits the question.

Get the code

Here's the complete, working script for this tutorial. It runs exactly as shown.

Download the finished script (61_pie_and_donut_part_to_whole.py)

This script imports a small shared helper (and reads any bundled sample data) that live next to it in /downloads/ — grab these into the same folder so it runs as-is: sdt_common.py.

More Baseball tutorials

Your first real Statcast pull, cached, with an exit-velocity histogram.
Baseball Beginner

Pull Your First MLB Data with pybaseball

Install pybaseball, turn on caching, and pull a week of real Statcast data. End with a histogram of batted-ball exit velocity so you can see the data is genuinely there.

~8 min
A pitch-location heatmap for one pitcher with the strike zone drawn on top.
Baseball Intermediate

Make a Pitch-Location Heatmap in Python

Use a single pitcher's Statcast data to build a 2-D location heatmap, draw the strike zone from the catcher's view, and read what the hot spots tell you.

~8 min