Forcing Attention with Visualizations#

from IPython.display import display
from matplotlib.pyplot import rc
rc('font', size=18)
rc('figure', facecolor='white')
from matplotlib.pyplot import subplots, close
from numpy import hstack
from numpy.random import default_rng
from pandas import Series, date_range

rng = default_rng(2)

fig, ax = subplots()

s = Series(
    index=(index := date_range('2000', freq='7D', periods=200)),
        rng.integers(-1, 2, size=60),
        rng.integers(-1, 3, size=30),
        rng.integers(-1, 1, size=20),
        rng.integers(-1, 2, size=90),
    ]).cumsum().clip(0, None)
).rolling(3).mean() * 100_000

ax = s.plot(figsize=(12, 8), linewidth=3, ax=ax)
ax.yaxis.set_major_formatter(lambda x, pos: f'{x/1_000_000:g}M')

from IPython.display import HTML

from matplotlib.pyplot import subplots
from matplotlib.animation import FuncAnimation
from matplotlib.dates import AutoDateLocator, ConciseDateFormatter, date2num

from numpy import linspace
from pandas import to_datetime

rc('animation', embed_limit=2**25)
rc('axes.spines', top=False, right=False)

fig, ax = subplots(figsize=(14, 10), gridspec_kw={'right': .7})

artists = {
    'start':    ax.plot([], [], color='tab:blue', lw=2)[0],
    'critical': ax.plot([], [], color='tab:red', lw=3, zorder=9)[0],
    'finish':    ax.plot([], [], color='tab:blue', lw=2)[0],
    'title':    ax.set_title('', size='x-large'),
    'annot_max': ax.annotate(
        'We were making so much money here',
        (date2num(to_datetime('2001-09-01')), s.loc['2001-09-01']),
        xytext=(50, 0), textcoords='offset points', bbox={'facecolor': 'white', 'alpha': 0, 'edgecolor': 'none'},
        alpha=0, ha='left', va='center', arrowprops={'facecolor': 'black', 'shrink': .05, 'alpha': 0}
    'annot_right': ax.annotate(
        'I bet you’re looking\nover here now', (s.index[-1], s.iloc[-1]),
        alpha=0, ha='left', va='bottom'
    'highlight': ax.axvspan(
        *date2num(['2000-11-01', '2001-09-01']), ymin=0, ymax=0,
        alpha=.5, color='yellow'

def frame_gen():
    yield 'pause', 'A Story About a Line'
    for d in s.loc[:'2000-11-01'].copy().expanding():
        yield 'start', d
    for _ in range(20):
        yield 'pause', 'A Pause... For Dramatic Effect!'
    for d in s.loc['2000-11-01':'2001-09-01'].expanding():
        yield 'critical', d

    for d in s.loc['2001-09-01':].expanding():
        yield 'finish', d
    for alpha in linspace(0, 1, 20):
        yield 'annot_right', {'alpha': alpha}
    for alpha in linspace(0, 1, 20)[::-1]:
        yield 'annot_right', {'alpha': alpha}
    yield 'annot_right', {
        'text': 'Let’s go back to our roots\nto figure out what caused\nour exponential growth', 
        'va': 'top'
    for alpha in linspace(0, 1, 20):
        yield 'annot_right', {'alpha': alpha}
    for _ in range(60):
        yield 'pause', None
    yield 'annot_right', {'alpha': 0}
    for height in linspace(0, 1, 50):
        yield 'highlight', {'height': height}
    yield 'pause', 'We started using animated visualizations to expain our data!'
    yield 'annot_max', {'text': {'alpha': 1}, '_bbox_patch': {'alpha': 1}, 'arrow_patch': {'alpha': 1}}
    for _ in range(60):
        yield 'pause', None
    yield 'pause', 'Thanks for Watching This Matplotlib Animation!'

def update(args):
    match args:
        case ('pause', title):
            if title is not None:
            return [artists['title']]
        case ('start', data):
            line = artists['start']
            artists['title'].set_text('At the Beginning, We Were Barely Scraping by...')
            line.set_data(data.index, data.to_numpy())
            return [line, artists['title']]
        case ('critical', data):
            line = artists['critical']
            artists['title'].set_text('Then, WE HIT GOLD!')
            line.set_data(data.index, data.to_numpy())
            return [line, artists['title']]
        case ('finish', data):
            line = artists['finish']
            artists['title'].set_text('But the Economy Began Worsening')
            line.set_data(data.index, data.to_numpy())
            return [line, artists['title']]
        case ('annot_right', d):
            return [artists['annot_right']]
        case ('highlight', d):
            if 'height' in d:
                xy = artists['highlight'].get_xy()
                height = d.pop('height')
                xy[:, 1] = [0, height, height, 0, 0]
                d['xy'] = xy
            return [artists['highlight']]
        case ('annot_max', d):
            artists['annot_max'].set(**d.get('text', {}))
            artists['annot_max']._bbox_patch.set(**d.get('_bbox_patch', {}))
            artists['annot_max'].arrow_patch.set(**d.get('arrow_patch', {}))
            return [artists['annot_max']]
        case _:
            raise ValueError('bad match case')

ax.update_datalim([(date2num(s.index.min()), s.min()), (date2num(s.index.max()), s.max())])

locator = AutoDateLocator()
formatter = ConciseDateFormatter(
    locator, zero_formats=['', '%b\n%Y', '%b', '%b-%d', '%H:%M', '%H:%M']
animation = FuncAnimation(
    fig, update, frames=frame_gen, blit=True,
    save_count=1 + len(s)+ 20 + 20 + 1 + 20 + 20 + 1 + 60 + 20 + 1 + 50 + 60,

ax.yaxis.set_major_formatter(lambda x, pos: f'{x/1_000_000:g}M')
ax.set_ylabel('Cumulative Revenue')

html = animation.to_jshtml(default_mode='once')