Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Analysis with Python (pandas, polars, DuckDB)  Module 10 – Visualising data with matplotlib

Choosing the right chart for the question

Pick the chart from the question: sorted bars to compare, lines for trends, histograms for spread, scatter plots for relationships, honest axes.

  • Beginner
  • 30 minutes
  • Examples run with Python 3.14.8, pandas 3.0.6 and matplotlib 3.11.2
  • By MySmartCoPilot
Free lesson, Pro answers: 30 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Match comparison, trend, distribution, relationship and composition questions to chart types
  • Explain why bar charts start at zero and when line charts need not
  • Avoid many-slice pies, dual axes and 3D effects
  • Write a chart title that states the finding

Before you start

What you’ll learn · Before you start

What you will learn

  • Match comparison, trend, distribution, relationship and composition questions to chart types
  • Explain why bar charts start at zero and when line charts need not
  • Avoid many-slice pies, dual axes and 3D effects
  • Write a chart title that states the finding

Before you start

On this page

Twelve branches reported 3,166 thousand in revenue. A sorted bar chart puts Pune and Surat first; an alphabetical export makes the reader hunt for them. Choose the chart from the question it must answer. Bars compare categories, lines show change over time, histograms and box plots show distributions, an ECDF gives cumulative shares, scatter plots preserve pairs of measurements, and stacked bars show parts of a whole. A pie can serve a simple whole with two or three parts.

Then make the reading honest: bars start at zero because their lengths encode values, and the title states a chart finding: A statement supported by the plotted data that identifies the comparison or result a reader should notice. supported by the data. This lesson draws the same invented branch table several ways and measures how sorting, scales and labels change what the reader sees.

The question determines which comparison a chart makes

Five kinds of question and their charts: compare, trend, distribution, relationship and parts of a whole.What should the reader see?Pick the chart from the questionThen say the finding in the title"Pune and Surat bring in 31% of revenue"Which is bigger? Compare categories→ bar chart, sorted by value (length from zero)How did it change? A trend over time→ line chart (position and slope)How are the values spread? A distribution→ histogram, box plot or ECDFDo two numbers move together? A relationship→ scatter plot (position in two directions)What share is each part? Composition→ stacked bar; a pie only for 2 or 3 parts

From the question to the chart

Text description of the diagram

Start from what the reader should see, then pick the chart that shows it:

  • Which is bigger? Comparing categories calls for a bar chart sorted by value. The eye compares lengths, so the bars start at zero.
  • How did it change? A trend over time calls for a line chart. The eye follows position and slope.
  • How are the values spread? A distribution calls for a histogram, a box plot or an ECDF.
  • Do two numbers move together? A relationship calls for a scatter plot, which places each item by both numbers.
  • What share is each part? Composition calls for a stacked bar, and a pie only when there are two or three parts.

Then write the finding as the title, for example "Pune and Surat bring in 31% of revenue".

Each chart works because it hands the reader one easy comparison. The table says which one, and what goes wrong most often:

The question The chart The eye compares What goes wrong
Which is bigger? Bar chart, sorted by value Lengths from a chart baseline: The reference from which a bar length is measured; a common zero baseline makes ordinary value-bar lengths proportional to their values. An axis that does not start at zero; alphabetical order
How did it change? Line chart Position and slope Uneven time steps; too many lines at once
How are the values spread? Histogram, box plot or ECDF Shape, quartiles, the share below a value Bins that hide the shape; one average instead of the spread
Do two numbers move together? Scatter plot Position along both axes Thousands of points hiding each other; reading cause into it
What share is each part? Stacked bar; a pie for 2 or 3 parts Lengths; angles Many slices; parts that do not add up to a whole

Three charts describe a spread. A histogram groups numeric values into bins. This lesson uses equal-width bins, whose counts show where values bunch up; explicitly supplied bin edges can also have unequal widths. A box plot shrinks a group to a box from its first to its third quartile, a line at the median and whiskers towards the extremes, which is compact enough to compare a dozen groups in one row. An ECDF, the empirical cumulative distribution function, is a rising curve that gives the fraction of the data at or below any value, with no bins to choose. The distributions lesson of this module builds all three.

Step through the branch question before choosing any software option.

Interactive · stage by stage Question and chart on the lesson data Step through the branch question before choosing any software option.
Question and chart on the lesson data, step 3 of 3. Showing Ask the question, Choose the mark, Keep the meaning. In focus: Keep the meaning.Ask the questionWhich branch leads?Choose the markLength from zeroKeep the meaningOne value per branch

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Ask the question. The quarterly revenue table has twelve branches. The question is a ranking of categories.
  2. Choose the mark. Bars place all twelve values on a common baseline; the reader compares their lengths.
  3. Keep the meaning. Time lines and paired scatter points would answer different questions. Keep the category and revenue together.

The same table can support another chart when the question changes; the data shape alone does not choose it.

Key idea

Choose the mark from the comparison the reader needs: a category, a time sequence, a distribution, a paired relationship or a whole.

Checkpoint

Check: Question and chart

  1. Question 1 of 2 What should decide the chart type first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The comparison the reader needs

    The question determines which marks convey the answer.

  2. Question 2 of 2 One column holds a flat's area and another its rent. What must the chart preserve?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Each flat's area-rent pair

    A scatter point carries both values for the same listing.

