Create a Pie Chart in Excel with openpyxl
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.
Prerequisites
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:
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
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:
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:
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.
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:
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)
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:
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
| Symptom | Cause | Fix |
|---|---|---|
| Only one slice appears | Values Reference covers a single cell | Span every data row |
| A slice labelled "Category" | Categories reference included the header | Start categories at min_row=2 |
| Series named "552000" | Values reference excluded the header with titles_from_data=True | Start values at min_row=1 |
| Labels show raw values | showPercent not set | DataLabelList() with showPercent=True |
| Percentages do not total 100 | Pre-computed and rounded in pandas | Let Excel compute them from values |
| Every slice is the default blue | No data_points assigned | One DataPoint per slice with a fill |
| Chart covers the data | Anchored inside the table | Anchor to the right of the last column |
| Twelve unreadable slivers | Too many categories | Group 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.
Related
Up to the parent guide:
- Creating Charts in Excel with openpyxl — chart types, references and anchoring.
Related guides:
- Create a Bar Chart in Excel with openpyxl — the chart most pies should have been.
- Add a Line Chart to an Excel Report with Python — for anything that moves over time.
- Add a Chart to an Excel File with xlsxwriter — the same charts on the write-only engine.
- Add a Summary Sheet to an Excel Report in Python — where a mix chart usually belongs.