Implements advanced event stacking and grid layout
Introduces a 3-phase algorithm in `EventStackManager` for dynamic event positioning. Groups events by start time proximity to determine optimal layout. Optimizes horizontal space by using side-by-side grid columns for simultaneous events and allowing non-overlapping events to share stack levels. Supports nested stacking for late-arriving events within grid columns. Includes comprehensive documentation (`STACKING_CONCEPT.md`) and a visual demonstration (`stacking-visualization.html`) to explain the new layout logic. Updates event rendering to utilize the new manager and adds extensive test coverage.
This commit is contained in:
parent
57bf122675
commit
2f58ceccd4
8 changed files with 4509 additions and 14 deletions
772
STACKING_CONCEPT.md
Normal file
772
STACKING_CONCEPT.md
Normal file
|
|
@ -0,0 +1,772 @@
|
|||
# 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
|
||||
<swp-event
|
||||
data-event-id="event-1"
|
||||
data-stack-link='{"stackLevel":0,"next":"event-2"}'>
|
||||
</swp-event>
|
||||
|
||||
<swp-event
|
||||
data-event-id="event-2"
|
||||
data-stack-link='{"stackLevel":1,"prev":"event-1","next":"event-3"}'>
|
||||
</swp-event>
|
||||
|
||||
<swp-event
|
||||
data-event-id="event-3"
|
||||
data-stack-link='{"stackLevel":2,"prev":"event-2"}'>
|
||||
</swp-event>
|
||||
```
|
||||
|
||||
### 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<string, StackLink> {
|
||||
// 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<string, StackLink>()
|
||||
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue