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

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

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

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

## Scenario 8: Edge Case - Events Starting Exactly 15 Minutes Apart (WITH Overlap)

Edge Case: What happens when events start exactly at the ±15 min threshold AND overlap?

Events:

- Event A: 11:00 - 12:00 (1 hour)
- Event B: 11:15 - 12:30 (1.25 hours)

Analysis:
• A starts at 11:00
• B starts at 11:15 (diff = 15 min ≤ 15 min) → Within threshold ✓
• A and B overlap (11:15 - 12:00) → They DO overlap ✓
• Visual priority: Show that they start simultaneously (±15 min)
• Result: Use GRID (column sharing) even though they overlap
❌ Wrong: Stacking (Hides Simultaneity)
11:00
11:30
12:00
12:30
Event A
11:00-12:00
Event B
11:15-12:30
Problems:
• B is offset to the right → looks like it happens AFTER A
• Doesn't convey that they start almost simultaneously (15 min apart)
• Wastes horizontal space
✅ Correct: GRID Column Sharing
11:00
11:30
12:00
12:30
Event A
11:00-12:00
Event B
11:15-12:30
Benefits:
• Side-by-side layout shows they're concurrent
• Each event gets 50% width
• Clear visual: these events start nearly simultaneously (±15 min)
• Despite overlapping, simultaneity is visual priority
Key Rule: Events starting within ±15 minutes should ALWAYS use GRID (column sharing), even if they overlap. The visual priority is to show that events start simultaneously, not to avoid overlap. Overlap is handled by the grid container having appropriate height.
Expected Behavior:

    // Phase 1: Group by start time
    groupEventsByStartTime([A, B])
      → Group 1: [A, B]  // 15 min apart ≤ threshold

    // Phase 2: Decide container type
    decideContainerType(Group 1)
      → GRID  // Always GRID for grouped events, even if overlapping

    // Phase 3: Calculate stack level
    calculateGridGroupStackLevel(Group 1)
      → stackLevel: 0  // No other events to stack above

    // Result:
    <swp-event-group class="cols-2 stack-level-0" style="top: 0px; margin-left: 0px; z-index: 100;">
      <swp-event data-event-id="A" style="height: 120px;">Event A</swp-event>
      <swp-event data-event-id="B" style="height: 150px; top: 10%;">Event B</swp-event>
    </swp-event-group>

## Scenario 9: Grid with Staggered Start Times

Event A: 09:00 - 10:00 (1 hour)
Event B: 09:30 - 10:30 (1 hour, starts 30 min after A)
Event C: 10:15 - 12:00 (1h 45min, starts 45 min after B)
Special Case: End-to-Start Conflicts Create Shared Columns
• Event A: 09:00 - 10:00
• Event B: 09:30 - 10:30 (starts 30 min before A ends → conflicts with A)
• Event C: 10:15 - 12:00 (starts 15 min before B ends → conflicts with B)
Key Rule: Events share columns (GRID) when they conflict within threshold
• Conflict = Event starts within ±threshold minutes of another event's end time
• A and B: B starts 30 min before A ends → conflict (≤ 30 min threshold)
• B and C: C starts 15 min before B ends → conflict (≤ 30 min threshold)
• Therefore: A, B, and C all share columns in a 3-column GRID
With threshold = 15 min: Only A-B conflict (30 min > 15), C is separate → Stack
With threshold = 30 min: Both A-B and B-C conflict → All 3 share columns in GRID
With Threshold = 15 min
09:00
10:00
11:00
12:00
Event A
09:00-10:00
Event B
09:30-10:30
Event C
10:15-12:00
With Threshold = 30 min
09:00
10:00
11:00
12:00
Event A
09:00-10:00
Event C
10:15-12:00
Event B
09:30-10:30

### Stack Analysis

Threshold = 15 min (Stack):

- Event A: stackLevel 0
- Event B: stackLevel 1 (starts 30 min before A ends, but 30 > 15 threshold) → Stack with margin-left: 15px
- Event C: stackLevel 2 (starts 15 min before B ends, but separate from A-B stack) → Stack with margin-left: 30px

Threshold = 30 min (Shared GRID with 2 columns):

- Grid Group (A, B & C): 2-column grid layout
- Column 1: Event A (09:00-10:00) + Event C (10:15-12:00) - they don't overlap!
- Column 2: Event B (09:30-10:30) - overlaps both A and C
- Event A: grid column 1, top: 0px
- Event B: grid column 2, top: 30px
- Event C: grid column 1, top: 75px (shares column with A, no overlap)
- All events: stackLevel 0, margin-left: 0px (no stacking, all in same grid container)

## Scenario 10: Complex Column Sharing

Event A: 12:00 - 15:00 (3 hours)
Event B: 12:30 - 13:00 (30 min, starts 30 min after A)
Event C: 13:30 - 14:30 (1 hour, starts 30 min after B ends)
Event D: 14:00 - 15:00 (1 hour, starts 30 min before C ends)
Event E: 14:00 - 15:00 (1 hour, starts same time as D)
Analysis with threshold = 30 min:
• A-B conflict: B starts 30 min after A (≤ 30) → grouped
• B-C conflict: C starts 30 min after B ends (≤ 30) → grouped with A-B
• C-D conflict: D starts 30 min before C ends (≤ 30) → grouped with A-B-C
• D-E conflict: D and E start at same time (0 min) → grouped with all
• Therefore: All 5 events in ONE grid group
Column allocation:
• A overlaps: B, C, D, E → needs own column
• B overlaps: A → needs own column
• C overlaps: A, D, E → needs own column
• D overlaps: A, C, E → needs own column
• E overlaps: A, C, D → can share column with B (they don't overlap)
• Result: 4 columns needed
With Threshold = 15 min
12:00
13:00
14:00
15:00
Event A
12:00-15:00
Event B
12:30-13:00
Event C
13:30-14:30
Event D
14:00-15:00
Event E
14:00-15:00
With Threshold = 30 min
12:00
13:00
14:00
15:00
Event A
12:00-15:00
Event B
12:30-13:00
Event E
14:00-15:00
Event C
13:30-14:30
Event D
14:00-15:00

### Expected Layout

Threshold = 15 min (Stack + Small Grid):

- Event A: stackLevel 0
- Event B: stackLevel 1 (overlaps A, 30 min > 15 threshold) → margin-left: 15px
- Event C: stackLevel 2 (overlaps A, 30 min > 15 threshold) → margin-left: 30px
- Grid Group (D & E): stackLevel 3 (start simultaneously) → margin-left: 45px
  - 2-column grid: D in column 1, E in column 2

Threshold = 30 min (Large Grid):

- Grid Group (A, B, C, D, E): All in ONE grid group
  - Column 1: Event A (top: 0px, height: 180px)
  - Column 2: Event B (top: 30px) + Event E (top: 120px)
  - Column 3: Event C (top: 90px)
  - Column 4: Event D (top: 120px)

Key Points:

- With threshold = 30: All events grouped due to chained end-to-start conflicts
- With threshold = 15: Only D & E grouped (start simultaneously), A/B/C stacked separately
- B and E can share column 2 (they don't overlap: B ends 13:00, E starts 14:00)
- D and E start at same time but need separate columns (they overlap perfectly)
- Result with 30 min: 4 columns instead of 5 (optimization saves 1 column)