pythonmatplotlibtime-seriestimeline

Timeline in Python - creating spaces between dates lines


Following by is the code and its output.

  1. Is there a way to create space between the date line to the lower event name, so that there will not be overlapping as seen in the figure.
  2. There is a space between the 'positive' events names and their horizontal tick. Is there a way to eliminate the space (and make it as 'negative' events on the ticks)?

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

df = pd.DataFrame(
    {
        'event': ['Qfpgv KFJPF fpnkmkq',
                  'Ltzx cqwup xnywi bxfjzgq ol mwwqmg ukszs',
                  'MUTD ysrfzad Urmiv lqyjexdq xqkqzfx vqtwrgh',
                  'Vxdys vjxqnqojq qvqoshjmhv dmyzf fj wvtrjv',
                  'Kcxtm-Bix Nzvlqj ajmydgbxk',
                  'Nrsbo! ukguvle xavahfg tqyikwqg, UZSP tgrlqfr',
                  'Rjxetf/uzpqwhwr qtshxvlp tljybtncbq qvqybnjgq dzqj',
                  'Qwvbt-Khspqw olfypkbvh tljmyyvz ajmy zazvqfm',
                  'UHW Umkqtqm zvhq tljybtncbq',
                  'Wwscye rukqdf, vfyvqmf udzvqmcv tljybtncbq',
                  'Twljq uqtrjxwh hyvnvwbl tljmyyvz rbykqkwqjg djzv Kqkmv xnyzqmv.',
                  'Qfpgv Qnwroj rymqzqm tljybtncbq kxqj vq Kqmnjp kxqkz.',
                  'Vwkqr jvqjg fqtwp, Jvccjvj CQM Sgqhojif mblqjc',
                  'Qxltj dqg Vqsue tljmyyvz jvtsqjwuj wkhruwqlqj, ixdro xqjolvkphw',
                  'Rwkq Vqwdqlqj odujhg jvswhuh fduuleolqj',
                  'Nzvq nqfupxqj jtsqjwuj',
                  'Vqolqjyphfwhv sohwwhuqvhtg jtsqjwuj',
                  'Ulnwj ri gihw dqg rqooih OY wlg ihghovdfh orqjv',
                  'Fkxohqjdoahoo sohwwhu ydplqhuv rqh wkhhtxdo jtsqjwuj'
                 ],
        'date': ['1984', '1987', '1991', '1994', '1997', '1998', '1999', '2002', '2004',
                 '2005', '2007', '2009', '2010', '2012', '2013', '2014', '2017', '2019', '2021']
    }
)

df['date'] = pd.to_datetime(df['date'])
df['date'] = df['date'].dt.year

levels = np.tile(
    [-5, 5, -3, 3, -1, 1, -7, 7, -4, 4, -2, 2, -6, 6, -3, 3, -1, 1, -5, 5, -3, 3, -1, 1, 5],
    int(np.ceil(len(df) / 6))
)[:len(df)]

fig, ax = plt.subplots(figsize=(12.8, 4), constrained_layout=True)

ax.vlines(df['date'], 0, levels, color="tab:red")  # The vertical stems.
ax.plot(  # Baseline and markers on it.
    df['date'],
    np.zeros_like(df['date']),
    "-o",
    color="k",
    markerfacecolor="w"
)

