Lesson 14 of 16 · Level 4: Expert

Python Charts with matplotlib: Bar Charts, Line Charts and a Report Dashboard

  • ⏱ 13 min read
  • ❓ 4 review questions
  • ✍️ Exercise with solution
  • 📥 Downloadable template
In this lesson
  1. Which chart do you need?
  2. Setup and your first chart
  3. Preparing the data with pandas
  4. Bar charts: comparing categories
  5. Plotting straight from pandas
  6. A dashboard with several charts
  7. Charts that don't mislead
  8. Real case: does heat sell ice cream?
  9. Common matplotlib errors
  10. Summary
What you'll learn
  • Create bar charts, line charts and stacked bar charts with matplotlib.
  • Plot straight from pandas with .plot().
  • Build a dashboard of several charts and save it as an image for a report.
  • Choose the right chart and avoid the mistakes that mislead readers.
  • Use a scatter plot to uncover relationships in a small business's data.
📥 Practice this lesson

The examples use the coffee roaster's sales from lessons 11 and 13; the exercise uses the expense tracker data from lesson 8.

A table of numbers answers questions for anyone with time to read it. A good chart answers them in two seconds. In this lesson you'll turn the sales analysis from lessons 11 and 13 into a visual dashboard with matplotlib, Python's most widely used charting library and the foundation many others are built on.

2 sis how long it should take someone to get a good chart's message
150 dpior more when saving, so the chart looks sharp in a report
1message per chart, stated in the title: "February sales fell 35%" beats "Sales"

Which chart do you need?

Before writing code, decide which question you're answering. Almost every business chart falls into one of these four:

Line · ax.plot

How does it change over time? Sales by month, a daily exchange rate.

Bar · ax.barh

Which is biggest? Sales by region, product or rep, sorted.

Stacked bar

What makes up the total? Each month's sales broken down by product.

Scatter · ax.scatter

Is one thing related to another? Temperature and sales, price and units.

Setup and your first chart

pip install matplotlib pandas

(Activate your virtual environment first, as in lesson 10.) The basic matplotlib pattern never changes: create a figure with one or more "axes" (each axes is one chart), draw, then save or show it.

import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar"]
sales = [3887.5, 2525.0, 3765.0]

fig, ax = plt.subplots(figsize=(7, 4))   # size in inches
ax.plot(months, sales, marker="o")
ax.set_title("Sales by month")
ax.set_ylabel("Revenue ($)")
fig.savefig("sales_by_month.png", dpi=150)   # save the image
plt.show()                                   # open a window (Jupyter shows it automatically)

Always work with fig and ax (the "object-oriented interface"). You'll see code online that calls plt.plot() directly: it works for quick charts but gets messy as soon as you want more than one.

Preparing the data with pandas

We start from the sales file. As you know from lesson 11, one row has no region; it belongs to Maria, who covers the Midwest:

import pandas as pd

df = pd.read_csv("sales.csv", parse_dates=["date"])
df["region"] = df["region"].fillna("Midwest")
df["revenue"] = df["units"] * df["price"]

by_region = df.groupby("region")["revenue"].sum().sort_values()
print(by_region)
region
East       2707.5
Midwest    2870.0
West       4600.0
Name: revenue, dtype: float64

Bar charts: comparing categories

fig, ax = plt.subplots(figsize=(7, 4))
ax.barh(by_region.index, by_region.values, color="#2f6fed")
ax.set_title("West leads Q1 2026 sales")
ax.set_xlabel("Revenue ($)")

for i, v in enumerate(by_region.values):          # label each bar with its value
    ax.text(v, i, f" ${v:,.0f}", va="center")

ax.spines[["top", "right"]].set_visible(False)     # remove unnecessary borders
fig.tight_layout()
fig.savefig("sales_by_region.png", dpi=150)

Three decisions make this chart easier to read: horizontal bars (long names fit without rotating them), bars sorted from smallest to largest (with sort_values(), the biggest ends up on top) and the value written next to each bar, so nobody has to estimate it from the axis.

1. You want to show how sales changed month to month. Which chart do you use?

  • A pie chart
  • A sorted horizontal bar chart
  • A line chart

Lines show trends over time. Bars compare categories, and pie charts only work for a few parts of a whole.

Plotting straight from pandas

DataFrames and Series have a .plot() method that uses matplotlib under the hood. It's ideal for pivot tables:

months = {1: "Jan", 2: "Feb", 3: "Mar"}

by_product = df.pivot_table(index=df["date"].dt.month, columns="product",
                            values="revenue", aggfunc="sum", fill_value=0)
by_product.index = by_product.index.map(months)
print(by_product)

