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)
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)
There is a vacation in the third week of january in the example calendar file, which is also visible in the cumulative duty time.