Skip to content

Note

Click here to download the full example code

Per-Day Business Hours

Each weekday can be configured to have different bushours. To do so, pass a bushours dict where each key is a weekday name and the value is a tuple of (start_hour, end_hour). On the business axis, days are scaled proportionally to their total bushours length — i.e. days with less bushours will appear narrower.

Boundaries accept numeric hours, ISO time strings, or datetime.time objects — (9, 17), ("09:00", "17:00"), and (datetime.time(9), datetime.time(17)) are all equivalent.

Core code:

ax.set_xscale("busday", bushours={
    "Mon": (9, 17),
    "Tue": (9, 17),
    "Wed": (9, 12),
    "Thu": (9, 22),
    "Fri": (9, 12),
})

Per-Day Sessions: Mon/Tue 9–17, Wed/Fri 9–12, Thu 9–22, Calendar Time (scale='linear'), Business Time (scale='busday', bushours={per-day})

import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

import busdayaxis

busdayaxis.register_scale()

OPEN = 9

# define dummy data
bushours = {
    "Mon": (OPEN, 17),
    "Tue": (OPEN, 17),
    "Wed": (OPEN, 12),
    "Thu": (OPEN, 22),
    "Fri": (OPEN, 12),
}
_day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
close_per_wd = np.array([bushours.get(name, (0, 0))[1] for name in _day_names])
num_days = 5
dates = pd.date_range("2025-01-06", periods=num_days * 24, freq="h")  # Mon–Fri
close_hours = close_per_wd[dates.weekday]
returns = np.random.normal(0, 0.002, len(dates))
returns[~np.is_busday(np.array(dates, dtype="datetime64[D]"))] = 0.0
returns[(dates.hour <= OPEN) | (dates.hour > close_hours)] = 0.0
prices = (1 + pd.Series(returns, index=dates)).cumprod()


fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 7), sharey=True)
fig.suptitle("Per-Day Sessions: Mon/Tue 9–17, Wed/Fri 9–12, Thu 9–22", fontsize=13)

full_days = pd.date_range(dates.min().normalize(), dates.max().normalize(), freq="D")

# axis with default linear scale
ax1.plot(dates, prices.values, linewidth=1.3)
ax1.set_title("Calendar Time (scale='linear')")
ax1.set_ylabel("Price")
ax1.xaxis.set_major_locator(mdates.HourLocator())
ax1.xaxis.set_major_formatter(mdates.DateFormatter("%a %Hh"))
ax1.tick_params(axis="x", rotation=90)


# axis with business scale
ax2.plot(dates, prices.values, linewidth=1.3)
ax2.set_xscale("busday", bushours=bushours)
ax2.xaxis.set_major_locator(busdayaxis.HourLocator())
ax2.xaxis.set_major_formatter(mdates.DateFormatter("%a %Hh"))
ax2.set_title("Business Time (scale='busday', bushours={per-day})")
ax2.set_ylabel("Price")
ax2.tick_params(axis="x", rotation=90)

# Shade inactive periods per day
for d in full_days:
    close = close_per_wd[d.weekday()]
    ax1.axvspan(d, d + pd.Timedelta(hours=OPEN), color="grey", alpha=0.15, linewidth=0)
    ax1.axvspan(
        d + pd.Timedelta(hours=close),
        d + pd.Timedelta(hours=24),
        color="grey",
        alpha=0.15,
        linewidth=0,
    )
# Mark open/close boundaries
for d in full_days:
    close = close_per_wd[d.weekday()]
    ax2.axvline(d + pd.Timedelta(hours=OPEN), linestyle="--", linewidth=0.8, alpha=0.6)
    ax2.axvline(d + pd.Timedelta(hours=close), linestyle="--", linewidth=0.8, alpha=0.6)

_ = plt.tight_layout(rect=[0, 0, 1, 0.96])

Total running time of the script: ( 0 minutes 0.600 seconds)

Download Python source code: plot_3_per_day_bushours.py

Download Jupyter notebook: plot_3_per_day_bushours.ipynb

Gallery generated by mkdocs-gallery