Adding grouping ticks to a bar chart

Viewed 87

I have a chart created from a pandas DataFrame that looks like this:

bar chart with 3 hourly ticks

I've formatted the ticks with:

ax = df.plot(kind='bar')
ax.set_xticklabels(df.index.strftime('%I %p'))

However, I'd like to add a second set of larger ticks, to achieve this kind of effect: enter image description here

I've tried many variations of use set_major_locator and set_major_formatter (as well as combining major and minor formatter), but it seems I'm not approaching it correctly and I wasn't able to find useful examples of similar combined ticks online either.

Does someone have a suggestion on how to achieve something similar to the bottom image?

The dataframe has a datetime index and is binned data, from something like df.resample(bin_size, label='right', closed='right').sum())

1 Answers

One idea is to set major ticks to display the date (%-d-%b) at noon each day with some padding (e.g., pad=40). This will leave a minor tick gap at noon, so for consistency you could set minor ticks only on the odd hours and give them rotation=90.

Note that this uses matplotlib's bar() since pandas' plot.bar() doesn't play well with the date formatting.

import matplotlib.dates as mdates

# toy data
dates = pd.date_range('2021-08-07', '2021-08-10', freq='1H')
df = pd.DataFrame({'date': dates, 'value': np.random.randint(10, size=len(dates))}).set_index('date')

# pyplot bar instead of pandas bar
fig, ax = plt.subplots(figsize=(14, 4))
ax.bar(df.index, df.value, width=0.02)

# put day labels at noon
ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[12]))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%-d-%b'))
ax.xaxis.set_tick_params(which='major', pad=40)

# put hour labels on odd hours
ax.xaxis.set_minor_locator(mdates.HourLocator(byhour=range(1, 25, 2)))
ax.xaxis.set_minor_formatter(mdates.DateFormatter('%-I %p'))
ax.xaxis.set_tick_params(which='minor', pad=0, rotation=90)

# add day separators at every midnight tick
ticks = df[df.index.strftime('%H:%M:%S') == '00:00:00'].index
arrowprops = dict(width=2, headwidth=1, headlength=1, shrink=0.02)
for tick in ticks:
    xy = (mdates.date2num(tick), 0) # convert date index to float coordinate
    xytext = (0, -65)               # draw downward 65 points
    ax.annotate('', xy=xy, xytext=xytext, textcoords='offset points',
                annotation_clip=False, arrowprops=arrowprops)

bar plot with major day ticks, minor hour ticks, and day separators

Related