3

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)

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
vestland
  • 55,229
  • 37
  • 187
  • 305
  • 1
    Just concerning your last edit; no, there is no "theme". The reason is that there is no standard artist that could be styled in an appropriate way to achieve the desired alternating shading, but rather you need to create the spans manually. – ImportanceOfBeingErnest Feb 12 '19 at 17:07

2 Answers2

4

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

9769953
  • 10,344
  • 3
  • 26
  • 37
  • 1
    This will often work, but not always, because pandas changes the units on the axes depending on the input date frequency. You can use `ax = ts.plot(x_compat=True)` to *guarantee* this solution works, but the ticklabels will look different in that case. – ImportanceOfBeingErnest Feb 12 '19 at 15:07
  • @9769953 Thank you for answering! Any Idea on how to get all the way with tje alternating black ang grey colors between the lines? – vestland Feb 12 '19 at 15:15
  • @ImportanceOfBeingErnest Do you know if theres any way to read from the plot itself where the vertical lines have been drawn? – vestland Feb 12 '19 at 15:30
4

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

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Thank you! This is great! For some reason I had an issue with disappearing gridlines when I had the chesterish theme activated. The shaded area was there, but no gridlines. Thankfully, a simple `plt.grid(which='Major')` took care of that too. I added a screenshot in the question if anyones interested in the final product with the chesterish theme activated. – vestland Feb 13 '19 at 07:45
  • I hope you don't mind a little follow-up question: Your example starts with a shaded area. How would you change your example to start with a non-shaded area? I tried myself, but quickly made a mess out of it all. – vestland Feb 13 '19 at 11:16
  • 1
    I updated the answer with an argument `first` added. If `True` the first interval is shaded, if `False` it is not. – ImportanceOfBeingErnest Feb 13 '19 at 14:02