Sorting twelve bars makes the ranking the reading order

A chain of 12 branches exported its quarterly revenue, in thousands, in alphabetical order. The numbers are invented for this lesson and stay fixed:

branches.csv: revenue of 12 branches, in thousands CSV · branches.csv
branch,revenue
Bhopal,198
Coimbatore,173
Guwahati,151
Indore,236
Jaipur,315
Kochi,251
Lucknow,279
Mysuru,162
Nagpur,224
Pune,520
Surat,470
Vadodara,187

Every chart in the lesson uses the same small style file, which matplotlib reads with plt.style.use: dark text, grey axes, a light grid and one blue for the bars that carry the message.

house.mplstyle: one look for every chart MPLSTYLE · house.mplstyle
# house.mplstyle: one look for every chart in this lesson.
# Use it with plt.style.use("house.mplstyle") from the folder that holds this file.

# Size and text
figure.figsize: 5.0, 3.3
figure.dpi: 100
font.size: 13
axes.titlesize: 13.5
axes.titleweight: bold
axes.titlelocation: left
axes.titlepad: 10

# Ink: dark text, grey axes, a light grid that stays in the background
text.color: "#1c2027"
axes.labelcolor: "#1c2027"
axes.edgecolor: "#6b7280"
xtick.color: "#4b5563"
ytick.color: "#4b5563"
grid.color: "#d6dbe1"
axes.spines.top: False
axes.spines.right: False
axes.axisbelow: True

# Colours: one blue for what matters, greys for the rest
axes.prop_cycle: cycler("color", ["1d5fcb", "6b7280", "1c2027", "4b5563"])
patch.facecolor: "#6b7280"
lines.linewidth: 2
legend.frameon: False

# Saved files: the same bytes on every run, text kept as text
savefig.bbox: tight
savefig.pad_inches: 0.15
svg.fonttype: none
svg.hashsalt: house

The question is “which branches bring in the most?”, so the chart is a bar chart sorted by value, with the two leaders in blue. Horizontal bars leave room for the branch names, and ax.bar_label prints each value at the end of its bar:

Sorted bars that answer the question Python · sorted_bars.py
import matplotlib.pyplot as plt
import pandas as pd

plt.style.use("house.mplstyle")

# Quarterly revenue of 12 branches, in thousands (invented numbers), in the order the export lists them.
revenue = pd.read_csv("branches.csv", index_col="branch")["revenue"]

ranked = revenue.sort_values()          # smallest first: barh draws the first bar at the bottom
top_two = ranked.index[-2:][::-1]
share = ranked.iloc[-2:].sum() / ranked.sum()

fig, ax = plt.subplots(figsize=(5.0, 4.6))
colours = ["#1d5fcb" if branch in top_two else "#6b7280" for branch in ranked.index]
bars = ax.barh(ranked.index, ranked.to_numpy(), color=colours)
ax.bar_label(bars, padding=3)
ax.set_xlim(0, 600)
ax.set_xlabel("Revenue this quarter (thousands)")
ax.set_title(f"{top_two[0]} and {top_two[1]} bring in {share:.0%} of revenue")
fig.savefig("sorted-bars.svg", metadata={"Date": None})

print("bars from the top:", ", ".join(ranked.index[::-1]))
print("value axis starts at:", ax.get_xlim()[0])
print(f"title: {ax.get_title(loc='left')!r} ({ranked.iloc[-2:].sum()} of {ranked.sum():,})")

Output

bars from the top: Pune, Surat, Jaipur, Lucknow, Kochi, Indore, Nagpur, Bhopal, Vadodara, Coimbatore, Mysuru, Guwahati
value axis starts at: 0.0
title: 'Pune and Surat bring in 31% of revenue' (990 of 3,166)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sorted_bars.py

Horizontal bars sorted by revenue: Pune 520 and Surat 470 lead, highlighted, and Guwahati is last with 151.0100200300400500600Revenue this quarter (thousands)GuwahatiMysuruCoimbatoreVadodaraBhopalNagpurIndoreKochiLucknowJaipurSuratPune151162173187198224236251279315470520Pune and Surat bring in 31% of revenue

The chart sorted_bars.py draws

Text description of the diagram

A horizontal bar chart of quarterly revenue for 12 branches, in thousands, sorted from largest at the top to smallest at the bottom, with each value printed at the end of its bar. The value axis runs from 0 to 600.

The two largest branches are drawn in blue and the rest in grey: Pune 520, Surat 470, then Jaipur 315, Lucknow 279, Kochi 251, Indore 236, Nagpur 224, Bhopal 198, Vadodara 187, Coimbatore 173, Mysuru 162 and Guwahati 151.

The title states the finding: Pune and Surat bring in 31 % of revenue (990 of 3,166).

The title is computed from the data, so it stays true when the numbers change next quarter. Now the same 12 numbers drawn the ways that export tools and slide decks often draw them, with what each version actually tells a reader:

What four versions of the chart tell a reader Python · five_ways.py
import pandas as pd

revenue = pd.read_csv("branches.csv", index_col="branch")["revenue"]
total = revenue.sum()
largest, smallest = revenue.idxmax(), revenue.idxmin()

# 1. Bars sorted by value: the ranking is the reading order.
ranked = revenue.sort_values(ascending=False)
print("sorted bars:", ", ".join(ranked.index[:4]), "... down to", smallest)

# 2. Bars in the export's alphabetical order: where do the two largest sit?
order = list(revenue.index)
places = [order.index(b) + 1 for b in ranked.index[:2]]
print(f"export order: the two largest are bars {places[0]} and {places[1]} of {len(order)}")

# 3. Bars on an axis that starts at 140 instead of 0: compare drawn lengths with the data.
floor = 140
drawn = (revenue[largest] - floor) / (revenue[smallest] - floor)
true = revenue[largest] / revenue[smallest]
print(f"axis from {floor}: {largest}'s bar is {drawn:.1f} times {smallest}'s; the data say {true:.1f} times")

# 4. A 12-slice pie: each slice's angle, and the closest pair of neighbouring slices.
angles = (revenue / total * 360).sort_values(ascending=False)
gaps = angles.diff(-1).dropna()
a, b = gaps.idxmin(), angles.index[angles.index.get_loc(gaps.idxmin()) + 1]
middle = angles.iloc[4:9]
print(f"12-slice pie: slices 5 to 9 span {middle.min():.1f} to {middle.max():.1f} degrees")
print(f"closest neighbours: {a} {angles[a]:.2f} and {b} {angles[b]:.2f} degrees, {gaps.min():.2f} apart")

Output

sorted bars: Pune, Surat, Jaipur, Lucknow ... down to Guwahati
export order: the two largest are bars 10 and 11 of 12
axis from 140: Pune's bar is 34.5 times Guwahati's; the data say 3.4 times
12-slice pie: slices 5 to 9 span 21.3 to 28.5 degrees
closest neighbours: Mysuru 18.42 and Guwahati 17.17 degrees, 1.25 apart

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 five_ways.py

  • Sorted bars. The ranking is the reading order: Pune, Surat, Jaipur and so on down to Guwahati.
  • Alphabetical bars. The two largest branches are bars 10 and 11 of 12, so the reader has to scan all twelve bars to find the leaders.
  • An axis from 140. Pune’s bar is drawn 34.5 times as long as Guwahati’s, while Pune’s revenue is 3.4 times Guwahati’s: the chart exaggerates the gap tenfold.
  • A 12-slice pie. Five of the slices lie between 21.3 and 28.5 degrees, and the two closest neighbours differ by 1.25 degrees. Nobody ranks those by eye, and twelve labels crowd round the edge.
  • 3D bars. Depth adds no data. Perspective makes the nearer bars look larger, the bars in front hide the ones behind, and the top of a 3D bar cannot be read against the axis. The UK Government Analysis Function’s chart guidance lists 3D shapes among the effects to leave out.

Follow the same twelve values from the export order into the sorted chart.

Interactive · stage by stage Ranking and reading order on the lesson data Follow the same twelve values from the export order into the sorted chart.
Ranking and reading order on the lesson data, step 3 of 3. Showing Read twelve values, Sort by revenue, State the share. In focus: State the share.Read twelve valuesTotal: 3,166Sort by revenuePune 520; Surat 470State the share990 / 3,166 = 31%

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Read twelve values. Keep every branch in the comparison, including Guwahati at 151 thousand.
  2. Sort by revenue. The two largest values become adjacent at the top of the horizontal chart.
  3. State the share. The title's rounded share is computed from the same table that supplies the bars.

Only the order changes. That is why sorting helps the eye find the leaders without altering the accounting.

Key idea

Sorting changes the order in which values are read, while the categories, values and total stay the same.

Checkpoint

Check: Ranking and reading order

  1. Question 1 of 2 What does sorting the branch bars by revenue change?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The reading order

    It puts the comparison order into the layout without changing any value.

  2. Question 2 of 2 Pune and Surat contribute 990 of the 3,166 total. Their share, rounded to the nearest whole percent, is what?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 31 percent

    990 divided by 3,166 is about 0.3127, which rounds to 31 percent.

A cut bar baseline changes the ratio of drawn lengths

A bar shows its value by its length, so the length has to start at zero. Cut the axis and every bar loses the same amount, which the smallest bars cannot afford:

The same three bars from 0 and from 140 Python · baselines.py
import matplotlib.pyplot as plt
import pandas as pd

plt.style.use("house.mplstyle")

revenue = pd.read_csv("branches.csv", index_col="branch")["revenue"]
three = revenue[["Pune", "Kochi", "Guwahati"]]

fig, (honest, cut) = plt.subplots(2, 1, figsize=(5.0, 5.8), layout="constrained")
for ax, floor in ((honest, 0), (cut, 140)):
    ax.bar(three.index, three.to_numpy(), color="#6b7280")
    ax.set_ylim(floor, 560)
    drawn = (three["Pune"] - floor) / (three["Guwahati"] - floor)
    ax.set_title(f"Axis from {floor}: Pune looks {drawn:.1f}x")
    ax.tick_params(axis="x", labelsize=11)
honest.set_ylabel("Revenue (thousands)")
cut.set_ylabel("Revenue (thousands)")
fig.savefig("zero-or-cut.svg", metadata={"Date": None})

for ax in (honest, cut):
    print(f"{ax.get_title(loc='left')!r}: value axis {ax.get_ylim()[0]:.0f} to {ax.get_ylim()[1]:.0f}")
print(f"the data: Pune {three['Pune']} / Guwahati {three['Guwahati']} = {three['Pune'] / three['Guwahati']:.1f}x")

Output

'Axis from 0: Pune looks 3.4x': value axis 0 to 560
'Axis from 140: Pune looks 34.5x': value axis 140 to 560
the data: Pune 520 / Guwahati 151 = 3.4x

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 baselines.py

The same three bars twice: from 0, Pune is 3.4 times Guwahati; from 140, Pune looks 34.5 times as tall.PuneKochiGuwahati0100200300400500Revenue (thousands)Axis from 0: Pune looks 3.4xPuneKochiGuwahati200300400500Revenue (thousands)Axis from 140: Pune looks 34.5x

The chart baselines.py draws

Text description of the diagram

Two bar charts of the same revenue, in thousands: Pune 520, Kochi 251 and Guwahati 151.

  • Top, value axis from 0 to 560: the bars are proportional to the values, and Pune's bar is 3.4 times Guwahati's.
  • Bottom, value axis from 140 to 560: every bar loses 140, so Guwahati's bar is only 11 long against Pune's 380, and Pune looks 34.5 times as large. Kochi, which earns two thirds more than Guwahati, looks ten times as large.

If starting at zero leaves the differences too small to see, the differences are probably not the story, or a dot plot, which marks each value with a dot instead of a bar, is the better chart.

A line shows its values by their position and the slope between them, so the reader’s eye follows the shape, not the distance from zero. A zoomed axis is acceptable when the range is where the values live and the chart says so. An on-time delivery rate that stays between 90 % and 100 % is a good example:

One trend on an axis from 0 % and from 90 % Python · trend_baselines.py
import matplotlib.pyplot as plt

plt.style.use("house.mplstyle")

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
on_time = [94.1, 94.6, 93.8, 94.9, 95.3, 92.6, 91.8, 92.4, 94.0, 94.8, 95.2, 95.6]  # % of deliveries on time

fig, (full, zoom) = plt.subplots(2, 1, figsize=(5.0, 5.8), sharex=True, layout="constrained")
for ax, (low, high) in ((full, (0, 100)), (zoom, (90, 100))):
    ax.plot(months, on_time, color="#1d5fcb", marker="o", markersize=4)
    ax.set_ylim(low, high)
    ax.set_xticks(months[::3])
    ax.grid(axis="y")
full.set_title("Axis from 0 %")
zoom.set_title("Axis from 90 %, labelled")
full.set_ylabel("On time (%)")
zoom.set_ylabel("On time (%)")
zoom.annotate("Axis starts at 90 %", xy=(0.02, 0.04), xycoords="axes fraction", fontsize=10, color="#4b5563")
fig.savefig("trend-baselines.svg", metadata={"Date": None})

drop = max(on_time[:5]) - min(on_time[5:8])
for ax in (full, zoom):
    low, high = ax.get_ylim()
    print(f"{ax.get_title(loc='left')}: the dip of {drop:.1f} points fills {drop / (high - low):.0%} of the height")

Output

Axis from 0 %: the dip of 3.5 points fills 4% of the height
Axis from 90 %, labelled: the dip of 3.5 points fills 35% of the height

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trend_baselines.py

One on-time rate drawn twice: flat on an axis from 0 %, a clear mid-year dip on a labelled axis from 90 %.020406080100On time (%)Axis from 0 %JanAprJulOct9092949698100On time (%)Axis starts at 90 %Axis from 90 %, labelled

The chart trend_baselines.py draws

Text description of the diagram

Two line charts of the share of deliveries on time in each month, from January to December: 94.1, 94.6, 93.8, 94.9, 95.3, 92.6, 91.8, 92.4, 94.0, 94.8, 95.2 and 95.6 %.

  • Top, y-axis from 0 % to 100 %: the dip from 95.3 % in May to 91.8 % in July, 3.5 points, fills 4 % of the plot's height and the line looks flat.
  • Bottom, y-axis from 90 % to 100 %, with the note "Axis starts at 90 %": the same dip fills 35 % of the height, so the mid-year trouble and the recovery to 95.6 % in December are easy to see.

On the full axis the mid-year dip of 3.5 points fills 4 % of the plot’s height, and the line looks flat. On the zoomed axis the same dip fills 35 %, which shows the trouble clearly, but also makes it look larger. So a zoomed axis needs three things: a round starting value, a label that says where the axis starts, and a sentence in the text that says it too, because a screen reader user meets the chart through that text.

Step through the subtraction that makes the cut chart misleading.

Interactive · stage by stage Baselines and magnification on the lesson data Step through the subtraction that makes the cut chart misleading.
Baselines and magnification on the lesson data, step 3 of 3. Showing Keep the data, Start at zero, Cut at 140. In focus: Cut at 140.Keep the data520 and 151Start at zeroRatio: about 3.4Cut at 140380 and 11

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Keep the data. The two revenues are unchanged whichever axis range the author chooses.
  2. Start at zero. Lengths of 520 and 151 have the same ratio as the revenues.
  3. Cut at 140. Subtracting the same baseline from both gives a drawn ratio of about 34.5 instead.

The distortion is caused by the baseline, not a change in the branch data.

Key idea

Bar lengths need a zero baseline; a zoomed line axis needs an explicit range and a text explanation.

Checkpoint

Check: Baselines and magnification

  1. Question 1 of 2 Why does an ordinary value bar start at zero?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Its length represents the value

    A common zero baseline makes bar lengths proportional to the amounts.

  2. Question 2 of 2 Pune is 520 and Guwahati 151. With a baseline of 140, what is Guwahati's drawn length?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 11 thousands

    The cut chart draws 151 minus 140, or 11; Pune is drawn as 380, exaggerating their ratio.

A crossing on two y-axes is controlled by their separate scales

Pies. A pie asks the reader to compare angles, and slices that differ by a degree or two look the same, as the 1.25-degree gap above shows. Keep pies for two or three parts that add up to a meaningful whole, such as cash and digital payments. The UK guidance is a little looser: five categories or fewer, a meaningful whole and one category that clearly dominates. For anything else, sorted bars answer the same question better.

Dual axes. A chart with two y-axes draws each line against its own scale, and someone chose both scales. Change one range and the lines move against each other, so whether and where they cross is a decision of the person who drew the chart, not a fact about the data:

Where two lines cross depends on the axis ranges Python · dual_axes.py
# Two series on two y-axes: where the lines cross depends only on the axis ranges someone picked.
orders = [40, 42, 45, 47, 50, 52, 55, 57, 60, 62, 64, 66]                    # thousands, left axis 30 to 70
basket = [310, 305, 312, 318, 322, 330, 335, 338, 344, 350, 352, 358]        # average order value, right axis


def height(values, low, high):
    """Where each value is drawn, as a share of the plot's height (0 = bottom, 1 = top)."""
    return [(v - low) / (high - low) for v in values]


left = height(orders, 30, 70)
for low, high in [(300, 360), (250, 400), (280, 340)]:
    right = height(basket, low, high)
    above = [r > l for l, r in zip(left, right)]
    switches = [m + 1 for m in range(1, 12) if above[m] != above[m - 1]]
    story = f"the lines cross in month {switches[0]}" if switches else "the lines never cross"
    leader = "basket line on top at the end" if above[-1] else "orders line on top at the end"
    print(f"right axis {low} to {high}: {story}; {leader}")

Output

right axis 300 to 360: the lines cross in month 10; basket line on top at the end
right axis 250 to 400: the lines cross in month 5; orders line on top at the end
right axis 280 to 340: the lines never cross; basket line on top at the end

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dual_axes.py

The same two series cross in month 10, in month 5 or never, depending only on the right axis. Draw two charts stacked above each other with a shared time axis instead, or divide each series by its first value and multiply by 100, so that both start at 100 and share one axis.

3D effects. A 3D chart of two-dimensional data adds a dimension that carries nothing and makes every bar harder to read. Leave it out, and leave out shadows and textures for the same reason.

Follow what changes in dual_axes.py while its input lists remain fixed.

Interactive · stage by stage Wholes and separate scales on the lesson data Follow what changes in dual_axes.py while its input lists remain fixed.
Wholes and separate scales on the lesson data, step 3 of 3. Showing Hold both series, Change a range, Share meaning. In focus: Share meaning.Hold both seriesOrders and basket valueChange a rangeMonth 10; month 5Share meaningIndex or two panels

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Hold both series. The twelve order counts and basket values stay fixed.
  2. Change a range. Different right-axis limits move the apparent crossing.
  3. Share meaning. Use indexed change on one scale, or separately labelled panels sharing time.

A crossing between unlike scales cannot establish that one quantity overtook the other.

Key idea

Use a meaningful whole for composition and a meaningful common scale for comparison.

Checkpoint

Check: Wholes and separate scales

  1. Question 1 of 2 When does a simple pie have a meaningful whole?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Its mutually exclusive parts account for the same total

    Cash and digital payments can partition all orders under a declared payment rule.

  2. Question 2 of 2 The dual-axis example moves a crossing from month 10 to month 5 without changing values. What changed?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The right axis range

    Rescaling changes where the right series is drawn against the left series.

A finding title tells the reader which comparison matters

A title that names the topic, such as “Revenue by branch”, makes every reader find the message alone. A title that states the finding, such as “Pune and Surat bring in 31% of revenue”, tells them what to look for and what to remember. The UK guidance pairs such a headline title with a subtitle that says what the data is, which place it covers and which period it shows: “Revenue of 12 branches, in thousands, all regions, first quarter”.

Keep the title as strong as the evidence and no stronger. “Orders rose after the new menu” is a finding; “The new menu raised orders” is a claim about cause that one line chart cannot support. And compute numbers in titles from the data, as sorted_bars.py does, so a chart redrawn next quarter cannot keep last quarter’s message.

Build the headline from the branch table rather than from last quarter’s copied wording.

Interactive · stage by stage Titles from evidence on the lesson data Build the headline from the branch table rather than from last quarter's copied wording.
Titles from evidence on the lesson data, step 3 of 3. Showing Find the leaders, Compute their share, Name the scope. In focus: Name the scope.Find the leadersPune and SuratCompute their share31% of revenueName the scopeQuarter; thousands

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Find the leaders. Use the same ranked values that drew the bars.
  2. Compute their share. Divide their combined revenue by all twelve branches' revenue.
  3. Name the scope. The subtitle identifies the period, units and population behind the result.

