# Event Stacking Concept **Calendar Plantempus - Visual Event Overlap Management** --- ## Overview **Event Stacking** is a visual technique for displaying overlapping calendar events by offsetting them horizontally with a cascading effect. This creates a clear visual hierarchy showing which events overlap in time. --- ## Visual Concept ### Basic Stacking When multiple events overlap in time, they are "stacked" with increasing left margin: ``` Timeline: 08:00 ───────────────────────────────── │ 09:00 │ Event A starts │ ┌─────────────────────┐ │ │ Meeting A │ 10:00 │ │ │ │ │ Event B starts │ │ │ ┌─────────────────────┐ 11:00 │ │ │ Meeting B │ │ └──│─────────────────────┘ │ │ │ 12:00 │ │ Event C starts │ │ │ ┌─────────────────────┐ │ └──│─────────────────────┘ 13:00 │ │ Meeting C │ │ └─────────────────────┘ 14:00 ───────────────────────────────── Visual Result (stacked view): ┌─────────────────────┐ │ Meeting A │ │ ┌─────────────────────┐ │ │ Meeting B │ └─│─────────────────────┘ │ ┌─────────────────────┐ │ │ Meeting C │ └─│─────────────────────┘ └─────────────────────┘ ``` Each subsequent event is offset by **15px** to the right. --- ## Stack Link Data Structure Stack links create a **doubly-linked list** stored directly in DOM elements as data attributes. ### Interface Definition ```typescript interface StackLink { prev?: string; // Event ID of previous event in stack next?: string; // Event ID of next event in stack stackLevel: number; // Position in stack (0 = base, 1 = first offset, etc.) } ``` ### Storage in DOM Stack links are stored as JSON in the `data-stack-link` attribute: ```html ``` ### Benefits of DOM Storage ✅ **State follows the element** - No external state management needed ✅ **Survives drag & drop** - Links persist through DOM manipulations ✅ **Easy to query** - Can traverse chain using DOM queries ✅ **Self-contained** - Each element knows its position in the stack --- ## Overlap Detection Events overlap when their time ranges intersect. ### Time-Based Overlap Algorithm ```typescript function doEventsOverlap(eventA: CalendarEvent, eventB: CalendarEvent): boolean { // Two events overlap if: // - Event A starts before Event B ends AND // - Event A ends after Event B starts return eventA.start < eventB.end && eventA.end > eventB.start; } ``` ### Example Cases **Case 1: Events Overlap** ``` Event A: 09:00 ──────── 11:00 Event B: 10:00 ──────── 12:00 Result: OVERLAP (10:00 to 11:00) ``` **Case 2: No Overlap** ``` Event A: 09:00 ──── 10:00 Event B: 11:00 ──── 12:00 Result: NO OVERLAP ``` **Case 3: Complete Containment** ``` Event A: 09:00 ──────────────── 13:00 Event B: 10:00 ─── 11:00 Result: OVERLAP (Event B fully inside Event A) ``` --- ## Visual Styling ### CSS Calculations ```typescript const STACK_OFFSET_PX = 15; // For each event in stack: marginLeft = stackLevel * STACK_OFFSET_PX; zIndex = 100 + stackLevel; ``` ### Example with 3 Stacked Events ```typescript Event A (stackLevel: 0): marginLeft = 0 * 15 = 0px zIndex = 100 + 0 = 100 Event B (stackLevel: 1): marginLeft = 1 * 15 = 15px zIndex = 100 + 1 = 101 Event C (stackLevel: 2): marginLeft = 2 * 15 = 30px zIndex = 100 + 2 = 102 ``` Result: Event C appears on top, Event A at the base. --- ## Optimized Stacking (Smart Stacking) ### The Problem: Naive Stacking vs Optimized Stacking **Naive Approach:** Simply stack all overlapping events sequentially. ``` Event A: 09:00 ════════════════════════════ 14:00 Event B: 10:00 ═════ 12:00 Event C: 12:30 ═══ 13:00 Naive Result: Event A: stackLevel 0 Event B: stackLevel 1 Event C: stackLevel 2 ← INEFFICIENT! C doesn't overlap B ``` **Optimized Approach:** Events that don't overlap each other can share the same stack level. ``` Event A: 09:00 ════════════════════════════ 14:00 Event B: 10:00 ═════ 12:00 Event C: 12:30 ═══ 13:00 Optimized Result: Event A: stackLevel 0 Event B: stackLevel 1 ← Both at level 1 Event C: stackLevel 1 ← because they don't overlap! ``` ### Visual Comparison: The Key Insight **Example Timeline:** ``` Timeline: 09:00 ───────────────────────────────── │ Event A starts │ ┌─────────────────────────────┐ 10:00 │ │ Event A │ │ │ │ │ │ Event B starts │ │ │ ╔═══════════════╗ │ 11:00 │ │ ║ Event B ║ │ │ │ ║ ║ │ 12:00 │ │ ╚═══════════════╝ │ │ │ │ │ │ Event C starts │ │ │ ╔═══════════╗ │ 13:00 │ │ ║ Event C ║ │ │ └───────╚═══════════╝─────────┘ 14:00 ───────────────────────────────── Key Observation: • Event B (10:00-12:00) and Event C (12:30-13:00) do NOT overlap! • They are separated by 30 minutes (12:00 to 12:30) • Both overlap with Event A, but not with each other ``` **Naive Stacking (Wasteful):** ``` Visual Result (Naive - Inefficient): ┌─────────────────────────────────────────────────┐ │ Event A │ │ ┌─────────────────────┐ │ │ │ Event B │ │ │ │ ┌─────────────────────┐ │ │ └─│─────────────────────┘ │ │ │ Event C │ │ │ └─────────────────────┘ │ └─────────────────────────────────────────────────┘ 0px 15px 30px └──┴────┘ Wasted space! Stack Levels: • Event A: stackLevel 0 (marginLeft: 0px) • Event B: stackLevel 1 (marginLeft: 15px) • Event C: stackLevel 2 (marginLeft: 30px) ← UNNECESSARY! Problem: Event C is pushed 30px to the right even though it doesn't conflict with Event B! ``` **Optimized Stacking (Efficient):** ``` Visual Result (Optimized - Efficient): ┌─────────────────────────────────────────────────┐ │ Event A │ │ ┌─────────────────────┐ ┌─────────────────────┐│ │ │ Event B │ │ Event C ││ │ └─────────────────────┘ └─────────────────────┘│ └─────────────────────────────────────────────────┘ 0px 15px 15px └────────────────────┘ Same offset for both! Stack Levels: • Event A: stackLevel 0 (marginLeft: 0px) • Event B: stackLevel 1 (marginLeft: 15px) • Event C: stackLevel 1 (marginLeft: 15px) ← OPTIMIZED! Benefit: Event C reuses stackLevel 1 because Event B has already ended when Event C starts. No visual conflict, saves 15px of horizontal space! ``` **Side-by-Side Comparison:** ``` Naive (3 levels): Optimized (2 levels): A A ├─ B ├─ B │ └─ C └─ C Uses 45px width Uses 30px width (0 + 15 + 30) (0 + 15 + 15) 33% space savings! → ``` ### Algorithm: Greedy Stack Level Assignment The optimized stacking algorithm assigns the lowest available stack level to each event: ```typescript function createOptimizedStackLinks(events: CalendarEvent[]): Map { // Step 1: Sort events by start time const sorted = events.sort((a, b) => a.start - b.start) // Step 2: Track which stack levels are occupied at each time point const stackLinks = new Map() for (const event of sorted) { // Find the lowest available stack level for this event let stackLevel = 0 // Check which levels are occupied by overlapping events const overlapping = sorted.filter(other => other !== event && doEventsOverlap(event, other) ) // Try each level starting from 0 while (true) { const levelOccupied = overlapping.some(other => stackLinks.get(other.id)?.stackLevel === stackLevel ) if (!levelOccupied) { break // Found available level } stackLevel++ // Try next level } // Assign the lowest available level stackLinks.set(event.id, { stackLevel }) } return stackLinks } ``` ### Example Scenarios #### Scenario 1: Three Events, Two Parallel Tracks ``` Input: Event A: 09:00-14:00 (long event) Event B: 10:00-12:00 Event C: 12:30-13:00 Analysis: A overlaps with: B, C B overlaps with: A (not C) C overlaps with: A (not B) Result: Event A: stackLevel 0 (base) Event B: stackLevel 1 (first available) Event C: stackLevel 1 (level 1 is free, B doesn't conflict) ``` #### Scenario 2: Four Events, Three at Same Level ``` Input: Event A: 09:00-15:00 (very long event) Event B: 10:00-11:00 Event C: 11:30-12:30 Event D: 13:00-14:00 Analysis: A overlaps with: B, C, D B, C, D don't overlap with each other Result: Event A: stackLevel 0 Event B: stackLevel 1 Event C: stackLevel 1 (B is done, level 1 free) Event D: stackLevel 1 (B and C are done, level 1 free) ``` #### Scenario 3: Nested Events with Optimization ``` Input: Event A: 09:00-15:00 Event B: 10:00-13:00 Event C: 11:00-12:00 Event D: 12:30-13:30 Analysis: A overlaps with: B, C, D B overlaps with: A, C (not D) C overlaps with: A, B (not D) D overlaps with: A (not B, not C) Result: Event A: stackLevel 0 (base) Event B: stackLevel 1 (overlaps with A) Event C: stackLevel 2 (overlaps with A and B) Event D: stackLevel 2 (overlaps with A only, level 2 is free) ``` ### Stack Links with Optimization **Important:** With optimized stacking, events at the same stack level are NOT linked via prev/next! ```typescript // Traditional chain (naive): Event A: { stackLevel: 0, next: "event-b" } Event B: { stackLevel: 1, prev: "event-a", next: "event-c" } Event C: { stackLevel: 2, prev: "event-b" } // Optimized (B and C at same level, no link between them): Event A: { stackLevel: 0 } Event B: { stackLevel: 1 } // No prev/next Event C: { stackLevel: 1 } // No prev/next ``` ### Benefits of Optimized Stacking ✅ **Space Efficiency:** Reduces horizontal space usage by up to 50% ✅ **Better Readability:** Events are visually closer, easier to see relationships ✅ **Scalability:** Works well with many events in a day ✅ **Performance:** Same O(n²) complexity as naive approach ### Trade-offs ⚠️ **No Single Chain:** Events at the same level aren't linked, making traversal more complex ⚠️ **More Complex Logic:** Requires checking all overlaps, not just sequential ordering ⚠️ **Visual Ambiguity:** Users might wonder why some events are at the same level ## Stack Chain Operations ### Building a Stack Chain (Naive Approach) When events overlap, they form a chain sorted by start time: ```typescript // Input: Events with overlapping times Event A: 09:00-11:00 Event B: 10:00-12:00 Event C: 11:30-13:00 // Step 1: Sort by start time (earliest first) Sorted: [Event A, Event B, Event C] // Step 2: Create links Event A: { stackLevel: 0, next: "event-b" } Event B: { stackLevel: 1, prev: "event-a", next: "event-c" } Event C: { stackLevel: 2, prev: "event-b" } ``` ### Traversing Forward ```typescript // Start at any event currentEvent = Event B; // Get stack link stackLink = currentEvent.dataset.stackLink; // { prev: "event-a", next: "event-c" } // Move to next event nextEventId = stackLink.next; // "event-c" nextEvent = document.querySelector(`[data-event-id="${nextEventId}"]`); ``` ### Traversing Backward ```typescript // Start at any event currentEvent = Event B; // Get stack link stackLink = currentEvent.dataset.stackLink; // { prev: "event-a", next: "event-c" } // Move to previous event prevEventId = stackLink.prev; // "event-a" prevEvent = document.querySelector(`[data-event-id="${prevEventId}"]`); ``` ### Finding Stack Root ```typescript function findStackRoot(event: HTMLElement): HTMLElement { let current = event; let stackLink = getStackLink(current); // Traverse backward until we find an event with no prev link while (stackLink?.prev) { const prevEvent = document.querySelector( `[data-event-id="${stackLink.prev}"]` ); if (!prevEvent) break; current = prevEvent; stackLink = getStackLink(current); } return current; // This is the root (stackLevel 0) } ``` --- ## Use Cases ### 1. Adding a New Event to Existing Stack ``` Existing Stack: Event A (09:00-11:00) - stackLevel 0 Event B (10:00-12:00) - stackLevel 1 New Event: Event C (10:30-11:30) Steps: 1. Detect overlap with Event A and Event B 2. Sort all three by start time: [A, B, C] 3. Rebuild stack links: - Event A: { stackLevel: 0, next: "event-b" } - Event B: { stackLevel: 1, prev: "event-a", next: "event-c" } - Event C: { stackLevel: 2, prev: "event-b" } 4. Apply visual styling ``` ### 2. Removing Event from Middle of Stack ``` Before: Event A (stackLevel 0) ─→ Event B (stackLevel 1) ─→ Event C (stackLevel 2) Remove Event B: After: Event A (stackLevel 0) ─→ Event C (stackLevel 1) Steps: 1. Get Event B's stack link: { prev: "event-a", next: "event-c" } 2. Update Event A's next: "event-c" 3. Update Event C's prev: "event-a" 4. Update Event C's stackLevel: 1 (was 2) 5. Recalculate Event C's marginLeft: 15px (was 30px) 6. Remove Event B's stack link ``` ### 3. Moving Event to Different Time ``` Before (events overlap): Event A (09:00-11:00) - stackLevel 0 Event B (10:00-12:00) - stackLevel 1 Move Event B to 14:00-16:00 (no longer overlaps): After: Event A (09:00-11:00) - NO STACK LINK (standalone) Event B (14:00-16:00) - NO STACK LINK (standalone) Steps: 1. Detect that Event B no longer overlaps Event A 2. Remove Event B from stack chain 3. Clear Event A's next link 4. Clear Event B's stack link entirely 5. Reset both events' marginLeft to 0px ``` --- ## Edge Cases ### Case 1: Single Event (No Overlap) ``` Event A: 09:00-10:00 (alone in time slot) Stack Link: NONE (no data-stack-link attribute) Visual: marginLeft = 0px, zIndex = default ``` ### Case 2: Two Events, Same Start Time ``` Event A: 10:00-11:00 Event B: 10:00-12:00 (same start, different end) Sort by: start time first, then by end time (shortest first) Result: Event A (stackLevel 0), Event B (stackLevel 1) ``` ### Case 3: Multiple Separate Chains in Same Column ``` Chain 1: Event A (09:00-10:00) - stackLevel 0 Event B (09:30-10:30) - stackLevel 1 Chain 2: Event C (14:00-15:00) - stackLevel 0 Event D (14:30-15:30) - stackLevel 1 Note: Two independent chains, each with their own root at stackLevel 0 ``` ### Case 4: Complete Containment ``` Event A: 09:00-13:00 (large event) Event B: 10:00-11:00 (inside A) Event C: 11:30-12:30 (inside A) All three overlap, so they form one chain: Event A - stackLevel 0 Event B - stackLevel 1 Event C - stackLevel 2 ``` --- ## Algorithm Pseudocode ### Creating Stack for New Event ``` function createStackForNewEvent(newEvent, columnEvents): // Step 1: Find overlapping events overlapping = columnEvents.filter(event => doEventsOverlap(newEvent, event) ) if overlapping is empty: // No stack needed return null // Step 2: Combine and sort by start time allEvents = [...overlapping, newEvent] allEvents.sort((a, b) => a.start - b.start) // Step 3: Create stack links stackLinks = new Map() for (i = 0; i < allEvents.length; i++): link = { stackLevel: i, prev: i > 0 ? allEvents[i-1].id : undefined, next: i < allEvents.length-1 ? allEvents[i+1].id : undefined } stackLinks.set(allEvents[i].id, link) // Step 4: Apply to DOM for each event in allEvents: element = findElementById(event.id) element.dataset.stackLink = JSON.stringify(stackLinks.get(event.id)) element.style.marginLeft = stackLinks.get(event.id).stackLevel * 15 + 'px' element.style.zIndex = 100 + stackLinks.get(event.id).stackLevel return stackLinks ``` ### Removing Event from Stack ``` function removeEventFromStack(eventId): element = findElementById(eventId) stackLink = JSON.parse(element.dataset.stackLink) if not stackLink: return // Not in a stack // Update previous element if stackLink.prev: prevElement = findElementById(stackLink.prev) prevLink = JSON.parse(prevElement.dataset.stackLink) prevLink.next = stackLink.next prevElement.dataset.stackLink = JSON.stringify(prevLink) // Update next element if stackLink.next: nextElement = findElementById(stackLink.next) nextLink = JSON.parse(nextElement.dataset.stackLink) nextLink.prev = stackLink.prev // Shift down stack level nextLink.stackLevel = nextLink.stackLevel - 1 nextElement.dataset.stackLink = JSON.stringify(nextLink) // Update visual styling nextElement.style.marginLeft = nextLink.stackLevel * 15 + 'px' nextElement.style.zIndex = 100 + nextLink.stackLevel // Cascade update to all subsequent events updateSubsequentStackLevels(nextElement, -1) // Clear removed element's stack link delete element.dataset.stackLink element.style.marginLeft = '0px' ``` --- ## Performance Considerations ### Time Complexity - **Overlap Detection:** O(n) where n = number of events in column - **Stack Creation:** O(n log n) due to sorting - **Chain Traversal:** O(n) worst case (entire chain) - **Stack Removal:** O(n) worst case (update all subsequent) ### Space Complexity - **Stack Links:** O(1) per event (stored in DOM attribute) - **No Global State:** All state is in DOM ### Optimization Tips 1. **Batch Updates:** When adding multiple events, batch DOM updates 2. **Lazy Evaluation:** Only recalculate stacks when events change 3. **Event Delegation:** Use event delegation instead of per-element listeners 4. **Virtual Scrolling:** For large calendars, only render visible events --- ## Implementation Guidelines ### Separation of Concerns **Pure Logic (No DOM):** - Overlap detection algorithms - Stack link calculation - Sorting logic **DOM Manipulation:** - Applying stack links to elements - Updating visual styles - Chain traversal **Event Handling:** - Detecting event changes - Triggering stack recalculation - Cleanup on event removal ### Testing Strategy 1. **Unit Tests:** Test overlap detection in isolation 2. **Integration Tests:** Test stack creation with DOM 3. **Visual Tests:** Test CSS styling calculations 4. **Edge Cases:** Test boundary conditions --- ## Future Enhancements ### Potential Improvements 1. **Smart Stacking:** Detect non-overlapping sub-groups and stack independently 2. **Column Sharing:** For events with similar start times, use flexbox columns 3. **Compact Mode:** Reduce stack offset for dense calendars 4. **Color Coding:** Visual indication of stack depth 5. **Stack Preview:** Hover to highlight entire stack chain --- ## Glossary - **Stack:** Group of overlapping events displayed with horizontal offset - **Stack Link:** Data structure connecting events in a stack (doubly-linked list) - **Stack Level:** Position in stack (0 = base, 1+ = offset) - **Stack Root:** First event in stack (stackLevel 0, no prev link) - **Stack Chain:** Complete sequence of linked events - **Overlap:** Two events with intersecting time ranges - **Offset:** Horizontal margin applied to stacked events (15px per level) --- **Document Version:** 1.0 **Last Updated:** 2025-10-04 **Status:** Conceptual Documentation - Ready for TDD Implementation