Create One Chart per Group with openpyxl
One chart with twelve lines on it — one per region — is unreadable; twelve small charts with one line each, arranged in a grid, are easy to scan. That arrangement, often called small multiples, is a standard way to compare groups, and building it by hand in Excel is exactly the kind of repetitive work a script should do. openpyxl can create any number of native charts in a loop. The work is in three details: each chart must reference only its own group's rows, the charts must be positioned so they do not overlap, and — the one most often missed — every chart must use the same axis scale, or the eye compares shapes that are not comparable. This page belongs to Creating Charts in Excel with openpyxl.
Prerequisites
pip install openpyxl pandas
regional_sales.xlsx is a long table with one row per region and month: six regions, nine months. LibreOffice (soffice) is used at the end to render the sheet for a quick visual check; it is optional.
Step 1 — Write each group as its own block
A chart references a rectangular range, so each region's data needs to sit in a contiguous block. Pivoting to one column per region is the compact way:
import pandas as pd
from openpyxl import Workbook
sales = pd.read_excel("regional_sales.xlsx")
wide = sales.pivot(index="Month", columns="Region", values="Sales")
regions = list(sales["Region"].unique())
wide = wide[regions]
wb = Workbook()
ws = wb.active
ws.title = "Data"
ws.append(["Month", *regions])
for month, row in wide.iterrows():
ws.append([month, *row.tolist()])
print(ws.dimensions, "|", [c.value for c in ws[1]])
The data sheet now has months down column A and one column per region. Each chart will use column A for its categories and one region's column for its values. Keeping the data on its own sheet and the charts on another keeps the dashboard tidy and lets people inspect the numbers. Reshaping long data to wide is covered in Create a Pivot Table from Excel with pandas.
Step 2 — One chart per column, in a loop
from openpyxl.chart import LineChart, Reference
n_months = len(wide)
y_max = float(wide.max().max()) * 1.1 # one scale for every chart
def region_chart(col_idx, title):
chart = LineChart()
chart.title = title
chart.add_data(Reference(ws, min_col=col_idx, min_row=1, max_row=n_months + 1), titles_from_data=True)
chart.set_categories(Reference(ws, min_col=1, min_row=2, max_row=n_months + 1))
chart.legend = None
chart.y_axis.scaling.min = 0
chart.y_axis.scaling.max = round(y_max, -2)
chart.x_axis.delete = False
chart.y_axis.delete = False
chart.width, chart.height = 9, 5.5
for s in chart.series:
s.smooth = False
return chart
charts = [region_chart(i, name) for i, name in enumerate(regions, start=2)]
print(len(charts), "charts; shared y-axis max:", round(y_max, -2))
Each chart takes one region's column (from row 1, so the header becomes the series name) and the shared month column as categories. The legend is dropped because the title already names the single series. The two lines that matter most are the shared axis limits: every chart runs from 0 to the same maximum, rounded to the nearest hundred. Without them, Excel scales each chart to its own data, and West's flat line at 600 would look as dramatic as East's line at 1,500 — the most common way small multiples mislead. smooth = False keeps openpyxl's line series from being drawn as curves, a default noted in Add a Line Chart to an Excel Report with Python.
Step 3 — Lay the charts out in a grid
Charts are anchored to a cell. Compute the anchor for each position in a grid of, say, three columns:
from openpyxl.utils import get_column_letter
dash = wb.create_sheet("Charts", 0)
dash["A1"] = "Sales by region, January–September 2026 (same scale on every chart)"
COLS, CELL_W, CELL_H = 3, 6, 12 # charts per row; columns and rows per chart slot
for i, chart in enumerate(charts):
row_slot, col_slot = divmod(i, COLS)
anchor = f"{get_column_letter(1 + col_slot * CELL_W)}{3 + row_slot * CELL_H}"
dash.add_chart(chart, anchor)
print(f"{regions[i]:8} -> {anchor}")
wb.active = 0
wb.save("regional_small_multiples.xlsx")
divmod(i, 3) turns the chart number into a grid position — row 0, columns 0–2, then row 1 — and each slot is six columns wide and twelve rows tall, a little more than the chart's own size, so neighbouring charts never overlap. The title row above the grid states that the scales are shared, which tells readers they may compare heights directly. Making the chart sheet the active one means the file opens on the dashboard.
Step 4 — Verify what was written
Reload the file and confirm each chart points at its own column and shares the scale:
from openpyxl import load_workbook
check = load_workbook("regional_small_multiples.xlsx")
for chart in check["Charts"]._charts:
series = chart.series[0]
print(series.tx.strRef.f, series.val.numRef.f, "max", chart.y_axis.scaling.max)
Every chart reads a different column — $B, $C, $D… — with the same nine rows, and every axis maximum is the same. A test asserting these two properties protects the dashboard against an off-by-one in the loop, which would otherwise show one region's numbers under another region's title. For a visual check, render the chart sheet to PDF with LibreOffice as in Convert Excel to PDF on Linux with LibreOffice.
Step 5 — Variant: one sheet per group
When each group's owner should get "their" sheet — a tab per region with its table and its chart — write the group's rows to a new sheet and put the chart beside them:
wb2 = Workbook()
wb2.remove(wb2.active)
for region, group in sales.groupby("Region", sort=False):
sh = wb2.create_sheet(region)
sh.append(["Month", "Sales"])
for row in group[["Month", "Sales"]].itertuples(index=False):
sh.append(list(row))
chart = LineChart()
chart.title = f"{region} sales"
chart.add_data(Reference(sh, min_col=2, min_row=1, max_row=len(group) + 1), titles_from_data=True)
chart.set_categories(Reference(sh, min_col=1, min_row=2, max_row=len(group) + 1))
chart.y_axis.scaling.min, chart.y_axis.scaling.max = 0, round(y_max, -2)
chart.x_axis.delete = chart.y_axis.delete = False
chart.legend = None
sh.add_chart(chart, "D2")
wb2.save("regional_sheets.xlsx")
print(load_workbook("regional_sheets.xlsx").sheetnames)
Each sheet holds one region's rows and a chart that references them — with the same shared maximum, so a manager flipping between tabs sees comparable charts. Splitting the sheets into separate files for distribution is covered in Save Each Sheet of a Workbook as a Separate File.
Performance notes
Each chart is a small XML part, so dozens of charts add little to the file size or to the time it takes to write it — six charts here write in a fraction of a second. The practical limits are human and visual: Excel redraws every chart on a sheet when it opens or recalculates, so a sheet with a hundred charts feels sluggish, and nobody reads a hundred charts anyway. Keep the grid to the groups that matter — the top twelve by revenue, say, plus an "all other" chart — and move the rest to a detail sheet or a separate file. If the data has thousands of groups, a heatmap table often communicates better than any number of charts; see Turn a Pivot Table into a Heatmap in Excel.
Common pitfalls
- Independent axis scales. Each chart auto-scales to its own data; set the same
minandmaxeverywhere. - Overlapping charts. Compute anchors from a grid larger than the chart size.
- Off-by-one references. Header row 1, data from row 2; check each chart's ranges after writing.
- Smoothed lines. openpyxl line series are smoothed unless
smooth = False. - Too many charts on one sheet. Beyond a dozen or so, split by sheet or filter to the groups that matter.
Conclusion
Pivot the long data so each group is a contiguous column, create one chart per column in a loop with its own value range and the shared category range, and give every chart the same axis limits so they can be compared. Place the charts in a grid with anchors computed from divmod, make the chart sheet active, verify each chart's ranges in a test, and use one sheet per group when the charts are meant for different owners.
Frequently asked questions
How do I create a chart for each group in a DataFrame with openpyxl? Pivot the data so each group has its own column, then loop over the columns and create a chart per column with Reference objects for that column's values and the shared category column. Add each chart at its own anchor cell.
How do I place many charts in a grid without overlapping? Compute each chart's anchor from its position: row and column slot from divmod(index, charts_per_row), multiplied by a slot size in columns and rows that is larger than the chart.
How do I give all small charts the same y-axis scale? Set chart.y_axis.scaling.min and chart.y_axis.scaling.max to the same values on every chart, typically zero and the overall maximum plus a margin.
How do I put each group on its own sheet with its chart? Group the DataFrame, create a sheet per group, write the group's rows, and add a chart that references that sheet's range. Use the same axis limits so the sheets are comparable.
Related
- Up one level: Creating Charts in Excel with openpyxl — native charts from Python.
- Build a Dashboard Sheet with Charts from Multiple Tabs — charts referencing several sheets.
- Create a Sheet for Each Group in a DataFrame — the sheet-per-group pattern without charts.
- Make an Excel Chart Grow with Its Data — keeping chart ranges current.
More on charts & images
- How-toBuild a Dashboard Sheet with Charts from Multiple Tabs
- How-toBuild a Rolling 12-Month Chart on an Excel Dashboard
- How-toKeep Charts and Images When Filling an Excel Template
- How-toAdd a Chart to an Excel File with xlsxwriter
- How-toAdd Sparklines to an Excel Report with xlsxwriter
- How-toBuild a Waterfall Chart in Excel with Python