Guide
Formatting And Charting Excel Reports With PythonDeep dive

Create a Pie Chart in Excel with openpyxl

PieChart, data labels that show percentages, exploding a slice, a doughnut variant — and the honest guidance on when a bar chart tells the same story better.

A pie chart does one thing well: showing that a single share dominates, or that one of three or four parts is much larger than the rest. Past that it becomes hard work — human eyes compare angles badly, and two slices within a few percentage points of each other look identical.

So this guide covers both halves of the job: how to build a good pie chart in openpyxl, and how to tell when the same data belongs in a bar chart instead. It is part of Creating Charts in Excel with openpyxl.

When a pie reads well and when it does not With three parts of clearly different sizes, a pie makes the dominant share obvious at a glance. With eight similar categories the slices become indistinguishable wedges that can only be read from their labels, at which point a sorted bar chart conveys the same information immediately. three uneven parts — a pie works Core 55% Services 28% Other 17% the dominant share is obvious without reading a number eight similar parts — use a bar 14% 13% 13% 12% 12% 11% 11% ranking and near-ties are readable; as wedges they would not be

Prerequisites

Bash
pip install openpyxl

No Excel installation is needed. openpyxl writes the chart definition into the file, and Excel or LibreOffice renders it when the workbook is opened.

Step 1: Write the data the chart plots

An Excel chart references cells, so the categories and values have to be in the sheet:

Python
from openpyxl import Workbook
from openpyxl.chart import PieChart, Reference

wb = Workbook()
ws = wb.active
ws.title = "Revenue"

rows = [
    ("Category", "Revenue"),
    ("Core products", 552_000),
    ("Services", 281_000),
    ("Support", 96_000),
    ("Training", 71_000),
]
for row in rows:
    ws.append(row)

for cell in ws["B"][1:]:
    cell.number_format = '#,##0'
ws.column_dimensions["A"].width = 18
ws.column_dimensions["B"].width = 14

Four categories is about the practical maximum for a pie. If your data has twelve, group the small ones into an "Other" row before charting — the chart is a summary, and a slice representing 0.4% conveys nothing except clutter.

Step 2: Build the chart

Python
chart = PieChart()
chart.title = "Revenue by category"

labels = Reference(ws, min_col=1, min_row=2, max_row=5)          # no header
data = Reference(ws, min_col=2, min_row=1, max_row=5)            # header included

chart.add_data(data, titles_from_data=True)
chart.set_categories(labels)
chart.height = 9          # centimetres
chart.width = 14

ws.add_chart(chart, "D2")
wb.save("pie.xlsx")

The asymmetry between the two Reference calls is the thing to get right and the source of most pie-chart bugs. The values reference starts at the header row because titles_from_data=True takes the series name from that first cell. The categories reference starts at the first data row, because a category list has no header. Get them the same way round and you either lose a slice or get a series called "552000".

height and width are in centimetres, unlike xlsxwriter's pixels — a small difference that matters when porting code between the two.

Step 3: Show percentages rather than raw numbers

A pie's whole subject is proportion, so the labels should say so:

Python
from openpyxl.chart.label import DataLabelList

chart.dataLabels = DataLabelList()
chart.dataLabels.showPercent = True
chart.dataLabels.showVal = False
chart.dataLabels.showCatName = False
chart.dataLabels.showLegendKey = False

Excel computes the percentages itself from the plotted values, so they always total 100 — which is better than pre-computing them in pandas, where rounding can leave you presenting a chart that adds to 99.9%.

Set showCatName = True and drop the legend if you would rather label the slices directly. That is usually the more readable choice: a legend forces the reader to match colours to names, while a labelled slice does not.

Step 4: Colour and explode individual slices

Assign a DataPoint per slice to control its fill, and use the same technique to pull one slice out for emphasis:

Python
from openpyxl.chart.marker import DataPoint
from openpyxl.chart.shapes import GraphicalProperties

COLOURS = ["5B5CF0", "0F766E", "B4740A", "BE185D"]

series = chart.series[0]
series.data_points = []
for idx, colour in enumerate(COLOURS):
    point = DataPoint(idx=idx)
    point.graphicalProperties = GraphicalProperties(solidFill=colour)
    if idx == 0:
        point.explosion = 12          # pull the first slice out slightly
    series.data_points.append(point)

wb.save("pie.xlsx")

idx is the zero-based position of the slice, matching the order of the data rows. Explosion is measured as a percentage of the radius — 10 to 15 reads as deliberate emphasis, while 40 makes the chart look broken.

Explode exactly one slice, or none. The moment two are pulled out, the reader loses the reference circle that made the proportions legible in the first place.

Why the two Reference ranges start on different rows The values reference includes the header cell because titles_from_data takes the series name from it. The categories reference starts at the first data row, because the label list has no header. Starting both at the same row either consumes a data row as a title or turns the header into a category. Category Revenue Core products 552,000 Services 281,000 Support 96,000 categories min_row=2 values min_row=1 titles_from_data=True Start both at row 1 and "Category" becomes a slice. Start both at row 2 and the series is named "552000".

Step 4b: Group the tail before you chart it

The commonest reason a generated pie is unreadable is that nobody decided how many slices it should have — the chart simply plots whatever the query returned, and a category list that grows from four to fourteen over a year turns a clear picture into a colour wheel. Deciding the cut in code keeps it stable:

Python
import pandas as pd


def top_n_with_other(frame, label_col, value_col, n=4, other="Other"):
    """Keep the largest n categories; roll everything else into one row."""
    ranked = frame.sort_values(value_col, ascending=False).reset_index(drop=True)
    if len(ranked) <= n + 1:
        return ranked
    head = ranked.iloc[:n]
    tail_total = ranked.iloc[n:][value_col].sum()
    tail = pd.DataFrame({label_col: [other], value_col: [tail_total]})
    return pd.concat([head, tail], ignore_index=True)
Rolling the long tail into a single Other slice Eleven categories produce seven slices below three percent each, which are too thin to see and too many to label. Keeping the largest four and summing the rest into one Other slice preserves the total exactly while leaving five readable segments. eleven categories as written seven slivers under 3% each — unlabellable top four plus Other Other five readable segments, and the total is unchanged

Summing rather than dropping the tail is the part that matters: a pie whose slices do not add to the reported total is worse than one with too many slices, because the percentages Excel computes are shares of what was plotted. Name the rolled-up slice for what it is — "Other (7 categories)" is more honest than a bare "Other", and it tells the reader there is detail to ask for.

Step 5: The doughnut variant

A doughnut is the same data with a hole, and openpyxl exposes it as its own class:

Python
from openpyxl.chart import DoughnutChart

donut = DoughnutChart(holeSize=55)
donut.title = "Revenue mix"
donut.add_data(data, titles_from_data=True)
donut.set_categories(labels)
donut.dataLabels = DataLabelList()
donut.dataLabels.showPercent = True
ws.add_chart(donut, "D22")
wb.save("pie.xlsx")

The hole gives you space for a headline number in a dashboard, but it also removes the wedge angles at the centre, which is where the proportion is easiest to judge. A doughnut is the weaker chart of the two for reading proportions and the better one for a dashboard tile; choose on that basis rather than on appearance.

Common pitfalls and gotchas

SymptomCauseFix
Only one slice appearsValues Reference covers a single cellSpan every data row
A slice labelled "Category"Categories reference included the headerStart categories at min_row=2
Series named "552000"Values reference excluded the header with titles_from_data=TrueStart values at min_row=1
Labels show raw valuesshowPercent not setDataLabelList() with showPercent=True
Percentages do not total 100Pre-computed and rounded in pandasLet Excel compute them from values
Every slice is the default blueNo data_points assignedOne DataPoint per slice with a fill
Chart covers the dataAnchored inside the tableAnchor to the right of the last column
Twelve unreadable sliversToo many categoriesGroup the tail into "Other"

Performance and scale notes

A pie chart is a few kilobytes in the file regardless of the values behind it, so the cost is never the chart. The scale question is the number of categories, and it is a design limit rather than a technical one: past six slices the chart stops being readable long before anything gets slow.

When a report needs the same breakdown for many groups — one pie per region, say — resist a grid of twelve pies. A single grouped or stacked bar chart shows all of them at once and lets the reader compare across groups, which a row of pies cannot. Create a Bar Chart in Excel with openpyxl covers that shape.

Conclusion

Build the pie from two Reference ranges that deliberately start on different rows — values from the header with titles_from_data=True, categories from the first data row — and let Excel compute the percentage labels so they always add to 100. Colour the slices explicitly with DataPoint objects, explode at most one, and group the long tail into "Other" before charting. Then check the shape of the data: three or four uneven parts make a good pie, and everything else makes a better bar chart.

Frequently asked questions

Why does my pie chart only show one slice? The values Reference included the header row while titles_from_data was not set, or it covered a single cell. Check that the Reference spans every data row and that min_row is the header when titles_from_data=True.

How do I show percentages instead of raw values? Attach a DataLabelList with showPercent=True. Excel computes the percentages from the plotted values, so they always add to 100 even when the underlying numbers are rounded.

Can I set the colour of an individual slice? Yes. Assign a DataPoint with a GraphicalProperties fill to series.data_points, one per slice you want to control.

When should I not use a pie chart? When there are more than five or six categories, when two shares are close, or when the reader needs to compare across periods. A bar chart is easier to read in all three cases.

Up to the parent guide:

Related guides: