Which chart for which question
In short: The kind of chart is chosen by the question: sorted bars for a comparison, a line for change over time, and bars again, not a pie, for a share of the whole.
Problem
Four questions, one set of data
The manager has four questions about the year:
- Which products bring in the most money?
- How does the revenue go day after day?
- What part of the revenue is each category?
- What is a usual order?
The data is the same: the orders and their lines. The chart, though, is different for each question. There is no one chart that answers them all. There is a chart that answers one question well, and many that answer it badly.
The idea
The question first, then the chart
Do not start from “I want a pie” or “I want a line”. Start from the question and see what kind it is:
- comparison (“which is more”): bars sorted by value; with long names horizontal (
ax.barh); - change over time (“how is it going”): a line;
- share of a whole (“what part”): sorted bars again, in percent; a pie is harder to read;
- distribution (“what is usual, how much does it spread”): a histogram, in the next lesson;
- relationship (“do they grow together”): a scatter plot, also in the next lesson.
Let us see the first three with the same data.
The idea
Comparison: sorted horizontal bars
“Which products bring in the most?” is a comparison of 25 products. Their names are long, so the bars are horizontal: ax.barh(names, values). The names sit on the left and can be read without rotating them.
Sorting with sort_values() makes the chart a ranking. barh draws the first row at the bottom, so ascending order puts the biggest at the top. figsize=(7, 7) gives the figure a size in inches: 25 bars need height.
White cheese brings in the most: €10,325.30. After it come coffee beans (€9,585.10) and kashkaval cheese (€8,616.50). Sorted, the bars answer the question before you read the numbers.
More precisely, if you are curious
Without sort_values() the products come in alphabetical order from groupby. Alphabetical order is handy when you look for one product by name. For the question “which is the most” you need order by value.
Try it
The best sellers in units
Which 10 products sold the most units in the year? Store them in top (the product is the index, the units are the value) and draw them as horizontal bars with the biggest at the top.
Solution
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
products = pd.read_csv('products.csv')
# Revenue does not include the cancelled orders.
sold = orders[orders['status'] != 'cancelled']
lines = items.merge(sold, left_on='order_id', right_on='id')
lines['revenue'] = lines['quantity'] * lines['unit_price']
lines['month'] = lines['ordered_at'].str[:7]
lines['product'] = lines['product_id'].map(products.set_index('id')['name'])
top = lines.groupby('product')['quantity'].sum().nlargest(10).sort_values()
fig, ax = plt.subplots()
ax.barh(top.index, top.values)
ax.set_title('The best-selling products')
ax.set_xlabel('Units in the year')
fig.tight_layout()lines.groupby('product')['quantity'].sum().nlargest(10).sort_values() gives the ten. At the top is white bread with 4,223 units, then fresh milk (3,882) and mineral water (3,707). White cheese, which brings in the most money, is only tenth in units (1,307). “Best seller” depends on whether you count units or euros. nlargest puts the biggest first, and without sort_values() it ends up at the bottom.
The idea
Change over time: a line
“How does the revenue go day after day?” is change over time. A line joins every day to the next one, and the eye follows the direction. 365 bars would be a fence in which nothing can be seen.
The daily revenue jumps because of the weekly rhythm. A second line with the 7-day average (rolling(7) from the lesson on dates) shows the direction under the noise:
There are 365 days. The strongest is 26 December 2025 with €852.90. The thin line zigzags every week, and the thick one shows the big peak in December and the dips in January and September. Along the bottom are dates: ordered_at has been turned into dates with pd.to_datetime, and matplotlib puts in the months by itself.
The idea
Small charts side by side
In the previous lesson the seven categories tangled in one axes. Another way is to put each category in its own small chart and set the charts side by side. These are called small multiples.
plt.subplots(2, 3) gives a figure with 2 rows and 3 columns of axes. axes is a NumPy array of shape (2, 3), and axes.flat hands them out one by one. sharey=True gives them all the same vertical scale. fig.suptitle is a title for the whole figure. Along the bottom there is only the number of the month (pv.index.str[5:]), so it fits:
With a shared scale the heights can be compared: dairy is highest all year, and kitchen is low with one peak in December. Each line reads on its own, with no legend and no colours to tell them apart.
The idea
Share of a whole: bars against a pie
“What part of the revenue is each category?” is a question about shares. It is often drawn as a pie chart. Here are the pie and the sorted bars with the same numbers, side by side. plt.subplots(1, 2) gives two axes: here they are called left and right:
The pie shows that dairy is the biggest slice, a little over a quarter. Cleaning (9.8%) and kitchen (10.2%), though, look the same. Bread (11.9%) is close to them too. The bars on the right put them in order, and the differences show.
The idea
Why a pie misleads
Bars are compared by length from the same line. The eye does that well. A pie makes the eye compare the angles and areas of slices that are turned in different directions. The eye does that badly, especially when the shares are close.
A pie has other problems too:
- with more than 4–5 slices the labels pile up;
- two pies side by side (December against the year) can hardly be compared;
- colour is the only thing that links a slice to its name.
A pie does its job in one case: two or three slices and the question “more or less than half?”. For everything else sorted bars say the same thing more clearly.
The idea
Distribution and relationship
Two kinds of question are left. They need charts that come in the next lesson:
- distribution: “what is a usual order?” The answer is not one number but how 10,459 orders spread: how many are small and how many are large. A histogram shows that;
- relationship: “are orders with more lines more expensive?” Every order becomes a point with two numbers. The cloud of points is a scatter plot.
Exercise
What December brings
December is the strongest month. Does it have more orders, or bigger ones? Store in orders_n the number of orders in every month (without the cancelled ones) and in avg the average order in euros, and draw them as two lines in two axes side by side, each with a title.
Solution
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
products = pd.read_csv('products.csv')
# Revenue does not include the cancelled orders.
sold = orders[orders['status'] != 'cancelled']
lines = items.merge(sold, left_on='order_id', right_on='id')
lines['revenue'] = lines['quantity'] * lines['unit_price']
lines['month'] = lines['ordered_at'].str[:7]
lines['product'] = lines['product_id'].map(products.set_index('id')['name'])
orders_n = sold.groupby(sold['ordered_at'].str[:7]).size()
revenue = lines.groupby('month')['revenue'].sum()
avg = revenue / orders_n
fig, (left, right) = plt.subplots(1, 2, figsize=(10, 4))
left.plot(orders_n.index, orders_n.values)
left.set_title('Orders per month')
right.plot(avg.index, avg.values)
right.set_title('Average order, €')
left.tick_params(axis='x', labelrotation=90)
right.tick_params(axis='x', labelrotation=90)
fig.tight_layout()orders_n gives 1,211 orders in December against 701 to 961 in the other months. avg = revenue / orders_n gives an average order of €13.14 in December, and between €9.93 and €10.49 in the other months. December has both more orders and bigger ones. In one axes the average order (about 10) would lie on zero under the orders (hundreds). lines.groupby('month')['revenue'].mean() is the average line, not the average order.
What part of the revenue in December 2025 is each category, with the gift card? Store the share in percent in share, sorted, and draw it as horizontal bars with the biggest at the top.
Solution
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
products = pd.read_csv('products.csv')
# Revenue does not include the cancelled orders.
sold = orders[orders['status'] != 'cancelled']
lines = items.merge(sold, left_on='order_id', right_on='id')
lines['revenue'] = lines['quantity'] * lines['unit_price']
lines['month'] = lines['ordered_at'].str[:7]
lines['product'] = lines['product_id'].map(products.set_index('id')['name'])
categories = pd.read_csv('categories.csv')
cat_of = products.set_index('id')['category_id']
cat_name = categories.set_index('id')['name']
lines['category'] = lines['product_id'].map(cat_of).map(cat_name).fillna('Gift card')
dec = lines[lines['month'] == '2025-12']
share = (dec.groupby('category')['revenue'].sum() / dec['revenue'].sum() * 100).sort_values()
fig, ax = plt.subplots()
ax.barh(share.index, share.values)
ax.set_title('The share of each category in December')
ax.set_xlabel('Share of revenue, %')
fig.tight_layout()The filter lines['month'] == '2025-12' keeps December. The sum by category, divided by the sum of the month and times 100, gives the shares. Dairy is at the top with 22.4%, but kitchen is close with 20.4% (for the whole year it is 10.2%). The gift card is 13.6% of December, and 3.3% of the year. With a pie these differences would be guesswork. Without the filter you would get the shares for the whole year.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Joining: merge · Wide and long: pivot_table and melt · Dates and time series · The first chart
- Continues in: Distributions · Honest charts · Why did revenue fall?
- Tasks on this topic: Charts