Event Stacking Visualization

Visual demonstration of naive vs optimized event stacking

Scenario 1: Your Example - The Problem with Naive Stacking

Events:

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:

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:

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:

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:

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:

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):

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
07:00
08:00
09:00
10:00
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):

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
09:00
09:30
10:00
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)
09:00
09:30
10:00
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:

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
Event A
Event B
Event C
Event D
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:

  1. Flexbox Rule: Events with start times within ±15 minutes (configurable) share flexbox columns
  2. 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:

  1. Group by start time proximity: Events starting within ±15 min share a container
  2. 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
  3. 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

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).