ax = by_product.plot(kind="bar", stacked=True, rot=0, figsize=(7, 4))
ax.set_title("Sales by product and month")
product  Chocolate  Coffee     Tea
date
Jan            0.0  3080.0   807.5
Feb          640.0   840.0  1045.0
Mar          880.0  2030.0   855.0

kind accepts "line", "bar", "barh", "hist", "box", "scatter" and more. The dictionary gives us short, predictable month labels that don't depend on your computer's language settings.

A dashboard with several charts

With plt.subplots(1, 3) you get three axes in a row and fill them one by one:

months = {1: "Jan", 2: "Feb", 3: "Mar"}
by_month = df.groupby(df["date"].dt.month)["revenue"].sum()
by_month.index = by_month.index.map(months)

fig, axs = plt.subplots(1, 3, figsize=(15, 4.5))

axs[0].plot(by_month.index, by_month.values, marker="o", linewidth=2)
axs[0].set_title("Sales by month")
axs[0].set_ylim(bottom=0)

by_product.plot(kind="bar", stacked=True, ax=axs[1], rot=0)
axs[1].set_title("Sales by product")
axs[1].set_xlabel("")
axs[1].legend(title="", frameon=False)

axs[2].barh(by_region.index, by_region.values, color="#2f6fed")
axs[2].set_title("Sales by region")

for ax in axs:
    ax.spines[["top", "right"]].set_visible(False)

fig.suptitle("Sales dashboard · Q1 2026", fontsize=14, fontweight="bold")
fig.tight_layout()
fig.savefig("sales_dashboard.png", dpi=150)

The result:

Sales dashboard with three charts: sales by month as a line, sales by product as stacked bars, and sales by region as horizontal bars

The two-second read: February was the weak month (mostly because coffee dropped from $3,080 to $840), chocolate starts selling in February, and the West alone brings in 45.2% of the quarter's $10,177.50. That's exactly what a good dashboard should deliver.

Saving for a report

Use dpi=150 or more for documents and slides, format="svg" for a vector chart that stays sharp at any size, and bbox_inches="tight" if a title gets cut off. Combine it with lesson 12 and you can generate this dashboard every Monday and email it without touching anything.

Charts that don't mislead

Bars with the axis starting at 2,000.

Bars start at zero: otherwise a 5% difference looks like 200%.

Generic titles like "Sales."

One message per chart, stated in the title: "February sales fell 35%."

Pie charts with 6 or 8 slices.

Beyond 3 or 4 parts, bars are far easier to compare.

A different color for every bar "because it looks nice."

Few colors, used on purpose: one neutral color for everything and one strong color for what you want to highlight.

3D effects.

No 3D: it distorts proportions and adds no information.

2. In a bar chart, the y-axis starts at 2,500, and the February bar ($2,525) looks about 50 times shorter than March ($3,765). What do you fix?

  • Nothing; it makes the difference easier to see.
  • Start the axis at zero, for example with ax.set_ylim(bottom=0).
  • Switch to a 3D chart.

In a bar chart, length represents the value. If the axis doesn't start at zero, the proportions lie: here March is really about 1.5 times February, not 50 times.

3. What does fig, axs = plt.subplots(2, 2) give you?

  • One figure with 4 charts in a 2 × 2 grid, reached as axs[row, col].
  • Two separate figures.
  • A chart twice as big.

subplots(rows, columns) returns the figure and a grid of axes. With a single row, (1, 3), you use one index: axs[0], axs[1]…

Real case: does heat sell ice cream?

The owner of an ice cream shop in Myrtle Beach, South Carolina, suspects she sells more on hot days and wants to decide how much mix to prepare based on the forecast. For 10 days she wrote down the high temperature and the cones sold. To see whether two numeric variables are related, the right chart is a scatter plot: one dot per day.

import matplotlib.pyplot as plt
import pandas as pd

# Ice cream shop in Myrtle Beach, SC: high temperature and cones sold over 10 days
data = pd.DataFrame({
    "temperature": [76, 79, 80, 82, 84, 86, 88, 90, 92, 94],
    "cones":       [118, 135, 150, 149, 170, 182, 190, 214, 220, 241],
})
r = data["temperature"].corr(data["cones"])
hot = data[data["temperature"] > 86]["cones"].mean()
mild = data[data["temperature"] <= 86]["cones"].mean()
print(f"Correlation: {r:.2f}")
print(f"Average above 86°F: {hot:.0f} cones")
print(f"Average at 86°F or below: {mild:.0f} cones")

fig, ax = plt.subplots(figsize=(6, 4))
ax.scatter(data["temperature"], data["cones"], color="#2459d6", s=60)
ax.set_title(f"Hotter days, more ice cream (correlation {r:.2f})")
ax.set_xlabel("High temperature (°F)")
ax.set_ylabel("Cones sold")
ax.set_ylim(bottom=0)
ax.spines[["top", "right"]].set_visible(False)
fig.tight_layout()
fig.savefig("heat_ice_cream.png", dpi=150)
Correlation: 0.99
Average above 86°F: 216 cones
Average at 86°F or below: 151 cones
High temperature versus cones sold Ten dots rising from left to right: from 118 cones at 76 degrees Fahrenheit to 241 cones at 94 degrees. Correlation of 0.99. 0125250 768594 °F
  • One day of sales 10 daysEach dot: temperature (x-axis) and cones sold (y-axis).
  • Best day 241At 94°F, the hottest day.
The dots form almost a straight rising line, which is why the correlation is 0.99 (1 would be a perfect relationship).

Correlation runs from −1 to 1: near 1, the two variables rise together; near 0, there's no linear relationship. The practical takeaway is clear: days above 86°F sell an average of 216 cones versus 151, about 65 more. But careful: correlation isn't causation. If those hot days were also weekends or the Fourth of July, part of the jump could come from that. In lesson 16 you'll see how a model learns relationships like this to make predictions.

4. You want to see whether a product's price is related to the units you sell each week. Which chart do you use?

  • A pie chart with each week's price
  • Stacked bars by week
  • A scatter plot, with price on one axis and units on the other

A scatter plot shows the relationship between two numeric variables: each dot is one week. If the dots fall from left to right, higher prices mean fewer sales.

Common matplotlib errors

The most frequent error appears when two lists aren't the same length, for example three months and four values:

ValueError: x and y must have same first dimension, but have shapes (3,) and (4,)

The "shapes" (3,) and (4,) are the lengths of each list: a month is missing or there's an extra value. Other common stumbles:

Calling plt.savefig() after plt.show(): the image comes out blank.

Once the window closes, the figure is no longer "current." Save first, or use fig.savefig().

Creating charts in a loop (one per store) and seeing More than 20 figures have been opened.

Close each figure with plt.close(fig) after saving it.

Overlapping labels on the x-axis.

Rotate them with ax.tick_params(axis="x", rotation=45) or switch to horizontal bars.

Practice exercise: your expense dashboard

Using expenses.csv (columns date, category, description, amount) from lesson 8's project:

  1. Calculate total spending by category and show it as sorted horizontal bars, with the value on each bar.
  2. Calculate spending by month and show it as a line with the axis starting at zero.
  3. Put both charts in a 1 × 2 figure and save it as expense_dashboard.png.
See solution
import matplotlib.pyplot as plt
import pandas as pd

e = pd.read_csv("expenses.csv", parse_dates=["date"])
by_cat = e.groupby("category")["amount"].sum().sort_values()
by_month = e.groupby(e["date"].dt.to_period("M"))["amount"].sum()
by_month.index = by_month.index.astype(str)       # "2026-09", "2026-10"…

fig, axs = plt.subplots(1, 2, figsize=(12, 4))

axs[0].barh(by_cat.index, by_cat.values, color="#2f6fed")
for i, v in enumerate(by_cat.values):
    axs[0].text(v, i, f" ${v:,.0f}", va="center")
axs[0].set_title("Food is 42% of spending")

axs[1].plot(by_month.index, by_month.values, marker="o")
axs[1].set_ylim(bottom=0)
axs[1].set_title("Spending by month")

for ax in axs:
    ax.spines[["top", "right"]].set_visible(False)

fig.tight_layout()
fig.savefig("expense_dashboard.png", dpi=150)

The bars read transportation $291, entertainment $312, housing $404 and food $739; the line rises from $795.05 in September to $952.03 in October. If your dates span many months and the labels overlap, add axs[1].tick_params(axis="x", rotation=45).

Summary

  • The basic pattern: fig, ax = plt.subplots(), draw on ax, then fig.savefig().
  • Lines for time, sorted bars to compare categories, stacked bars for composition, scatter plots for relationships.
  • pandas plots directly with .plot(kind=...), and you can combine both styles by passing ax=.
  • A dashboard is one figure with several axes: plt.subplots(rows, columns).
  • Bars start at zero, one message per chart, few colors and no 3D.

Next lesson: Web Scraping with Python: requests and BeautifulSoup Step by Step. You'll collect data that doesn't come in any file by extracting it from web pages, responsibly.

🎓 Save your progress and get a free certificate

Create your account in 30 seconds, mark your lessons and download your certificate at the end.

Enroll for free