The result and the scope do different jobs: the title gives the finding; the subtitle identifies the data.

Key idea

Compute the headline number from the chart’s input and keep causal claims within the evidence.

Checkpoint

Check: Titles from evidence

  1. Question 1 of 2 What does a finding title add beyond a topic title?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The result the reader should notice

    It identifies the comparison rather than merely naming its subject.

  2. Question 2 of 2 Which statement fits one time chart after a menu change?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Orders rose after the new menu

    This describes the observed timing.

Words and mark styles carry meaning when colours cannot

Use colour for meaning, not decoration. In the sorted chart, blue marks the two branches the title talks about and grey marks the rest, so the colour repeats the message instead of competing with it.

Colour must never be the only way to tell things apart. WCAG 2.2, the web’s accessibility standard, says so in success criterion 1.4.1, and it matters to the many readers who see some colours poorly. Label bars and lines directly, and vary the line style or the marker as well as the colour when several series share a chart. Criterion 1.4.11 asks for a contrast of at least 3:1 between the parts of a chart that are needed to understand it, such as bars and lines, and their background.

When you do need several colours, start from a palette designed for colour-vision deficiencies. matplotlib ships several as style sheets, among them these two:

Colour cycles that ship with matplotlib Python · palettes.py
import matplotlib.pyplot as plt

# Two colour cycles made for readers with colour-vision deficiencies ship with matplotlib as style sheets.
for name in ["tableau-colorblind10", "petroff10"]:
    with plt.style.context(name):
        colours = plt.rcParams["axes.prop_cycle"].by_key()["color"]
    print(f"{name}: {len(colours)} colours, starting {', '.join(c.lower() for c in colours[:4])}")

# The house style of this lesson: one blue for what matters, greys for the rest.
with plt.style.context("house.mplstyle"):
    print("house.mplstyle:", ", ".join(plt.rcParams["axes.prop_cycle"].by_key()["color"]))

Output

tableau-colorblind10: 10 colours, starting #006ba4, #ff800e, #ababab, #595959
petroff10: 10 colours, starting #3f90da, #ffa90e, #bd1f01, #94a4a2
house.mplstyle: #1d5fcb, #6b7280, #1c2027, #4b5563

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 palettes.py

plt.style.use("tableau-colorblind10") or plt.style.use("petroff10") switches the default colours of every chart that follows. The petroff palettes come from a published study that chose colour sequences people like while keeping the colours apart under simulated colour-vision deficiencies and in greyscale.

An informative chart needs an equivalent text alternative (WCAG 2.2 criterion 1.1.1). For the complex charts here, provide a short identification and finding, plus a longer description with the essential values and trends. The figures in this lesson carry both; open the description under a figure to read its long version.

Bar Graph Maker Draw a quick sorted bar chart from your own numbers and check that its value axis starts at zero. Line Graph Maker Plot a trend from a column of values to see how the axis range changes the story.

Trace the branch finding through its colour, labels and text alternative.

Interactive · stage by stage Colour and alternatives on the lesson data Trace the branch finding through its colour, labels and text alternative.
Colour and alternatives on the lesson data, step 3 of 3. Showing Keep the finding, Add another cue, Supply the content. In focus: Supply the content.Keep the findingTwo named leadersAdd another cueWords and marksSupply the contentDescription and values

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Keep the finding. Pune and Surat remain named in the title and directly labelled beside their values.
  2. Add another cue. Line patterns and markers can separate series without relying on hue.
  3. Supply the content. A reader who cannot see the chart receives its essential comparisons and values in text.

The meaning survives when the colour difference does not.

Key idea

Colour can reinforce a comparison; labels, patterns and text alternatives carry it independently.

Checkpoint

Check: Colour and alternatives

  1. Question 1 of 2 What must remain when two chart colours look alike?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A non-colour cue for their meanings

    A label, dash or marker makes the distinction available without hue.

  2. Question 2 of 2 WCAG's threshold for necessary chart marks against adjacent colours is at least what contrast ratio to one?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 3 ratio

    The relevant non-text contrast threshold is 3:1; ordinary text has a separate contrast requirement.

Practice: redraw the branch export

The export tool drew the branches as vertical bars in alphabetical order, on an axis from 140, under the title “Revenue by branch”. Rewrite draw_revenue so that it answers “which branches bring in the most?” at a glance. The checks inspect the Axes your function draws on, not a picture: the kind of bars, their order, where the value axis starts, the axis label, the title and the value labels. The starter code passes none of the 6 checks:

redraw_task.py: the starter code Python · redraw_task.py
import pandas as pd


def draw_revenue(ax, revenue: pd.Series) -> None:
    """Draw the branches' revenue on `ax` so that "which branches bring in the most?" is answered at a glance.

    `revenue` holds one value per branch, in thousands, in the export's alphabetical order.
    """
    # Starter code: the chart as the export tool drew it.
    ax.bar(revenue.index, revenue.to_numpy())
    ax.set_ylim(140, 560)
    ax.set_title("Revenue by branch")
The sample checks, run against the starter code Python · check_redraw.py
import matplotlib.pyplot as plt
import pandas as pd
from matplotlib.container import BarContainer

from redraw_task import draw_revenue

revenue = pd.read_csv("branches.csv", index_col="branch")["revenue"]
fig, ax = plt.subplots()
draw_revenue(ax, revenue)

