Event Stacking Visualization
Visual demonstration of naive vs optimized event stacking
Scenario 1: Your Example - The Problem with Naive Stacking
Events:
- Event A: 09:00 - 14:00 (5 hours, contains both B and C)
- Event B: 10:00 - 12:00 (2 hours)
- Event C: 12:30 - 13:00 (30 minutes)
Key Observation: Event B and Event C do NOT overlap with each other!
They are separated by 30 minutes (12:00 to 12:30).
❌ Naive Stacking (Inefficient)
09:00
10:00
11:00
12:00
13:00
14:00
Event A (09:00-14:00)
Event B (10:00-12:00)
Event C (12:30-13:00)
Event A: marginLeft: 0px Level 0
Event B: marginLeft: 15px Level 1
Event C: marginLeft: 30px Level 2
Problem: Event C is pushed 30px to the right even though it doesn't conflict with Event B! Wastes 15px of space.
✅ Optimized Stacking (Efficient)
09:00
10:00
11:00
12:00
13:00
14:00
Event A (09:00-14:00)
Event B (10:00-12:00)
Event C (12:30-13:00)
Event A: marginLeft: 0px Level 0
Event B: marginLeft: 15px Level 1
Event C: marginLeft: 15px Level 1
Benefit: Event C reuses stackLevel 1 because it doesn't conflict with Event B. Saves 15px (33% space savings)!
Scenario 2: Multiple Parallel Tracks
Events:
- Event A: 09:00 - 15:00 (6 hours, very long event)
- Event B: 10:00 - 11:00 (1 hour)
- Event C: 11:30 - 12:30 (1 hour)
- Event D: 13:00 - 14:00 (1 hour)
Key Insight: Events B, C, and D all overlap with A, but NOT with each other.
They can all share stackLevel 1!
❌ Naive (4 levels)
09:00
10:00
11:00
12:00
13:00
14:00
15:00
Event A
Event B
Event C
Event D
Event A: Level 0 (0px)
Event B: Level 1 (15px)
Event C: Level 2 (30px)
Event D: Level 3 (45px)
Total width: 60px (0+15+30+45)
✅ Optimized (2 levels)
09:00
10:00
11:00
12:00
13:00
14:00
15:00
Event A
Event B
Event C
Event D
Event A: Level 0 (0px)
Event B: Level 1 (15px)
Event C: Level 1 (15px)
Event D: Level 1 (15px)
Total width: 30px (0+15+15+15)
50% space savings!
Scenario 3: Nested Overlaps with Optimization
Events:
- Event A: 09:00 - 15:00 (6 hours, contains all)
- Event B: 10:00 - 13:00 (3 hours)
- Event C: 11:00 - 12:00 (1 hour)
- Event D: 12:30 - 13:30 (1 hour)
Complex Case: C and D both overlap with A and B, but C and D don't overlap with each other. They can share a level!
❌ Naive (4 levels)
09:00
10:00
11:00
12:00
13:00
14:00
15:00
Event A
Event B
Event C
Event D
A: Level 0, B: Level 1, C: Level 2, D: Level 3
✅ Optimized (3 levels)
09:00
10:00
11:00
12:00
13:00
14:00
15:00
Event A
Event B
Event C
Event D
A: Level 0, B: Level 1, C & D: Level 2
25% space savings! D shares level with C because they don't overlap.
Scenario 4: Fully Nested Events - All Must Stack
Events:
- Event A: 09:00 - 15:00 (6 hours, contains B)
- Event B: 10:00 - 14:00 (4 hours, contains C)
- Event C: 11:00 - 13:00 (2 hours, innermost)
Important Case: When Event C is completely inside Event B, and Event B is completely inside Event A,
all three events overlap with each other. No optimization is possible - they must all stack sequentially.
Naive Stacking
09:00
10:00
11:00
12:00
13:00
14:00
15:00
Event A (09:00-15:00)
Event B (10:00-14:00)
Event C (11:00-13:00)
Event A: marginLeft: 0px Level 0
Event B: marginLeft: 15px Level 1
Event C: marginLeft: 30px Level 2
Analysis: All events overlap with each other:
• A overlaps B: ✓ (B is inside A)
• A overlaps C: ✓ (C is inside A)
• B overlaps C: ✓ (C is inside B)
Result: Sequential stacking required.
Optimized Stacking (Same Result)
09:00
10:00
11:00
12:00
13:00
14:00
15:00
Event A (09:00-15:00)
Event B (10:00-14:00)
Event C (11:00-13:00)
Event A: marginLeft: 0px Level 0
Event B: marginLeft: 15px Level 1
Event C: marginLeft: 30px Level 2
No Optimization Possible:
The optimized algorithm tries to assign C to level 1, but level 1 is occupied by B which overlaps with C.
It then tries level 2 - which is free. Result is identical to naive approach.
Algorithm Behavior:
For Event C (11:00-13:00):
overlapping = [Event A, Event B] // Both A and B overlap with C
Try stackLevel 0:
✗ Occupied by Event A (which overlaps C)
Try stackLevel 1:
✗ Occupied by Event B (which overlaps C)
Try stackLevel 2:
✓ Free! Assign C to stackLevel 2
Result: C must be at level 2 (no optimization)
Key Takeaway: Optimization only helps when events at higher levels don't overlap with each other.
When events are fully nested (matryoshka doll pattern), both approaches yield the same result.
Scenario 5: Column Sharing - When Events Start Close Together
New Concept: When events start within a threshold (±15 minutes, configurable), they should be displayed side-by-side (column sharing) instead of stacked.
Events:
- Event A: 10:00 - 13:00 (3 hours)
- Event B: 11:00 - 12:30 (1.5 hours, starts 60 min after A)
- Event C: 11:00 - 12:00 (1 hour, starts same time as B)
Threshold Logic (±15 minutes):
• Event A starts at 10:00
• Events B and C both start at 11:00
• A vs B/C: 60 minutes apart (exceeds ±15 min threshold) → A is stacked separately
• B vs C: 0 minutes apart (within ±15 min threshold) → B and C share flexbox
• Result: A gets full width (stackLevel 0), B and C share flexbox at stackLevel 1 (50%/50%)
❌ Pure Stacking (Poor UX)
10:00
11:00
12:00
12:30
13:00
Event A (10:00-13:00)
Event B (11:00-12:30)
Event C (11:00-12:00)
Event A: Level 0 (0px)
Event B: Level 1 (15px)
Event C: Level 2 (30px)
Problem: B and C start at the same time but are stacked sequentially.
Wastes horizontal space and makes it hard to see that they start together.
✅ Column Sharing (Better UX)
10:00
11:00
12:00
12:30
13:00
Event A (10:00-13:00)
Event B (11:00-12:30)
Event C (11:00-12:00)
Event A: stackLevel 0 (full width)
Events B & C: stackLevel 1 (flex: 1 each = 50% / 50%)
Benefits:
• Clear visual indication that B and C start at same time
• Better space utilization (no 30px offset for C)
• Scales well: if Event D is added at 11:00, all three share 33% / 33% / 33%
Column Sharing Algorithm:
const FLEXBOX_START_THRESHOLD_MINUTES = 15; // Configurable
function shouldShareFlexbox(event1, event2) {
const startDiff = Math.abs(event1.start - event2.start) / (1000 * 60);
return startDiff <= FLEXBOX_START_THRESHOLD_MINUTES;
}
For events A, B, C:
• A starts at 10:00
• B starts at 11:00 (diff = 60 min > 15 min) → A and B do NOT share
• C starts at 11:00 (diff = 0 min ≤ 15 min) → B and C DO share
Result:
• Event A: stackLevel 0, full width
• Events B & C: stackLevel 1, flexbox container (50% each)
Hybrid Approach: Column Sharing + Stacking + Nesting
The best approach combines three techniques:
- Column Sharing (Flexbox): When events start within ±15 min threshold
- Regular Stacking: When events start far apart (> 15 min)
- Nested Stacking: When an event starts outside threshold but overlaps a flexbox column
Example: If a 4th event (Event D) starts at 11:30, it would NOT join the B/C flexbox
(30 min > 15 min threshold). Instead, D would be stacked INSIDE whichever column it overlaps (e.g., B's column)
with a 15px left margin to show the nested relationship.
Scenario 6.5: Real Data - Events 144, 145, 146 (Chain Overlap)
Events (from actual JSON data):
- Event 145 (Månedlig Planlægning): 07:00 - 08:00 (1 hour)
- Event 144 (Team Standup): 07:30 - 08:30 (1 hour)
- Event 146 (Performance Test): 08:15 - 10:00 (1h 45min)
Key Observation:
• 145 ↔ 144: OVERLAP (07:30-08:00 = 30 min)
• 145 ↔ 146: NO OVERLAP (145 ends 08:00, 146 starts 08:15)
• 144 ↔ 146: OVERLAP (08:15-08:30 = 15 min)
Expected Stack Levels:
• Event 145: stackLevel 0 (margin-left: 0px)
• Event 144: stackLevel 1 (margin-left: 15px) - overlaps 145
• Event 146: stackLevel 2 (margin-left: 30px) - overlaps 144
Why 146 cannot share level with 145:
Even though 145 and 146 don't overlap, 146 overlaps with 144 (which has stackLevel 1).
Therefore 146 must be ABOVE 144 → stackLevel 2.
✅ Correct: Chain Overlap Stacking
145: Månedlig (07:00-08:00)
144: Standup (07:30-08:30)
146: Performance (08:15-10:00)
Event 145: marginLeft: 0px Level 0
Event 144: marginLeft: 15px Level 1
Event 146: marginLeft: 30px Level 2
Why stackLevel 2 for 146?
146 overlaps with 144 (stackLevel 1), so 146 must be positioned ABOVE 144.
Even though 146 doesn't overlap 145, it forms a "chain" through 144.
Scenario 7: Column Sharing for Overlapping Events Starting Simultaneously
Events (start at same time but overlap):
- Event 153: 09:00 - 10:00 (1 hour)
- Event 154: 09:00 - 09:30 (30 minutes)
Key Observation:
• Events start at SAME time (09:00)
• Event 154 OVERLAPS with Event 153 (09:00-09:30)
• Even though they overlap, they should share columns 50/50 because they start simultaneously
Expected Rendering:
• Use GRID container (not stacking)
• Both events get 50% width (side-by-side)
• Event 153: Full height (1 hour) in left column
• Event 154: Shorter height (30 min) in right column
Rule:
Events starting simultaneously (±15 min) should ALWAYS use column sharing (GRID),
even if they overlap each other.
❌ Wrong: Stacking
153 (09:00-10:00)
154 (09:00-09:30)
Problem: Event 154 is stacked on top of 153 even though they start at the same time.
This makes it hard to see that they're simultaneous events.
✅ Correct: Column Sharing (GRID)
153 (09:00-10:00)
154 (09:00-09:30)
Benefits:
• Clear visual that events start simultaneously
• Better use of horizontal space
• Each event gets 50% width instead of being stacked
Scenario 6: Column Sharing with Nested Stacking
Complex Case: What happens when a 4th event needs to be added to an existing column-sharing group?
Events:
- Event A: 10:00 - 13:00 (3 hours)
- Event B: 11:00 - 12:30 (1.5 hours)
- Event C: 11:00 - 12:00 (1 hour)
- Event D: 11:30 - 11:45 (15 minutes) ← NEW!
New Rule: Flexbox threshold = ±15 minutes (configurable)
• B starts at 11:00
• C starts at 11:00 (diff = 0 min ≤ 15 min) → B and C share flexbox ✓
• D starts at 11:30 (diff = 30 min > 15 min) → D does NOT join flexbox ✗
• D overlaps only with B → D is stacked inside B's column ✓
❌ All Events in Flexbox (Wrong)
10:00
11:00
12:00
12:30
13:00
Problem: All events get 33% width, making them too narrow.
Event D is squeezed even though it's contained within Event B's timeframe.
✅ Flexbox + Nested Stack in Column
10:00
11:00
12:00
12:30
13:00
Event A (10:00-13:00)
Event B (11:00-12:30)
Event D (11:30-11:45)
Event C (11:00-12:00)
Event A: stackLevel 0 (full width)
Events B & C: stackLevel 1 (flexbox 50%/50%)
Event D: Nested in B's column with 15px marginLeft
Strategy:
• B and C start at 11:00 (diff = 0 min ≤ 15 min threshold) → Use flexbox ✓
• D starts at 11:30 (diff = 30 min > 15 min threshold) → NOT in flexbox ✗
• D overlaps with B (11:00-12:30) but NOT C (11:00-12:00) ✓
• D is stacked INSIDE B's flexbox column with 15px left margin
Nested Stacking in Flexbox Columns:
const FLEXBOX_START_THRESHOLD_MINUTES = 15; // Configurable
Step 1: Identify flexbox groups (events starting within ±15 min)
• B starts at 11:00
• C starts at 11:00 (diff = 0 min ≤ 15 min) → B and C share flexbox ✓
• D starts at 11:30 (diff = 30 min > 15 min) → D does NOT join flexbox ✗
Step 2: Create flexbox for B and C
• Flexbox container at stackLevel 1 (15px from A)
• B gets 50% width (left column)
• C gets 50% width (right column)
Step 3: Process Event D (11:30-11:45)
• D overlaps with B (11:00-12:30)? YES ✓
• D overlaps with C (11:00-12:00)? NO ✗ (D starts at 11:30, C ends at 12:00)
Wait... 11:30 < 12:00, so they DO overlap!
• D overlaps with ONLY B? Let's check:
- B: 11:00-12:30, D: 11:30-11:45 → overlap ✓
- C: 11:00-12:00, D: 11:30-11:45 → overlap ✓
• Actually D overlaps BOTH! But start time difference (30 min) > threshold
• Decision: Stack D inside the column it overlaps most with (B is longer)
Step 4: Nested stacking inside B's column
• D is placed INSIDE B's flexbox column (position: relative)
• D gets 15px left margin (stacked within the column)
• D appears only in B's half, not spanning both
Result: Flexbox preserved, D clearly nested in B!
Decision Tree: When to Use Nested Stacking
Analyzing events B (11:00-12:30), C (11:00-12:00), D (11:30-11:45):
Step 1: Check flexbox threshold (±15 min)
├─ B starts 11:00
├─ C starts 11:00 (diff = 0 min ≤ 15 min) → Join flexbox ✓
└─ D starts 11:30 (diff = 30 min > 15 min) → Do NOT join flexbox ✗
Step 2: Create flexbox for B and C
└─ Flexbox container: [B (50%), C (50%)]
Step 3: Process Event D
├─ D starts OUTSIDE threshold (30 min > 15 min)
├─ Check which flexbox columns D overlaps:
│ ├─ D overlaps B? → YES ✓ (11:30-11:45 within 11:00-12:30)
│ └─ D overlaps C? → YES ✓ (11:30-11:45 within 11:00-12:00)
│
└─ D overlaps BOTH B and C
Step 4: Placement strategy
• D cannot join flexbox (start time > threshold)
• D overlaps multiple columns
• Choose primary column: B (longer duration: 1.5h vs 1h)
• Nest D INSIDE B's column with 15px left margin
Result:
• Flexbox maintained for B & C (50%/50%)
• D stacked inside B's column with clear indentation
💡 Key Insight: Flexbox Threshold + Nested Stacking
The Two-Rule System:
- Flexbox Rule: Events with start times within
±15 minutes (configurable) share flexbox columns
- Nested Stacking Rule: Events starting OUTSIDE threshold are stacked inside the overlapping flexbox column with 15px left margin
Why ±15 minutes (not ±30)?
A tighter threshold ensures that only events with truly simultaneous start times share columns.
Events starting 30 minutes later are clearly sequential and should be visually nested/indented.
When event overlaps multiple columns:
Choose the column with longest duration (or earliest start) as the "primary" parent,
and nest the event there with proper indentation. This maintains the flexbox structure
while showing clear parent-child relationships.
Configuration: FLEXBOX_START_THRESHOLD_MINUTES = 15
Summary: Unified Layout Logic
🎯 The Core Algorithm - One Rule to Rule Them All
All scenarios follow the same underlying logic:
- Group by start time proximity: Events starting within ±15 min share a container
- Container type decision:
- If group has 1 event → Regular positioning (no special container)
- If group has 2+ events with no mutual overlaps → Flexbox container
- If group has overlapping events → Regular stacking container
- Handle late arrivals: Events starting OUTSIDE threshold (> 15 min later) are nested inside the container they overlap with
Scenario 1-2
Optimized Stacking
- No flexbox groups
- Events share levels when they don't overlap
- Pure optimization play
Scenario 5
Flexbox Columns
- B & C start together (±15 min)
- They don't overlap each other
- Perfect for flexbox (50%/50%)
Scenario 6
Nested in Flexbox
- B & C flexbox maintained
- D starts later (> 15 min)
- D nested in B's column
Unified Algorithm - All Scenarios Use This
const FLEXBOX_START_THRESHOLD_MINUTES = 15;
const STACK_OFFSET_PX = 15;
// PHASE 1: Group events by start time proximity
function groupEventsByStartTime(events) {
const groups = [];
const sorted = events.sort((a, b) => a.start - b.start);
for (const event of sorted) {
const existingGroup = groups.find(g => {
const groupStart = g[0].start;
const diffMinutes = Math.abs(event.start - groupStart) / (1000 * 60);
return diffMinutes <= FLEXBOX_START_THRESHOLD_MINUTES;
});
if (existingGroup) {
existingGroup.push(event);
} else {
groups.push([event]);
}
}
return groups; // Each group = events starting within ±15 min
}
// PHASE 2: Decide container type for each group
function decideContainerType(group) {
if (group.length === 1) return 'NONE';
// Check if any events in group overlap each other
const hasOverlaps = group.some((e1, i) =>
group.slice(i + 1).some(e2 => doEventsOverlap(e1, e2))
);
return hasOverlaps ? 'STACKING' : 'FLEXBOX';
}
// PHASE 3: Handle events that start OUTSIDE threshold
function nestLateArrivals(groups, allEvents) {
for (const event of allEvents) {
const belongsToGroup = groups.some(g => g.includes(event));
if (belongsToGroup) continue; // Already placed
// Find which group/container this event overlaps with
const overlappingGroup = groups.find(g =>
g.some(e => doEventsOverlap(event, e))
);
if (overlappingGroup) {
// Nest inside the overlapping container
// If flexbox: choose column with longest duration
// If stacking: add to stack with proper offset
nestEventInContainer(event, overlappingGroup);
}
}
}
Result: One algorithm handles ALL scenarios!
Algorithm Complexity
- Overlap Detection: O(n²) where n = number of events
- Grouping by Start Time: O(n log n) for sorting
- Stack Assignment: O(n²) for checking all overlaps
- Visual Update: O(n) to apply styling
Total: O(n²) - Same as naive approach, but with much better UX!
🎯 Key Insight: The Pattern That Connects Everything
The same 3-phase algorithm handles all scenarios:
| Phase |
Logic |
Scenario 1-4 |
Scenario 5 |
Scenario 6 |
| 1. Group |
Start time ±15 min |
No groups (all separate) |
[B, C] group |
[B, C] group, D separate |
| 2. Container |
Overlaps? Stack : Flex |
N/A (single events) |
Flexbox (no overlaps) |
Flexbox (no overlaps) |
| 3. Late arrivals |
Nest in overlapping container |
N/A |
N/A |
D nested in B's column |
Conclusion: The difference between scenarios is NOT different algorithms,
but rather different inputs to the same algorithm. The 3 phases always run in order,
and each phase makes decisions based on the data (start times, overlaps, thresholds).