How can I add an alternating background color when I have dates on the x-axis?

Question:

I’ve recently started using the dark chesterish theme from dunovank, and I
love how good a simple pandas.DataFrame.plot() looks like out of the box:

Snippet 1:

# Theme from dunovank, exclude if not installed:
from jupyterthemes import jtplot
jtplot.style()

# snippet from pandas docs:
ts = pd.Series(np.random.randn(1000),index=pd.date_range('1/1/2000', periods=1000)).cumsum()
ax = ts.plot()

Output 1:

enter image description here

But I’d like to add an alternating background color (seems to be all the rage with big news agencies). The post How can I set the background color on specific areas of a pyplot figure? gives a good description of how you can do it. And it’s really easy for numeric x-values:

Snippet 2:

# imports
import pandas as pd
import numpy as np
from jupyterthemes import jtplot

# Sample data
np.random.seed(123)
rows = 50
dfx = pd.DataFrame(np.random.randint(90,110,size=(rows, 1)), columns=['Variable Y'])
dfy = pd.DataFrame(np.random.randint(25,68,size=(rows, 1)), columns=['Variable X'])
df = pd.concat([dfx,dfy], axis = 1)
jtplot.style()

ax = df.plot()
for i in range(0, 60, 20):       
            ax.axvspan(i, i+10, facecolor='lightgrey', alpha=0.025)

Output 2:

enter image description here

But it gets a lot messier (for me at least) when the x-axis is of a time or date format. And that’s because the axis in my two examples goes from this

# in:
ax.lines[0].get_data()

# out:
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
        17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
        34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49],
       dtype=int64)

To this (abbreviated):

# in:
ts.plot().lines[0].get_data()

# out:
.
.
Period('2002-09-15', 'D'), Period('2002-09-16', 'D'),
Period('2002-09-17', 'D'), Period('2002-09-18', 'D'),
Period('2002-09-19', 'D'), Period('2002-09-20', 'D'),
Period('2002-09-21', 'D'), Period('2002-09-22', 'D'),
Period('2002-09-23', 'D'), Period('2002-09-24', 'D'),
Period('2002-09-25', 'D'), Period('2002-09-26', 'D')], dtype=object)  

ts.plot().lines[0].get_data() returns the data on the x-axis. But is there a way to find out where matplotlib renders the vertical lines for each ‘Jan’ observation, so I can more easily find decent intervals for the alternating black and grey background color?

enter image description here

Thank you for any suggestions!


Edit – Or is there a theme?

Or does anyone know if there exists a theme somewhere that is free to use?
I’ve checked all matplotlib themes import matplotlib.pyplot as plt; print(plt.style.available) and Seaborn, but with no success.


Edit 2 – Suggested solution from ImportanceOfBeingErnest with the chesterish theme activated:

enter image description here

In my humble opinion, this is a perfect setup for a time series chart (could maybe drop the splines though)

Asked By: vestland

||

Answers:

Use an axis vertical span with datetime values for the x-values:

from jupyterthemes import jtplot
import pandas as pd
import numpy as np
from datetime import datetime

jtplot.style()
ts = pd.Series(np.random.randn(1000),index=pd.date_range('1/1/2000', periods=1000)).cumsum()
ax = ts.plot()

# or an appropriate for-loop
ax.axvspan(datetime(1999, 12, 15), datetime(2000, 1, 15), facecolor='red', alpha=0.25)
ax.axvspan(datetime(2000, 12, 15), datetime(2001, 1, 15), facecolor='red', alpha=0.25)

timeseries graph with shaded vertical areas

Answered By: 9769953

Gridlines are by default shown at the positions of the major ticks. You can get those ticks via ax.get_xticks(). The problem will be that it is not guaranteed that the edges of the plot coincide with those ticks, in fact they are most often dissimilar. So in order to have a consistent shading over the range of the axes, the first shade should start at the edge of the plot and end at the first gridline, then the following shades can go in between gridlines, up to the last, which will again be between the last gridline and the edge of the axes.

Another problem is that the limits of the plot and hence the automatically generated gridlines may change over the lifetime of the plot, e.g. because you decide to have different limits or zoom or pan the plot. So ideally one would recreate the shading each time the axis limits change. This is what the following does:

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

# time series
ts = pd.Series(np.random.randn(1000),index=pd.date_range('1/1/2000', periods=1000)).cumsum()
# numeric series
#ts = pd.Series(np.random.randn(1000),index=np.linspace(25,800,1000)).cumsum()
ax = ts.plot(x_compat=True)

ax.grid()

class GridShader():
    def __init__(self, ax, first=True, **kwargs):
        self.spans = []
        self.sf = first
        self.ax = ax
        self.kw = kwargs
        self.ax.autoscale(False, axis="x")
        self.cid = self.ax.callbacks.connect('xlim_changed', self.shade)
        self.shade()
    def clear(self):
        for span in self.spans:
            try:
                span.remove()
            except:
                pass
    def shade(self, evt=None):
        self.clear()
        xticks = self.ax.get_xticks()
        xlim = self.ax.get_xlim()
        xticks = xticks[(xticks > xlim[0]) & (xticks < xlim[-1])]
        locs = np.concatenate(([[xlim[0]], xticks, [xlim[-1]]]))

        start = locs[1-int(self.sf)::2]  
        end = locs[2-int(self.sf)::2]

        for s, e in zip(start, end):
            self.spans.append(self.ax.axvspan(s, e, zorder=0, **self.kw))

gs = GridShader(ax, facecolor="lightgrey", first=False, alpha=0.7)

plt.show()

enter image description here

Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.