# annotate lines
for d, l, r in zip(df['date'], levels, df['event']):
    lines = r.split(' ')
    line1 = ' '.join(lines[:len(lines)//2])
    line2 = ' '.join(lines[len(lines)//2:])
    ax.annotate(
        line1 + '\n' + line2,
        xy=(d, l),
        xytext=(-5, np.sign(l) * 15),  # Increase the y-offset for more vertical space
        textcoords="offset points",
        horizontalalignment="center",
        verticalalignment="bottom",
        fontsize=8  # Adjust the font size to fit the annotations within the plot
    )
    
ax.text(0.5, 1.3, "PLOT PLOT PLOT", transform=ax.transAxes,  
         fontsize=16, fontweight='bold', ha='center') 
ax.get_yaxis().set_visible(False)  # Remove the y-axis
ax.spines['left'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)

plt.show()

Here is the image


Solution

  • You can solve the alignment with the red bars by using "bottom"/"top" depending on the sign:

    verticalalignment="bottom" if l<0 else "top",
    

    For the bottom annotations, you can collect all annotations in a list, draw the figure, then figure out which one has its bounding box on the bottom to rescale the Y-axis:

    annots = []
    for d, l, r in zip(df['date'], levels, df['event']):
        lines = r.split(' ')
        line1 = ' '.join(lines[:len(lines)//2])
        line2 = ' '.join(lines[len(lines)//2:])
        annots.append(ax.annotate(
            line1 + '\n' + line2,
            xy=(d, l),
            xytext=(-5, np.sign(l) * 20),  # Increase the y-offset for more vertical space
            textcoords="offset points",
            horizontalalignment="center",
            verticalalignment="bottom" if l<0 else "top",
            fontsize=8  # Adjust the font size to fit the annotations within the plot
        ))
    
    # ...
    
    # draw figure
    fig.canvas.draw()
    
    # fix Y-axis limits
    min_annot = min(a.get_window_extent().transformed(ax.transData.inverted()).bounds[1] for a in annots)
    ax.set_ylim(bottom = min_annot-0.5)
    

    Output:

    enter image description here

    Full code:

    df = pd.DataFrame(
        {
            'event': ['Qfpgv KFJPF fpnkmkq',
                      'Ltzx cqwup xnywi bxfjzgq ol mwwqmg ukszs',
                      'MUTD ysrfzad Urmiv lqyjexdq xqkqzfx vqtwrgh',
                      'Vxdys vjxqnqojq qvqoshjmhv dmyzf fj wvtrjv',
                      'Kcxtm-Bix Nzvlqj ajmydgbxk',
                      'Nrsbo! ukguvle xavahfg tqyikwqg, UZSP tgrlqfr',
                      'Rjxetf/uzpqwhwr qtshxvlp tljybtncbq qvqybnjgq dzqj',
                      'Qwvbt-Khspqw olfypkbvh tljmyyvz ajmy zazvqfm',
                      'UHW Umkqtqm zvhq tljybtncbq',
                      'Wwscye rukqdf, vfyvqmf udzvqmcv tljybtncbq',
                      'Twljq uqtrjxwh hyvnvwbl tljmyyvz rbykqkwqjg djzv Kqkmv xnyzqmv.',
                      'Qfpgv Qnwroj rymqzqm tljybtncbq kxqj vq Kqmnjp kxqkz.',
                      'Vwkqr jvqjg fqtwp, Jvccjvj CQM Sgqhojif mblqjc',
                      'Qxltj dqg Vqsue tljmyyvz jvtsqjwuj wkhruwqlqj, ixdro xqjolvkphw',
                      'Rwkq Vqwdqlqj odujhg jvswhuh fduuleolqj',
                      'Nzvq nqfupxqj jtsqjwuj',
                      'Vqolqjyphfwhv sohwwhuqvhtg jtsqjwuj',
                      'Ulnwj ri gihw dqg rqooih OY wlg ihghovdfh orqjv',
                      'Fkxohqjdoahoo sohwwhu ydplqhuv rqh wkhhtxdo jtsqjwuj'
                     ],
            'date': ['1984', '1987', '1991', '1994', '1997', '1998', '1999', '2002', '2004',
                     '2005', '2007', '2009', '2010', '2012', '2013', '2014', '2017', '2019', '2021']
        }
    )
    
    df['date'] = pd.to_datetime(df['date'])
    df['date'] = df['date'].dt.year
    
    levels = np.tile(
        [-5, 5, -3, 3, -1, 1, -7, 7, -4, 4, -2, 2, -6, 6, -3, 3, -1, 1, -5, 5, -3, 3, -1, 1, 5],
        int(np.ceil(len(df) / 6))
    )[:len(df)]
    
    fig, ax = plt.subplots(figsize=(12.8, 4), constrained_layout=True)
    
    ax.vlines(df['date'], 0, levels, color="tab:red")  # The vertical stems.
    ax.plot(  # Baseline and markers on it.
        df['date'],
        np.zeros_like(df['date']),
        "-o",
        color="k",
        markerfacecolor="w"
    )
    
    # annotate lines
    annots = []
    for d, l, r in zip(df['date'], levels, df['event']):
        lines = r.split(' ')
        line1 = ' '.join(lines[:len(lines)//2])
        line2 = ' '.join(lines[len(lines)//2:])
        annots.append(ax.annotate(
            line1 + '\n' + line2,
            xy=(d, l),
            xytext=(-5, np.sign(l) * 20),  # Increase the y-offset for more vertical space
            textcoords="offset points",
            horizontalalignment="center",
            verticalalignment="bottom" if l<0 else "top",
            fontsize=8  # Adjust the font size to fit the annotations within the plot
        ))
    
    
    ax.text(0.5, 1.3, "PLOT PLOT PLOT", transform=ax.transAxes,  
             fontsize=16, fontweight='bold', ha='center') 
    ax.get_yaxis().set_visible(False)  # Remove the y-axis
    ax.spines['left'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['top'].set_visible(False)
    
    fig.canvas.draw()
    
    min_annot = min(a.get_window_extent().transformed(ax.transData.inverted()).bounds[1] for a in annots)
    ax.set_ylim(bottom = min_annot-0.5)