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

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:

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

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

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

Threshold = 30 min (Large Grid):

Key Points: