Skip to content
datahacks.dev
Excel

Exporting Excel Charts as Images From a Script or Server

Export charts as PNG or SVG from a script: draw with matplotlib from the same data, render headless with LibreOffice, or use Windows COM.

6 min read
Summarize in AI
Flat illustration of a spreadsheet grid with a selected cell and a formula bar.

The chart lives in the workbook, and the request is for a picture. The weekly email needs it inline, the slide deck wants a PNG, and the web page embeds an SVG. Excel itself can save a chart as an image with a right-click, which does not help a job that runs at 6 a.m. on a server with no Excel on it. This article covers the three real options, what each gives you and what each costs, and how to get a picture that looks deliberate rather than accidental. It follows the chart-building steps in sending charts from a query to an Excel sheet and belongs to charts from data pulled out of databases.

Why Excel charts are hard to export on a server

An xlsx file stores a chart as XML: series, ranges, formatting. It does not store a picture of it. The picture is drawn by whatever program opens the file. Libraries such as openpyxl and XlsxWriter write that XML and cannot render it. So exporting "the chart in the workbook" requires an application that can draw it: Excel, or LibreOffice, which reads the same XML and draws its own interpretation.

That tells you the three routes.

Route one: draw the chart yourself

If the goal is a picture of the data, you do not need the workbook's chart at all. You have the data frame; draw it with a plotting library and save the image. This is the most dependable option for scheduled jobs, runs anywhere Python runs and gives you control over every detail.

Python
import matplotlib
matplotlib.use("Agg")                      # no display needed on a server
import matplotlib.pyplot as plt
import matplotlib.dates as mdates

fig, ax = plt.subplots(figsize=(8, 4), dpi=200)
ax.plot(df["month"], df["web"], color="#4C72B0", linewidth=2, label="Web")
ax.plot(df["month"], df["store"], color="#C44E52", linewidth=2, label="Store")
ax.set_title("Monthly revenue by channel", loc="left", fontsize=12)
ax.set_ylabel("Revenue (USD thousands)")
ax.yaxis.set_major_formatter(lambda v, _: f"{v/1000:,.0f}")
ax.xaxis.set_major_formatter(mdates.DateFormatter("%b %y"))
ax.spines[["top", "right"]].set_visible(False)
ax.grid(axis="y", color="#DDDDDD", linewidth=0.8)
ax.legend(frameon=False)
fig.text(0.01, 0.01, "Source: orders database, as of 2026-10-29", fontsize=8, color="#666666")
fig.tight_layout(rect=(0, 0.03, 1, 1))
fig.savefig("revenue.png", dpi=200)
fig.savefig("revenue.svg")
plt.close(fig)

Notes on the choices. matplotlib.use("Agg") selects a non-interactive backend that works without a display. figsize times dpi gives the pixel size: 8 by 4 inches at 200 dpi is 1600 by 800 pixels, which looks sharp on a retina screen and fits an email at reduced display width. PNG suits email and slides; SVG scales without blur and suits web pages and print. The source line inside the image survives when someone copies the picture without the caption.

Drawing from the same data frame that you write to the workbook keeps picture and table in step. The cost is that the image will not look exactly like the Excel chart; for most reports, that is fine, and the look is under your control. Put the style in one module (colors, fonts, spines, grid) so every chart in the report matches.

Other libraries do the same job: plotly with the kaleido engine writes static images, and altair with vl-convert does too. Use what your team already knows.

Route two: convert the workbook with LibreOffice

LibreOffice can open an xlsx file and render it to PDF, with charts included, with no display:

Shell
soffice --headless --convert-to pdf --outdir out/ report.xlsx

Then turn PDF pages into images with a tool such as pdftoppm:

Shell
pdftoppm -png -r 200 out/report.pdf out/page

This produces page-1.png and so on, one per printed page. To capture a single chart, put it alone on a sheet sized to the chart, with the print area set to it, and convert that workbook; the page is the chart. Use ws.set_landscape(), ws.fit_to_pages(1, 1) and a print area in XlsxWriter to control the page.

The caveats are real. LibreOffice renders charts its own way, with different fonts, spacing and defaults from Excel, and some formatting (a few chart types and effects) comes out differently or not at all. It needs the fonts installed on the server; a missing font is replaced silently, changing the layout. And the conversion takes seconds and memory per file, so run it in a worker with a timeout. If the Excel look is not essential, route one is simpler and prettier; use route two when you must render a workbook someone else built and you cannot redraw it.

Route three: Excel itself, on a desktop

On Windows with Excel installed, COM automation drives the real application and exports a chart exactly as Excel draws it:

Python
import win32com.client as win32

excel = win32.Dispatch("Excel.Application")
excel.Visible = False
excel.DisplayAlerts = False
wb = excel.Workbooks.Open(r"C:\reports\ops_dashboard.xlsx")
ws = wb.Worksheets("Dashboard")
ws.ChartObjects("Revenue").Chart.Export(r"C:\reports\revenue.png")
wb.Close(SaveChanges=False)
excel.Quit()

The result is the chart as the reader sees it. It is also the most fragile route. Excel is a desktop application: it needs a logged-in session or a configured service, can hang on a dialog nobody sees, leaks processes if the script crashes and is not licensed or supported by Microsoft for unattended server use. Use it on an analyst's PC for a one-off, or in a controlled workflow, not as a production dependency. Never use it on files from untrusted sources, since opening a workbook in Excel runs formulas and may evaluate links (opening untrusted Excel files safely in a pipeline). The xlwings library wraps the same automation with a friendlier interface and also works on macOS (running Python scripts from Excel with xlwings).

Make the image fit its destination

Size and format depend on where the picture goes.

DestinationFormatSize
Email bodyPNG600 to 700 px wide displayed; export at 2x
SlidePNG or SVGmatch the slide's chart area, 16:9 friendly
Web pageSVG, or PNG at 2xresponsive width
Print or PDFSVG or high-dpi PNG (300 dpi)page width
Chat messagePNGabout 1000 px wide

Email clients block remote images by default and do not render SVG, so inline PNG with a fixed width attribute. Provide alt text with the key message: "Store share of revenue fell from 38% to 31% over 24 months." Fonts: pick a widely available family, or embed the font in the SVG as paths, so the picture looks the same elsewhere.

Keep the picture honest

An exported image is detached from the data. Give it a title that states what it shows, the unit, the period and the source and date, either in the picture or in the caption. Generate it in the same job and the same run as the table, so they cannot disagree. Run the same sanity checks as for the workbook, because a picture goes further than a file does (avoiding misleading axes in Excel charts). Keep the data behind it: if someone asks how a number was produced, the table or CSV that fed the chart should be findable from the image's filename and date.

When an image is the wrong deliverable

An image is the wrong deliverable when the reader will want to hover, filter or copy values; give them the workbook or an interactive page. It is wrong when accessibility matters and there is no text alternative, since a screen reader sees nothing. It is wrong for data that changes by the minute, where a static picture starts lying quickly. And it is wrong when the picture is the only record: keep the data. See the matplotlib documentation on saving figures for the format and resolution options, and the LibreOffice command line parameters for headless conversion.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.