Telling a Story with Charts

Data Science · Lesson 24 of 32 7 min read Updated

The charts you make while exploring are for you: quick, rough and many. The charts you show to a shop owner, a principal or a manager are different. They must make one point so clearly that a busy person gets it in five seconds, and they must be honest.

In this lesson you will take a plain default chart and turn it, step by step, into an explanatory chart that tells a story: a title that states the finding, sorted and highlighted bars, annotations, less clutter and sensible colours.

Exploratory vs explanatory charts

ExploratoryExplanatory
AudienceYouSomeone who must understand or decide
NumberDozensOne to three
TitleDescribes the data ("Revenue by month")States the finding ("Revenue doubled in the festive season")
ColourDefaultsGrey for context, one colour for the point
LabelsWhatever matplotlib givesOnly what supports the message

Before you draw an explanatory chart, write the message as one sentence. If you cannot, you are not ready to present the chart yet.

Step 1: the default chart

Here is the question: "In which months should the shop plan extra stock and staff?" Start with a default bar chart of monthly revenue.

PythonRun it yourself
import pandas as pd
import matplotlib.pyplot as plt

sales = pd.read_csv("sales.csv", parse_dates=["order_date"]).drop_duplicates()
sales["revenue"] = sales["quantity"] * sales["unit_price"] * (1 - sales["discount"] / 100)
monthly = sales.groupby(sales["order_date"].dt.month)["revenue"].sum() / 100000

plt.bar(monthly.index, monthly.values)
plt.title("Revenue by month")
plt.show()

print(monthly.round(2))
print("Oct + Nov share of the year:", round((monthly[10] + monthly[11]) / monthly.sum() * 100, 1), "%")
Output
order_date
1     1.13
2     1.17
3     1.48
4     1.14
5     1.10
6     0.95
7     1.68
8     1.61
9     1.93
10    2.81
11    2.75
12    1.32
Name: revenue, dtype: float64
Oct + Nov share of the year: 29.1 %

The chart has twelve blue bars of similar height, with October and November clearly taller. It is correct, but the reader must do all the work: find the tall bars, work out the months from numbers 1 to 12, guess the unit, and decide what it means. The printout gives the key fact: October and November together bring in about 29% of the whole year's revenue.

Step 2: the message in the title, highlight in colour

Make the finding the title, label the axis with its unit, use month names, and colour only the bars that matter. Everything else becomes grey context.

PythonRun it yourself
import pandas as pd
import matplotlib.pyplot as plt

sales = pd.read_csv("sales.csv", parse_dates=["order_date"]).drop_duplicates()
sales["revenue"] = sales["quantity"] * sales["unit_price"] * (1 - sales["discount"] / 100)
monthly = sales.groupby(sales["order_date"].dt.month)["revenue"].sum() / 100000

names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
colours = ["#c2410c" if m in (10, 11) else "#cbd5e1" for m in monthly.index]

fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(names, monthly.values, color=colours)
ax.set_title("October and November bring in 29% of the year's revenue", loc="left", fontsize=13)
ax.set_ylabel("Revenue (₹ lakh)")
for side in ["top", "right"]:
    ax.spines[side].set_visible(False)
plt.show()

Now only two bars are orange; the other ten are light grey. The eye goes straight to October and November, and the left-aligned title tells the reader what to notice before they even look at the bars. Nothing about the data changed; only the design did.

Step 3: annotate the key point

An annotation is text placed on the chart, often with an arrow, that explains a specific point. Use one or two, not ten. ax.annotate() takes the text, the point to mark (xy) and the text position (xytext).

PythonRun it yourself
import pandas as pd
import matplotlib.pyplot as plt

sales = pd.read_csv("sales.csv", parse_dates=["order_date"]).drop_duplicates()
sales["revenue"] = sales["quantity"] * sales["unit_price"] * (1 - sales["discount"] / 100)
daily = sales.groupby("order_date")["revenue"].sum()
smooth = daily.rolling(14, center=True, min_periods=7).mean() / 1000

peak_day = smooth.idxmax()
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(smooth.index, smooth.values, color="#c2410c", linewidth=2)
ax.axvspan(pd.Timestamp("2025-10-01"), pd.Timestamp("2025-11-15"), color="#c2410c", alpha=0.08)
ax.annotate("Diwali, 20 Oct", xy=(pd.Timestamp("2025-10-20"), smooth.loc["2025-10-20"]),
            xytext=(pd.Timestamp("2025-06-15"), smooth.max() * 0.95),
            arrowprops={"arrowstyle": "->", "color": "gray"})
ax.set_title("Daily revenue climbs through the festive season (14-day average)", loc="left")
ax.set_ylabel("₹ thousand per day")
for side in ["top", "right"]:
    ax.spines[side].set_visible(False)
plt.show()

print("Peak of the 14-day average:", peak_day.date(), round(smooth.max(), 1), "thousand per day")
Output
Peak of the 14-day average: 2025-11-05 12.8 thousand per day