bars = [c for c in ax.containers if isinstance(c, BarContainer)]
container = bars[0] if bars else None
horizontal = container is not None and container.orientation == "horizontal"
if horizontal:
    # Bar lengths from the bottom bar to the top bar.
    lengths = [p.get_width() for p in sorted(container.patches, key=lambda p: p.get_y())]
    value_axis = ax.get_xlim()
    unit = ax.get_xlabel()
else:
    lengths = []
    value_axis = ax.get_ylim()
    unit = ax.get_ylabel()
titles = " ".join(ax.get_title(loc=where) for where in ("left", "center", "right"))
labels = [t.get_text() for t in ax.texts]

checks = {
    "horizontal bars, one per branch": horizontal and len(container.patches) == len(revenue),
    "sorted: the largest bar at the top": bool(lengths) and lengths == sorted(lengths),
    "the value axis starts at 0": value_axis[0] == 0,
    "the value axis names its unit (thousands)": "thousand" in unit.lower(),
    "the title names the largest branch": revenue.idxmax() in titles,
    "every bar shows its value": sorted(labels) == sorted(str(v) for v in revenue),
}
for name, ok in checks.items():
    print("pass" if ok else "FAIL", name)
print(f"{sum(checks.values())} of {len(checks)} checks pass")

Output

FAIL horizontal bars, one per branch
FAIL sorted: the largest bar at the top
FAIL the value axis starts at 0
FAIL the value axis names its unit (thousands)
FAIL the title names the largest branch
FAIL every bar shows its value
0 of 6 checks pass

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_redraw.py

Start with revenue.sort_values() and ax.barh, which draws the first bar at the bottom, so the largest value ends up at the top. Then ax.bar_label, ax.set_xlim(0, …), an x-axis label with the unit, and a title computed from revenue.idxmax(). Run check_redraw.py again until all 6 checks pass.

Practice

Choose marks for a fresh branch report, then check whether the baseline, scale and headline preserve its meaning.

Practice that adapts to you

Practise this lesson

18 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 18 A shop wants to see which of its nine product categories sold the most last month. Which chart answers that fastest?

    Choose one answer.

    Show the answer: Question 1 of 18

    Answer: A bar chart sorted from the largest category to the smallest

    Comparing categories is a job for bars, because the eye compares lengths from a common baseline. Sorting by value turns the ranking into the reading order. A nine-slice pie asks the reader to compare angles, a line between categories suggests an order or a trend that does not exist, and alphabetical order makes the reader hunt.

  2. Question 2 of 18 You have the number of website visitors for each of the last 90 days and want to show whether a campaign changed the trend. Which chart do you draw?

    Choose one answer.

    Show the answer: Question 2 of 18

    Answer: A line chart of visitors per day, with the campaign start marked

    Change over time is a line chart: the reader follows the position and the slope of the line from day to day, and a marked start date shows where to look. A histogram of the daily counts throws away their order in time, which is exactly what the question is about.

  3. Question 3 of 18 You have the delivery times of 4,000 orders and want to show whether most arrive within 30 minutes and how long the slowest take. Which chart fits?

    Choose one answer.

    Show the answer: Question 3 of 18

    Answer: A histogram of the delivery times

    How values are spread is a distribution question. A histogram shows where most times fall and how long the tail of slow deliveries is; an ECDF would also answer it and makes the share within 30 minutes readable directly. A single average hides both the bulk and the tail.

  4. Question 4 of 18 Do larger flats rent for more? You have the area and the monthly rent of 300 listings. Which chart shows it?

    Choose one answer.

    Show the answer: Question 4 of 18

    Answer: A scatter plot with area on the x-axis and rent on the y-axis

    Whether two numbers move together is a relationship question, and a scatter plot places every listing by both numbers at once. Two separate bar charts lose the pairing of each flat's area with its rent, which is the whole point.

  5. Question 5 of 18 You want to compare how spread out the exam scores are in six sections of a class. Which chart compares the six distributions most compactly?

    Choose one answer.

    Show the answer: Question 5 of 18

    Answer: Six box plots side by side, one per section

    A box plot shows a distribution's median, quartiles and extremes in a narrow column, so six of them fit side by side on one scale. Averages alone hide the spread, and a line between sections suggests an order that sections do not have.

  6. Question 6 of 18 Which part-to-whole question can reasonably be shown with a pie chart?

    Choose one answer.

    Show the answer: Question 6 of 18

    Answer: The share of orders paid in cash and paid digitally, which add up to all orders

    A pie works when there are only two or three parts that add up to a meaningful whole, as cash and digital payments do. Twelve slices cannot be ranked by eye, two years need a comparison chart rather than one whole, and five ordered ratings read better as bars in their natural order.

  7. Question 7 of 18 A bar chart's value axis starts at 90. Two bars show 100 and 110. How many times longer is the 110 bar drawn than the 100 bar?

    Type a number.

    Show the answer: Question 7 of 18

    Answer: 2 times

    The bars are drawn from 90, so their lengths are 110 − 90 = 20 and 100 − 90 = 10: the second bar is drawn twice as long, although 110 is only 1.1 times 100. Starting the axis at zero makes the drawn ratio equal the data's.

  8. Question 8 of 18 A slide shows orders and average order value on two y-axes, and the two lines cross in month 6. A colleague says "average order value overtook orders in month 6". What is wrong?

    Choose one answer.

    Show the answer: Question 8 of 18

    Answer: The crossing point depends only on the two axis ranges someone chose, so it carries no meaning

    With two y-axes, each line is drawn against its own scale, so moving either range moves the lines up or down against each other and shifts where they cross, or removes the crossing. Two charts stacked over a shared time axis, or both series indexed to 100 at the start, avoid the false comparison.

  9. Question 9 of 18 A line chart of a monthly on-time rate that always stays between 90 % and 100 % uses a y-axis from 90 % to 100 %. What should the chart do?

    Choose one answer.

    Show the answer: Question 9 of 18

    Answer: Make the starting value obvious on the axis and say in the text that the axis starts at 90 %

    A line is read by its position and slope, so a zoomed axis is acceptable when the range is where the values live. A zoom also makes small changes look large, so the reader has to be told: label the axis start clearly and mention it in the text. Bars must start at zero, because their length is the value.

  10. Question 10 of 18 Which title is best for the sorted chart of the 12 branches' revenue?

    Choose one answer.

    Show the answer: Question 10 of 18

    Answer: Pune and Surat bring in 31% of revenue

    A title that states the finding tells the reader what to look for and what to remember. The other titles only name the topic, so every reader has to find the message alone. The subtitle or the text then says what the data is and which period it covers.

  11. Question 11 of 18 For another report: what should decide the chart type first?

    Choose one answer.

    Show the answer: Question 11 of 18

    Answer: The comparison the reader needs

    The question determines which marks convey the answer.

  12. Question 12 of 18 For another report: what does sorting the branch bars by revenue change?

    Choose one answer.

    Show the answer: Question 12 of 18

    Answer: The reading order

    It puts the comparison order into the layout without changing any value.

  13. Question 13 of 18 An alphabetical export puts the top two branches at positions 10 and 11. After sorting largest first, what position holds the largest?

    Type a number.

    Show the answer: Question 13 of 18

    Answer: 1 position

    Descending revenue order puts the maximum at position one; it does not alter the twelve source values.

  14. Question 14 of 18 For another report: why does an ordinary value bar start at zero?

    Choose one answer.

    Show the answer: Question 14 of 18

    Answer: Its length represents the value

    A common zero baseline makes bar lengths proportional to the amounts.

  15. Question 15 of 18 For another report: when does a simple pie have a meaningful whole?

    Choose one answer.

    Show the answer: Question 15 of 18

    Answer: Its mutually exclusive parts account for the same total

    Cash and digital payments can partition all orders under a declared payment rule.

  16. Question 16 of 18 For another report: what does a finding title add beyond a topic title?

    Choose one answer.

    Show the answer: Question 16 of 18

    Answer: The result the reader should notice

    It identifies the comparison rather than merely naming its subject.

  17. Question 17 of 18 For another report: what must remain when two chart colours look alike?

    Choose one answer.

    Show the answer: Question 17 of 18

    Answer: A non-colour cue for their meanings

    A label, dash or marker makes the distinction available without hue.

  18. Question 18 of 18 Blue identifies Pune and Surat. Which addition preserves that distinction in greyscale?

    Choose one answer.

    Show the answer: Question 18 of 18

    Answer: Direct branch names and values

    The names and values identify the leaders without colour.

Interview questions

Fresher · Why should a bar chart’s value axis start at zero, when a line chart’s axis need not?

A bar shows its value by its length, measured from the baseline. If the axis starts above zero, every bar loses the same amount, so the drawn lengths stop being proportional to the values: on an axis from 90, bars for 100 and 110 are drawn 10 and 20 long, and 110 looks twice 100. A line shows its values by their position and the slopes between them, so readers compare the shape, not the distance from zero, and a zoomed axis can show a real change that would look flat from zero. A zoomed line chart still exaggerates small changes, so it should start at a round value, label where the axis starts and say so in the text. If a bar chart’s differences vanish from zero, use a dot plot or show the differences themselves.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    Leadership wants a pie chart of the company's 14 cost categories for the annual summary. What do you propose instead, and how do you deal with the smallest categories without hiding anything?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A product manager wants one chart of daily active users and daily revenue over six months, on two y-axes, to show that revenue follows usage. What do you build instead, and what do you say about the claim?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Start from the question: sorted bars compare, lines show trends, histograms, box plots and ECDFs show spread, scatter plots show relationships, and stacked bars show parts of a whole.
  • Bars start at zero because the eye compares their lengths; an axis from 140 drew Pune’s lead 10 times too large.
  • A line chart may zoom in when the range is the point, as long as the chart and the text say where the axis starts.
  • Use a pie for a simple whole of two or three parts, and avoid misleading dual scales and decorative 3D effects.
  • The title states the finding, computed from the data, and the subtitle states the data and the period.
  • Never rely on colour alone, and give every published chart a text alternative.

Terms in this lesson

chart baseline
The reference from which a bar length is measured; a common zero baseline makes ordinary value-bar lengths proportional to their values.
chart finding
A statement supported by the plotted data that identifies the comparison or result a reader should notice.

Corrections

  • Added chart-choice scenes, checkpoints and adaptive practice; clarified histogram bins and text alternatives while retaining the branch charts and redraw worksheet.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.