Search code examples
pythonmatplotlibtime-seriestimeline

How to plot timeline with multiple time-marker stems?


I'm new to matplotlib, and trying to plot something quite difficult.

I would like to plot something like (taken from the matplotlib docs):

Except, I want the timeline (x-axis) and stems to have labels in time, like:

timeline = ['0:01:00', '0:02:00', '0:03:00', '0:04:00', ...]
stems1 = ['0:01:45', '0:03:55', '0:04:22', ...]
stems2 = ['0:02:21', '0:06:54', ...

Notes:

  • Timeline ticks are evenly spaced
  • stems1 and stems2 don't necesarily have the same number of points, but are in order (like a video timeline)

It would be even better if stems1 and stems2 were different colors.

If anyone could point me in the right direction, or even code a working example, it'd be greatly appreciated! Thank you for reading.


Edit: Following @r-beginners's answer to this post

I have something like this:

for time, level, label, va in zip(timeline, levels, labels, verticalalignments):
        ax.annotate(label, xy=(time, level), xytext=(15, np.sign(level)*15),
                    textcoords="offset points",
                    verticalalignment=va,
                    horizontalalignment="right",
                    color='blue')

    for time, level, pred, va in zip(timeline, levels, preds, verticalalignments):
        ax.annotate(pred, xy=(time, level), xytext=(15, np.sign(level)*15),
                    textcoords="offset points",
                    verticalalignment=va,
                    horizontalalignment="right",
                    color='green')

enter image description here The issue is that the graphs are overlapping, stem color are both red, and the stems don't align with the timeline.


Edit 2: With @r-beginners code, I've tried it with 2 new stems, where stem1 isn't being plotted completely:

stem1 = ['0:08:08', '0:08:52', '0:09:42', '0:10:20', '0:10:55', '0:11:24', '0:12:31', '0:13:07', '0:13:45', '0:14:16', '0:14:49', '0:15:20', '0:15:51', '0:16:21', '0:16:53', '0:17:28', '0:19:01', '0:19:22', '0:20:19', '0:20:48', '0:21:19', '0:22:05', '0:23:06', '0:23:34', '0:24:03', '0:24:30', '0:24:51', '0:25:18', '0:25:54', '0:26:25', '0:27:07', '0:28:05', '0:29:04', '0:29:30', '0:30:34', '0:32:57', '0:33:28', '0:33:57', '0:34:35', '0:35:01', '0:35:41', '0:36:06', '0:36:30', '0:37:01', '0:37:33', '0:38:06', '0:38:40', '0:39:21', '0:40:02', '0:40:22', '0:40:42', '0:41:32', '0:41:56', '0:43:20', '0:43:39', '0:44:02', '0:44:26', '0:45:04', '0:45:32', '0:46:02', '0:47:00', '0:47:42', '0:48:05', '0:48:35', '0:49:02', '0:49:25', '0:49:56', '0:50:43', '0:51:25', '0:51:43', '0:52:18', '0:52:49', '0:53:08']

stem2 = ['0:09:49', '0:10:24', '0:14:27', '0:24:31', '0:26:03']

Code afterwards:

# Create figure
    fig, ax = plt.subplots(figsize=(100, 10), constrained_layout=True)
    ax.set(title='TEST')

    # Stem values
    names = [e for row in zip(stem1, stem2) for e in row]

    # Timeline
    timeline = [datetime.datetime.strptime(n, '%H:%M:%S') for n in names]

    # Stem levels
    levels = np.tile([-5, 5, -3, 3, -1, 1], int(np.ceil(len(names)/6)))[:len(names)]

    # Stems
    ax.vlines(timeline, 0, levels, color='tab:red')

    # Plot timeline
    ax.plot(timeline, np.zeros_like(timeline), "-o", color="k", markerfacecolor="w")

    # Plot GT labels and predictions
    for time, level, name in zip(timeline, levels, names):
        ax.annotate(name, xy=(time, level),
                    xytext=(15, np.sign(level)*3),
                    textcoords='offset points',
                    horizontalalignment='right',
                    verticalalignment='bottom' if level > 0 else 'top',
                    color='green' if level > 0 else 'blue')

    # De-clutter axes
    ax.yaxis.set_visible(False)
    ax.spines[["left", "top", "right"]].set_visible(False)

    # ...
    minutes = mdates.MinuteLocator(interval=1)
    minutes_fmt = mdates.DateFormatter('%M:%S')
    ax.xaxis.set_major_locator(minutes)
    ax.xaxis.set_major_formatter(minutes_fmt)

    # Rotate x-ticks
    plt.setp(ax.get_xticklabels(), rotation=30, ha="right")

    # Save figure
    plt.savefig('test.png', bbox_inches='tight')

Solution

  • The data was only partially available, so I created it appropriately. I have solved your problem by referring to the official example that you refer to. For overlapping strings, create a list of positive and negative placement heights and draw a vertical line in red. Convert the stem information to time series information and draw a timeline. In the annotation looping process, the placement height value is judged for placement and color to distinguish them. The time series representation of the x-axis is set by MinuteLocator to determine the time format.

    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    import matplotlib.dates as mdates
    from datetime import datetime
    
    fig, ax = plt.subplots(figsize=(16, 4), constrained_layout=True)
    ax.set(title="Time line demo ")
    
    stem1 = ['0:08:08', '0:08:52', '0:09:42', '0:10:20', '0:10:55', '0:11:24', '0:12:31', '0:13:07', '0:13:45', '0:14:16', '0:14:49', '0:15:20', '0:15:51', '0:16:21', '0:16:53', '0:17:28', '0:19:01', '0:19:22', '0:20:19', '0:20:48', '0:21:19', '0:22:05', '0:23:06', '0:23:34', '0:24:03', '0:24:30', '0:24:51', '0:25:18', '0:25:54', '0:26:25', '0:27:07', '0:28:05', '0:29:04', '0:29:30', '0:30:34', '0:32:57', '0:33:28', '0:33:57', '0:34:35', '0:35:01', '0:35:41', '0:36:06', '0:36:30', '0:37:01', '0:37:33', '0:38:06', '0:38:40', '0:39:21', '0:40:02', '0:40:22', '0:40:42', '0:41:32', '0:41:56', '0:43:20', '0:43:39', '0:44:02', '0:44:26', '0:45:04', '0:45:32', '0:46:02', '0:47:00', '0:47:42', '0:48:05', '0:48:35', '0:49:02', '0:49:25', '0:49:56', '0:50:43', '0:51:25', '0:51:43', '0:52:18', '0:52:49', '0:53:08']
    stem2 = ['0:09:49', '0:10:24', '0:14:27', '0:24:31', '0:26:03']
    
    stems = stem1 + stem2
    timelines = sorted([datetime.strptime(s, '%H:%M:%S') for s in stem])
    labels = [datetime.strftime(t, '%H:%M:%S') for t in timelines]
    levels = np.tile([-7, 7, -5, 5, -3, 3, -1, 1], int(np.ceil(len(timelines)/8)))[:len(timelines)]
    
    ax.vlines(timelines, 0, levels, color='tab:red')
    ax.plot(timelines, np.zeros_like(timelines), "-o", color="k", markerfacecolor="w")
    
    for t, l, b in zip(timelines, levels, labels):
        if datetime.strftime(t, '%H:%M:%S')[1:] in stem2:
            color = 'blue'
        else:
            color = 'green'
        ax.annotate(b, xy=(t, l),
                    xytext=(22, np.sign(l)*3), textcoords='offset points',
                    horizontalalignment='right',
                    verticalalignment='bottom' if l > 0 else 'top',
                    color=color
                   )
        
    ax.yaxis.set_visible(False)
    ax.spines[["left", "top", "right"]].set_visible(False)
    ax.spines['bottom'].set_position(('data', -8))
    
    minutes = mdates.MinuteLocator(interval=1)
    minutes_fmt = mdates.DateFormatter('%M:%S')
    ax.xaxis.set_major_locator(minutes)
    ax.xaxis.set_major_formatter(minutes_fmt)
    
    plt.setp(ax.get_xticklabels(), rotation=90, ha='center')
    
    plt.show()
    

    enter image description here