The line runs low and fairly flat for most of the year, rises in September and reaches its highest level in the shaded festive band from October to mid-November, then falls away in December. A grey arrow points from the label "Diwali, 20 Oct" to the line. The shaded band (axvspan) marks the season, and the annotation connects the pattern to a cause the reader already knows. Diwali fell on 20 October in 2025; check such dates for the year you are analysing.

Step 4: honesty checks

A beautiful chart can still mislead. Before sharing, run through these checks:

  • Bar charts start at zero. Cutting the axis at, say, ₹9 lakh makes a 10% difference look like 300%.
  • Compare like with like. Totals favour big groups. Compare Pune with Delhi using revenue per order or per customer if their sizes differ.
  • Show uncertainty where it matters. A difference between two small groups may be luck. The statistics lessons, especially Hypothesis Testing Basics, show how to check.
  • Do not cherry-pick the time window. Starting a line chart at the lowest month exaggerates growth.
  • One y-axis per chart. Two scales on one chart let you make any two lines appear to move together.
PythonRun it yourself
import matplotlib.pyplot as plt

cities = ["Pune", "Delhi"]
revenue = [9.6, 10.4]       # ₹ lakh

fig, axes = plt.subplots(1, 2, figsize=(9, 3.5), layout="constrained")
axes[0].bar(cities, revenue, color="#94a3b8")
axes[0].set_ylim(9.5, 10.5)
axes[0].set_title("Misleading: axis starts at 9.5")
axes[1].bar(cities, revenue, color="#94a3b8")
axes[1].set_ylim(0, 11)
axes[1].set_title("Honest: axis starts at 0")
plt.show()

print(f"Real difference: {(revenue[1] / revenue[0] - 1) * 100:.1f}%")
Output
Real difference: 8.3%

On the left, Delhi's bar looks about nine times taller than Pune's, because the axis starts at 9.5. On the right, with the axis from zero, the bars look almost the same, which matches the real difference of about 8%.

Accessible colour choices

About 1 in 12 men worldwide has some form of colour vision deficiency, most often difficulty telling red from green. Design for them:

  • Do not rely on red versus green alone to mean bad versus good. Add labels, icons or position.
  • Use one highlight colour against grey, as in step 2. It works for almost everyone.
  • For several categories, use a colour-blind-friendly set such as matplotlib's tableau-colorblind10 style, and keep each category's colour the same across all charts in a report.
  • For values from low to high, use one hue from light to dark ("Blues", "viridis"), never a rainbow.
  • Make text dark enough to read and large enough for slides (at least 11 to 12 points).

Common mistakes

  • Showing every exploratory chart. Choose the one to three charts that support the decision.
  • Descriptive titles. "Revenue by month" describes; "Revenue doubled in October" informs.
  • Rainbow colours for no reason. Colour should mean something: the highlight, a category, or a value scale.
  • Labelling every point. Label the one or two that matter; the axis handles the rest.

Key points

  • Decide the message first and write it as the chart title.
  • Use grey for context and one strong colour for the point you want noticed.
  • Annotate only the key moments with ax.annotate() and shaded spans such as ax.axvspan().
  • Keep charts honest: zero-based bars, fair comparisons, no cherry-picked windows, one y-axis.
  • Choose colours that work for colour-blind readers and never rely on red versus green alone.

Exercise

Your message is: "Kochi gets more rain than any other city." Using weather.csv, compute the total 2025 rainfall per city, and draw a sorted horizontal bar chart where Kochi's bar is highlighted and the others are grey. Put the message in the title, add the unit to the axis label, and write each bar's total at the end of the bar with ax.bar_label().

Show solution
Python
import pandas as pd
import matplotlib.pyplot as plt

weather = pd.read_csv("weather.csv")
rain = weather.groupby("city")["rainfall_mm"].sum().sort_values()
colours = ["#0369a1" if c == "Kochi" else "#cbd5e1" for c in rain.index]

fig, ax = plt.subplots(figsize=(7, 3.5))
bars = ax.barh(rain.index, rain.values, color=colours)
ax.bar_label(bars, fmt="%.0f mm", padding=3)
ax.set_title("Kochi gets more rain than any other city", loc="left")
ax.set_xlabel("Total rainfall in 2025 (mm)")
for side in ["top", "right"]:
    ax.spines[side].set_visible(False)
plt.show()

print(rain.round(0))
Output
city
Jaipur      436.0
Delhi      1046.0
Chennai    1320.0
Mumbai     1766.0
Kochi      2956.0
Name: rainfall_mm, dtype: float64

Sorting puts Kochi's long blue bar at the top, and bar_label() writes the exact totals, so readers do not need to read the axis at all.

Quick Quiz

1. What makes a good explanatory chart title?

2. Why use grey for most bars and one strong colour for one bar?

3. A bar chart's y-axis starts at 9.5 instead of 0. What is the problem?

4. Which colour scheme suits values from low to high, such as rainfall amounts?

Learn Data Science with a mentor

Free lessons get you started. Our Data Science & Machine Learning training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call