Time-series charts & date axes
Sales, website traffic, sensor readings and stock prices all share one thing: an x-axis made of dates. If you plot dates as plain strings, Matplotlib treats them as categories, spacing is wrong and labels overlap. Plot real datetime values and let the date machinery work for you.
Start with a proper DatetimeIndex
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
rng = np.random.default_rng(7)
days = pd.date_range("2025-01-01", "2025-12-31", freq="D")
orders = pd.Series(200 + rng.normal(0, 25, len(days)).cumsum() / 5 + 40 * np.sin(np.arange(len(days)) / 58),
index=days, name="orders")
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(orders.index, orders, lw=0.8, color="0.6", label="Daily")
ax.plot(orders.index, orders.rolling(7).mean(), lw=2, label="7-day average")
ax.legend(frameon=False)
The rolling average removes day-of-week noise so the trend becomes visible. Keep the raw series faint in the background so readers still see the variation.
Locators and formatters
A locator decides where ticks go; a formatter decides their text.
ax.xaxis.set_major_locator(mdates.MonthLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter("%b"))
ax.xaxis.set_minor_locator(mdates.WeekdayLocator(byweekday=mdates.MO))
| Tool | Use it for |
|---|---|
MonthLocator(bymonth=[1, 4, 7, 10]) |
Quarter starts |
WeekdayLocator(byweekday=mdates.MO) |
Weekly ticks on Mondays |
DateFormatter("%d %b") |
Labels like "15 Aug" |
ConciseDateFormatter(locator) |
Automatic, compact multi-level labels |
fig.autofmt_xdate() |
Rotates and aligns crowded date labels |
ConciseDateFormatter is the best default: it shows the year once and months or days beneath it, removing repetition.
loc = mdates.AutoDateLocator()
ax.xaxis.set_major_locator(loc)
ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(loc))
Resampling before plotting
Daily data over five years is 1,800 points - too noisy for a board slide. Aggregate first with pandas:
monthly = orders.resample("MS").sum() # month-start totals
fig, ax = plt.subplots()
ax.bar(monthly.index, monthly.values, width=20)
Note that bar width on a date axis is measured in days.
Handling gaps and events
- Missing days: if a shop was closed, leave
NaNin the series - Matplotlib breaks the line there instead of drawing a misleading straight segment. - Events: mark GST rate changes, lockdowns or campaign launches with
ax.axvline(pd.Timestamp("2025-10-20"), ls=":"). - Financial years: Indian FY runs April to March, so set
xlimfrom 1 April and considerbymonth=[4, 7, 10, 1]for quarter ticks.
Common mistakes
- Plotting a column of strings like "01-04-2025" instead of parsing with
pd.to_datetime(..., dayfirst=True). - Unsorted dates, which produce zig-zag lines; call
sort_index()first. - Comparing this year with last year on one date axis - instead plot both against day-of-year or month number.
- Too many tick labels; let a locator choose sensible spacing.
Practice
Generate two years of daily footfall for a Hyderabad mall. Plot the daily values faintly with a 30-day
rolling mean on top, use ConciseDateFormatter, mark 1 April of each year as the start of the financial
year with a vertical line, and then make a second chart comparing monthly totals of both years side by side.