The first chart
In short: A chart in matplotlib is a figure with axes: plot draws a line, bar draws bars, and the title, the axis labels and the legend say what is shown.
Problem
“Show me the year”
The manager wants to see how the year is going. You already know how to work out the revenue by month: the order lines without the cancelled ones, summed by month. The month is the first 7 characters of ordered_at, for example “2026-03”:
The numbers are right, but in 12 rows it is hard to see what is a peak and what is a drop. December is €15,915.90 and January is €7,228.20. How big is the drop compared with the other months? In a table the eye does not catch it. In a line it does at once.
The idea
Figure and axes
In the lesson “Welcome” you saw a chart made with one line of code. Now you will see what stands behind it.
A chart in matplotlib has two objects:
- the figure (
Figure) is the whole sheet: the size and saving to a file; - the axes (
Axes) is the box with the horizontal and the vertical axis, where you draw.
plt.subplots() gives you both: fig, ax = plt.subplots(). Then you draw in the axes. ax.plot(x, y) joins the points with a line:
[chart] in the text of the output means that the chart has no title. The line already shows the peak in December and the dip after it. It still lacks three things: a title, labels on the axes, and months you can read.
More precisely, if you are curious
Here and in Jupyter the chart shows by itself at the end of the cell, which is why there is no plt.show(). In a script (python chart.py) you show it with plt.show() or save it with fig.savefig('monthly.png'). Otherwise the script ends and the figure goes with it.
plt.plot(...) without fig, ax draws too: matplotlib then takes the “current” axes by itself. That is handy for one quick chart, but with several axes you do not know which one is current. That is why here you always draw in named axes.
The idea
Title, labels and ticks
A chart without labels makes the reader guess. The axes have a method for each thing:
ax.set_titlesays what you are looking at;ax.set_xlabelandax.set_ylabelsay what is along the bottom and what is up the side;ax.tick_params(axis='x', labelrotation=45)rotates the tick labels, the numbers and names under the axis, so they do not overlap;fig.tight_layout()arranges the sheet so the rotated labels are not cut off.
marker='o' puts a dot on every month:
Now you can see that 11 months are between €7,000 and €10,000, and December is above €15,000. January, February and September are the lowest. Why September drops, you will find out in the project.
Try it
Orders by month
Draw the number of orders in every month (without the cancelled ones) as a line. Store the counts in counts, and give the chart a title and labels on both axes.
Solution
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv')
sold = orders[orders['status'] != 'cancelled']
counts = sold.groupby(sold['ordered_at'].str[:7]).size()
fig, ax = plt.subplots()
ax.plot(counts.index, counts.values)
ax.set_title('Orders by month')
ax.set_xlabel('Month')
ax.set_ylabel('Orders')
ax.tick_params(axis='x', labelrotation=45)
fig.tight_layout()sold.groupby(sold['ordered_at'].str[:7]).size() gives 12 numbers: 886 orders in November, 1,211 in December, 728 in January and 701 in September. The line has the same shape as the revenue: a peak in December and a dip after it. With orders instead of sold you would also count the cancelled orders. Without set_xlabel and set_ylabel the reader does not know whether the vertical axis is in units or in euros.
The idea
Bars: ax.bar
A line joins points and says “from here to here something changed”. That makes sense for months that come one after another. Categories do not come one after another: for them you draw bars (a bar chart) with ax.bar(names, values).
The revenue by category needs the category name on every line: product → category_id → name. Two map calls give it:
There are 6 bars, and they add up to €107,466.20. The revenue of the year is €111,186.20. €3,720 is missing: the gift card has no category, map gives it NaN, and groupby skips NaN silently. The chart looks fine. So count the bars and compare their sum with the revenue.
Try it
All the categories
Draw the revenue by category so that the gift card is a bar of its own. Store the revenue by category in by_cat, and add a title and a label on the vertical axis.
Solution
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.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]
products = pd.read_csv('products.csv')
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')
by_cat = lines.groupby('category')['revenue'].sum()
fig, ax = plt.subplots()
ax.bar(by_cat.index, by_cat.values)
ax.set_title('Revenue by category, €')
ax.set_ylabel('Revenue, €')
ax.tick_params(axis='x', labelrotation=30)
fig.tight_layout().fillna('Gift card') after the two map calls gives the missing category a name, and groupby keeps it. There are now 7 bars, and they add up to exactly €111,186.20. Dairy is the highest bar with €31,427.90, and the gift card is the lowest with €3,720. Without fillna the chart has 6 bars and the gift card is missing.
The idea
The shortcut: df.plot
Series and DataFrame have a plot method. It calls matplotlib for you: kind='line' (the default), kind='bar' and the other kinds that come in the next lessons. With ax=ax it draws in your axes. Then you finish them with the methods of the axes, as before:
pandas has put the category names under the bars and rotated them. It has also put a label along the bottom: the name of the index, category. The title and the vertical label come from your code.
Both ways give the same chart. ax.bar gives you full control. df.plot saves lines when the data is already in a Series or a table.
The idea
Several lines and a legend
December is the peak of the year. Do all the categories grow the same? Two lines in one axes answer that. Every ax.plot adds a line, label gives it a name, and ax.legend() adds the legend: a box that says which line is which:
Kitchen jumps from €964.80 in November to €3,240.60 in December, 3.4 times as much. Then it falls to €592.80 in January. The frying pan and the knives are probably bought as gifts. Dairy grows less: from €2,792.10 to €3,561.20.
A table with a column for every category df.plot draws in one go, a line per column, with a legend from the column names. That is why the table in the lesson on pivot_table is wide:
Seven lines in one axes already tangle. Which chart helps when, and how to show many series without tangling them, is the next lesson.
Exercise
Who carries December?
The two big groups of categories are Food and Home (parent_id in categories.csv). Draw the revenue of each by month as two lines in one axes, with a legend, a title and labels on the axes. Store the table by month and group in pv.
Solution
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.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]
products = pd.read_csv('products.csv')
categories = pd.read_csv('categories.csv')
cat_of = products.set_index('id')['category_id']
cat_name = categories.set_index('id')['name']
parent_of = categories.set_index('id')['parent_id']
lines['group'] = lines['product_id'].map(cat_of).map(parent_of).map(cat_name)
pv = lines.pivot_table(index='month', columns='group', values='revenue', aggfunc='sum')
fig, ax = plt.subplots()
ax.plot(pv.index, pv['Food'], label='Food')
ax.plot(pv.index, pv['Home'], label='Home')
ax.legend()
ax.set_title('Food and Home by month, €')
ax.set_xlabel('Month')
ax.set_ylabel('Revenue, €')
ax.tick_params(axis='x', labelrotation=45)
fig.tight_layout()The group of a line takes three map calls: product → category → parent_id → name. pivot_table by month and group gives a Food column and a Home column. In December food grows by 30% (from €7,097.40 to €9,249.30), and home more than doubles (from €2,077.10 to €4,506.60). The gift card has no group and is in neither line. Without label the legend is empty, and without ax.legend() there is no legend at all.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Types and missing values · Groups: groupby · Joining: merge · Wide and long: pivot_table and melt
- Continues in: Which chart for which question · Distributions · Honest charts · Describing the data
- Tasks on this topic: Charts