Guide

This guide should introduce to the functionality provided in timetable. The guide uses two example calendar files, one containing work appointments and the other one containing duty information (e.g. work days, vacation and holidays).

A cumulative plot - in detail

Let’s start with an extensive example to explain the low-level primitives of timetable. The objective is to build a cumulative plot of the time spent during work. To spice things up, lets say that we want to get a daily overview, e.g. summing up the daily work.

The first step is to load the calendar file:

>>> from datetime import datetime, time, timedelta
>>> from timetable import (load_timetable, cut_timetable,
...         merge_intersections, annotate_timetable, collect_keys)
>>>
>>> start, end = datetime(2015, 1, 1), datetime(2015, 2, 1)
>>> calendar_files = {'Work': open('data/work.ics', 'rb')}
>>> timetable = load_timetable(calendar_files, clip_start=start, clip_end=end)

The load_timetable() function supports loading and merging multiple calendars at once, which is why a dictionary must be passed in containing the calendar files. Furthermore, the function also annotates entries with tags. If a calendar entry does not contain a tag, the key of the dictionary is used by default.

Note

It is a good idea to supply at least a clip_end date. Otherwise, an infinite amount of entries might get generated, if there are unlimited recurring events.

>>> days = []
>>> day = start
>>> while day < end:
...     days.append(datetime.combine(day, time()))
...     day += timedelta(days=1)

The next step is to compute a list of datetime objects at which we want to cut the timetable entries.

>>> # Cut timetable entries at day boundaries.
>>> work = {}
>>> for idx, day_timetable in enumerate(cut_timetable(timetable, days)):
...     # Merge all intersecting entries.
...     day_timetable = merge_intersections(day_timetable)
...     # Collect tags from merged entries.
...     day_timetable = annotate_timetable(day_timetable, collect_keys('tags'))
...     for start, end, entry in day_timetable:
...         # Compute duration and apply to timeseries.
...         duration = (end - start).total_seconds() / len(entry['tags'])
...         for tag in entry['tags']:
...             if not tag in work:
...                 work[tag] = [0 for day in days]
...             work[tag][idx] += duration

The separation of a timetable into daily timetables is done by the function cut_timetable().

Entries of a daily timetable might overlap. The merge_intersections() function merges entries from a timetable based on a key and generates non-intersecting entries.

The duration of an entry may now be calculated by simply subtracting the start datetime from the end datetime. The duration is accounted equally for each tag.

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from matplotlib.patches import Patch
>>> from matplotlib.cm import ScalarMappable
>>>
>>> fig = plt.figure()
>>> ax = fig.add_subplot(1, 1, 1)
>>>
>>> # Draw a stackplot with custom colors and legend handles.
>>> colormap = ScalarMappable(cmap='Paired')
>>> colormap.set_clim(0, len(work))
>>> data, colors, handles, labels = [], [], [], []
>>> for idx, name in enumerate(sorted(work)):
...     color = colormap.to_rgba(idx)
...     data.append(np.cumsum(work[name]) / 3600)
...     colors.append(color)
...     handles.append(Patch(color=color))
...     labels.append(name)
>>> ax.stackplot(days, data, colors=colors)
[...]
>>>
>>> fig.autofmt_xdate()
>>> ax.set_ylabel('Hours')
<...>
>>> ax.legend(handles, labels, loc='upper left')
<...>
>>>
>>> plt.show() 

The last code fragment builds a stacked cumulative plot for each of the collected time series.

(Source code, png, hires.png, pdf)

_images/guide-1.png

Using convenience functions

Timetable provides convenience functions for some of the tasks detailed in the above example. The following code computes the same work data as in the example:

>>> from datetime import datetime, timedelta
>>> from timetable import load_timetable, sum_timetable, datetime_range
>>>
>>> start, end = datetime(2015, 1, 1), datetime(2015, 2, 1)
>>> days = list(datetime_range(start, end, timedelta(days=1)))
>>>
>>> work = sum_timetable(load_timetable({'work': open('data/work.ics', 'rb')},
...         clip_start=start, clip_end=end), days)

The function datetime_range() is used to generate a list of datetime objects. sum_timetable() computes the activity timeseries data of group by a given key (e.g. the tags).

Adding duty information

Lets add duty information to the visualization. This example shows how to do basic timeseries arithmetic. The duty calendar contains a recurring event for the work duty (e.g. recurring each workday for a duration of 8 hours). Holidays and vacations are additional events and need to be subtracted from the work duty.

>>> from datetime import datetime, timedelta
>>> from timetable import load_timetable, sum_timetable, datetime_range
>>>
>>> start, end = datetime(2015, 1, 1), datetime(2015, 2, 1)
>>> days = list(datetime_range(start, end, timedelta(days=1)))
>>>
>>> work = sum_timetable(load_timetable({'Work': open('data/work.ics', 'rb')},
...         clip_start=start, clip_end=end), days)
>>> duty = sum_timetable(load_timetable({'Duty': open('data/duty.ics', 'rb')},
...         clip_start=start, clip_end=end), days)

We start by loading each calendar in two timetables and summing them up on each day.

>>> duty_series = [max(w - h - v, 0) / 3600
...         for w, h, v in zip(duty['Duty'], duty['Holiday'], duty['Vacation'])]

Now we need to subtract the holiday and vacation time from the duty time.

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from matplotlib.patches import Patch
>>> from matplotlib.cm import ScalarMappable
>>>
>>> fig = plt.figure()
>>> ax = fig.add_subplot(1, 1, 1)
>>>
>>> # Draw a stackplot with custom colors and legend handles.
>>> colormap = ScalarMappable(cmap='Paired')
>>> colormap.set_clim(0, len(work))
>>> data, colors, handles, labels = [], [], [], []
>>> for idx, name in enumerate(sorted(work)):
...     color = colormap.to_rgba(idx)
...     data.append(np.cumsum(work[name]) / 3600)
...     colors.append(color)
...     handles.append(Patch(color=color))
...     labels.append(name)
>>> ax.stackplot(days, data, colors=colors)
[...]

Draw the stacked cumulative plot as above.

>>> handles.extend(ax.plot(days, np.cumsum(duty_series), lw=2, ls='--'))
>>> labels.append('Duty')

Add a line for the duty time series.

>>> fig.autofmt_xdate()
>>> ax.set_ylabel('Hours')
<...>
>>> ax.legend(handles, labels, loc='upper left')
<...>
>>>
>>> plt.show() 

(Source code, png, hires.png, pdf)

_images/guide-2.png

There is a vacation in the third week of january in the example calendar file, which is also visible in the cumulative duty time.