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 NaN in 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 xlim from 1 April and consider bymonth=[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.

B

Written by Bablu Kumar Jha

Founder, BeeProton AI · Mumbai, Maharashtra, India