calendar visualization
event layout algorithm
scheduling
event management
algorithm design

Visualization of calendar events. Algorithm to layout events with maximum width

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The classic day-view calendar problem is not just "draw overlapping events side by side." The real goal is to assign each event the maximum width it can occupy without covering another event that overlaps in time.

That turns the layout into an interval-graph problem. You group overlapping events together, assign them to columns, and then expand each event horizontally as far as later columns allow.

Model Events As Time Intervals

Each event is an interval with a start and end. Two events conflict if their intervals overlap in time. In a vertical day view:

  • vertical position comes from time
  • horizontal columns are used only to separate overlapping intervals

A simple event model in Python:

python
1from dataclasses import dataclass
2
3@dataclass
4class Event:
5    id: str
6    start: int   # minutes from midnight
7    end: int

Using minutes keeps comparisons easy and avoids UI-specific assumptions early in the algorithm.

Step 1: Sort Events

Sort events by start time, and break ties by end time. This gives a stable order for building overlap groups and columns.

python
events = sorted(events, key=lambda e: (e.start, e.end))

This is the natural first step because interval layout depends on chronological order.

Step 2: Build Overlap Groups

Events that overlap directly or indirectly belong to the same layout group. For example, if event A overlaps B and B overlaps C, all three must be considered together even if A and C do not touch directly.

You can form groups with a sweep:

python
1def group_overlaps(events):
2    groups = []
3    current = []
4    current_end = -1
5
6    for event in events:
7        if not current or event.start < current_end:
8            current.append(event)
9            current_end = max(current_end, event.end)
10        else:
11            groups.append(current)
12            current = [event]
13            current_end = event.end
14
15    if current:
16        groups.append(current)
17
18    return groups

Each group can now be laid out independently. That is useful because width calculations do not need to consider events that are completely disjoint in time.

Step 3: Assign Columns Greedily

Within one group, assign each event to the first column whose latest event has already ended. If no column is free, open a new column.

python
1def assign_columns(group):
2    columns = []
3    placement = {}
4
5    for event in group:
6        for col_index, col in enumerate(columns):
7            if col[-1].end <= event.start:
8                col.append(event)
9                placement[event.id] = col_index
10                break
11        else:
12            columns.append([event])
13            placement[event.id] = len(columns) - 1
14
15    return columns, placement

This greedy strategy is standard for interval partitioning and produces the minimum number of columns for the group.

Step 4: Expand Events To Maximum Width

If you stop after column assignment, every event gets width 1 / column_count, but that is not maximum width. Some events can span into later columns if those columns are empty during the event's time range.

So for each event, scan columns to the right until you find one containing an overlapping event:

python
1def can_share_column(event, column):
2    return all(other.end <= event.start or other.start >= event.end for other in column)
3
4
5def compute_spans(group, columns, placement):
6    spans = {}
7
8    for event in group:
9        col = placement[event.id]
10        span = 1
11
12        for next_col in range(col + 1, len(columns)):
13            if can_share_column(event, columns[next_col]):
14                span += 1
15            else:
16                break
17
18        spans[event.id] = span
19
20    return spans

Now an event in column 0 might occupy two or three columns worth of width if the later columns are free during its time interval.

Convert Layout To UI Frames

Once you know the group column count, each event's column index, and its span, converting to UI geometry is straightforward:

  • 'top and height come from time'
  • 'left comes from column_index / column_count'
  • 'width comes from span / column_count'

That gives the maximum-width layout pattern seen in many day-view calendars.

Why This Works

The greedy column assignment solves the conflict-separation problem. The expansion pass solves the wasted-space problem. Without expansion, narrow events often leave empty white gaps to the right even when no conflict exists there. The expansion step is what upgrades a merely correct overlap layout into the more polished "maximum width" layout users expect.

Common Pitfalls

One common mistake is grouping only directly overlapping neighbors and missing transitive overlap groups. Another is assigning equal width to every event in the group and stopping there, which wastes space. Developers also often mishandle boundary conditions such as one event ending exactly when another begins. In most calendar UIs, that should count as non-overlapping, so end <= next.start is the safe comparison. Finally, if all layout logic is tied too early to pixel values, debugging becomes harder. Keep the algorithm in abstract columns and spans first, then map it to rendering coordinates.

Summary

  • Treat calendar events as intervals and sort them by start time.
  • Group events that overlap directly or indirectly.
  • Assign columns greedily to separate conflicting events.
  • Expand each event horizontally into later free columns to achieve maximum width.
  • Convert the final column index and span into screen coordinates only after the interval layout is known.

Course illustration
Course illustration

All Rights Reserved.