DEV Community

The Practical Developer
The Practical Developer

Posted on Originally published at practicaldevblog.blogspot.com

Matplotlib: Charts in Python Without Opening a Spreadsheet

The numbers are there, but the pattern is not

You have last week's sales in a CSV file: notebooks 42, pens 87, backpacks 23, mugs 61, desk lamps 35. You can read every number and still not answer the question your manager asked: which category is carrying the week? Rows of numbers hide shape. A bar chart shows it in one glance, pens towering over everything, backpacks barely registering. That gap is the whole reason to plot from inside Python. If your numbers already live in a script or a CSV you load with Python, opening a spreadsheet just to draw a chart is an extra round trip, and every round trip is a chance to paste the wrong column.

What I would check before using it

My judgement, after using it for quick reporting charts: Matplotlib is the right default when your data is already in Python and the chart ends up in a report, a slide deck, or a document. It is free, it has been around since 2003, so nearly every problem you hit has an answer somewhere, and it saves to PNG, PDF, and SVG, which covers almost every place a chart needs to go. The pyplot interface is also genuinely quick to learn for the basics: a bar chart is a handful of lines, and you can build on that slowly instead of learning a whole framework first.

The trade-offs are real, though. Default charts look plain, and making one look polished takes deliberate styling work. It is also a static plotting library at heart, so if you need a chart people can hover over, zoom into, or filter in a browser, you are fighting the tool. And the API has two styles, the quick pyplot functions and the more explicit figure-and-axes objects, which confuses beginners because tutorials mix them without warning.

So my verdict is: use it for static charts in Python reports and analysis, and learn the figure-and-axes style from the start even though it is a few more lines. The recommendation flips in two cases. If the audience needs an interactive dashboard, reach for Plotly or a dashboard tool instead. And if the data lives in a spreadsheet and nobody on the team writes Python, making the chart in the spreadsheet is faster and more honest than dragging the data into code just to feel clever. The tool should follow the data, not the other way round.

A week of shop sales, plotted two ways

Here is a realistic case: a small online stationery shop wants to see one week of October sales. The data is small enough to type out, which keeps the example honest, and big enough that a chart earns its keep. First, units sold per category, as a bar chart. Then daily revenue for the same week, as a line chart, so the shape of the week shows up: the slow Tuesday, the climb into the weekend, the Saturday peak.

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

# Weekly unit sales for a small online stationery shop, one week in October.
categories = ["Notebooks", "Pens", "Backpacks", "Mugs", "Desk lamps"]
units = [42, 87, 23, 61, 35]

total_units = sum(units)
best = categories[units.index(max(units))]
print("Total units sold:", total_units)
print("Best seller:", best, "with", max(units), "units")
print("Mean units per category:", round(total_units / len(units), 1))

fig, ax = plt.subplots(figsize=(8, 5))
ax.bar(categories, units)
ax.set_title("Units sold by category, one week in October")
ax.set_ylabel("Units sold")
ax.set_xticks(range(len(categories)))
ax.set_xticklabels(categories)
fig.tight_layout()
fig.savefig("sales_by_category.png")
plt.close(fig)

days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
revenue = [1240, 980, 1560, 1430, 1890, 2210, 1750]
total_revenue = sum(revenue)
print("Total revenue: $", total_revenue)
print("Mean daily revenue: $", round(total_revenue / len(days), 2))
print("Best day:", days[revenue.index(max(revenue))], "with $", max(revenue))

fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(days, revenue, marker="o")
ax.set_title("Daily revenue, one week in October")
ax.set_ylabel("Revenue ($)")
fig.tight_layout()
fig.savefig("daily_revenue.png")
plt.close(fig)
Enter fullscreen mode Exit fullscreen mode

The first line deserves attention, because it is the line most tutorials skip. matplotlib.use("Agg") selects a backend that draws to files instead of opening a window. On a server, or in any script run without a screen, the default backend has nowhere to draw, so you set Agg before importing pyplot and save with fig.savefig() instead of calling plt.show(). Running the script prints real numbers, not guesses:

Total units sold: 248
Best seller: Pens with 87 units
Mean units per category: 49.6
Total revenue: $ 11060
Mean daily revenue: $ 1580.0
Best day: Sat with $ 2210
Enter fullscreen mode Exit fullscreen mode

Those numbers are the story the charts tell. Pens sold 87 units out of 248, about 35 percent of everything, while backpacks managed only 23. That is not a rounding difference; it is a stocking decision waiting to happen. Revenue tells the second half: Tuesday dipped to $980, the lowest day, then the week climbed steadily to a $2,210 Saturday, nearly double the Tuesday figure, before easing to $1,750 on Sunday. The mean day brought in $1,580, so Saturday sat about $630 above average and Tuesday about $600 below it. If the shop owner only looked at the $11,060 weekly total, none of that would be visible.

One thing went wrong while building this, and it is the kind of mistake that looks trivial once you see it. My first version of the bar chart passed only four labels for five categories, leaving out "Desk lamps":

ax.set_xticklabels(["Notebooks", "Pens", "Backpacks", "Mugs"])  # only 4 labels
Enter fullscreen mode Exit fullscreen mode

Matplotlib stopped the script with this error:

ValueError: The number of FixedLocator locations (5), usually from a call to set_ticks, does not match the number of ticklabels (4).
Enter fullscreen mode Exit fullscreen mode

The fix is to keep one list as the source of truth and reuse it: set the ticks from range(len(categories)) and pass the full categories list as the labels, which is what the final script does. Whenever a label list and a data list are typed separately, they will drift apart sooner or later; deriving one from the other removes the whole class of mistake.

Common mistakes and how to fix them

The label mismatch above is the first one: a ValueError about FixedLocator locations not matching ticklabels. It means a tick label list is shorter or longer than the data. Fix it by building labels from the same list as the data, and count both lists before plotting if you are typing them by hand.

The second mistake is calling plt.show() and expecting a picture file. Under the Agg backend, show() does nothing visible and saves nothing; I verified this by running a script that only called show(), and no PNG file appeared afterwards. The fix is fig.savefig("chart.png"), and to check the file exists before moving on.

The third is mismatched x and y lengths in a line plot, which raises ValueError: x and y must have same first dimension, but have shapes (3,) and (2,). It usually means a day is missing from one list. The fix is to print len(days) and len(revenue) side by side, find the short list, and correct the data rather than trimming the longer one to match.

My verdict: start with one chart from your own numbers

Matplotlib earns its place by removing the gap between your data and your chart. You do not need to learn the whole library; you need one bar chart and one line chart that run from your own numbers, saved to files you can actually open. Take a CSV you already care about, a week of expenses or a month of signups, and plot it twice: totals as bars, the trend as a line. When the labels line up, the files appear, and the numbers in the printout match the heights in the chart, you have the whole workflow. Everything after that is decoration on a foundation that already works.

Further reading

Originally published at https://practicaldevblog.blogspot.com/2026/11/matplotlib-charts-in-python-without.html

Top comments (0)