Cleanup test files and move to another folder

This commit is contained in:
Janus C. H. Knudsen 2025-11-05 00:07:19 +01:00
parent 8456d8aa28
commit 9c765b35ab
28 changed files with 0 additions and 1981 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,217 +0,0 @@
De 6 vigtigste fund (med fixes)
Gruppering kan “brygge” mellem to grupper uden at merge dem
groupEventsByStartTime finder første eksisterende gruppe med konflikt og lægger eventet deri. Hvis et nyt event konflikter med flere grupper, bliver grupperne ikke merged → inkonsistente “grid”-klumper. Løs: merge alle matchende grupper eller brug union-find/sweep-line konfliktsæt.
EventStackManager
ContainerType er “GRID” for alle grupper >1 — også ved dybe overlaps
decideContainerType returnerer altid 'GRID' når events.length > 1. Det kan være tilsigtet, men så skal du være tryg ved, at lange overlappende events, der kun næsten starter samtidigt, stadig pakkes i kolonner fremfor “stacking”. Overvej: GRID kun når samtidighed er vigtigere end varighed, ellers fald tilbage til STACKING.
EventStackManager
Stack level-algoritmen kan eskalere niveauer unødigt
createOptimizedStackLinks sætter stackLevel = max(overlappende tidligere) + 1. Det er mere “stak-tårn” end “før-ledig-kolonne” og giver højere niveauer end nødvendigt (ikke minimal farvelægning). Løs: interval partitioning med min-heap (giver laveste ledige level).
EventStackManager
Grid-top beregnes fra ét event, men børn positioneres relativt til containerStart
I koordinatoren bruges earliestEvent til top, og renderer bruger earliestEvent.start som containerStart. Det er ok — men sørg for, at earliestEvent garanteret er det tidligste i gruppen og sortér eksplicit inden brug (robusthed mod fremtidige ændringer).
EventLayoutCoordinator
EventRenderer
Drag bruger rå new Date(...) i stedet for DateService
Kan give TZ/DST-glitches. Brug samme parse/logik som resten.
EventRenderer
Ingen reflow af kolonne efter drop
handleDragEnd normaliserer DOM men recalculerer ikke layout → forkert stacking/margin efter flyt. Kald din kolonne-pipeline igen for den berørte kolonne.
EventRenderer
Bonus: getEventsForColumn matcher kun start-dato === kolonnedato; events der krydser midnat forsvinder. Overvej interval-overlap mod døgnets [00:0023:59:59.999].
EventRenderer
Målrettede patches (små og sikre)
A) Merge grupper når et event rammer flere (EventStackManager)
Erstat den nuværende “find første gruppe”-logik med merge af alle matchende:
// inde i groupEventsByStartTime
const matches: number[] = [];
for (let gi = 0; gi < groups.length; gi++) {
const group = groups[gi];
const conflict = group.events.some(ge => {
const s2s = Math.abs(event.start.getTime() - ge.start.getTime()) / 60000;
if (s2s <= thresholdMinutes) return true;
const e2s = (ge.end.getTime() - event.start.getTime()) / 60000;
if (e2s > 0 && e2s <= thresholdMinutes) return true;
const rev = (event.end.getTime() - ge.start.getTime()) / 60000;
if (rev > 0 && rev <= thresholdMinutes) return true;
return false;
});
if (conflict) matches.push(gi);
}
if (matches.length === 0) {
groups.push({ events: [event], containerType: 'NONE', startTime: event.start });
} else {
// merge alle matchende grupper + dette event
const base = matches[0];
groups[base].events.push(event);
for (let i = matches.length - 1; i >= 1; i--) {
const idx = matches[i];
groups[base].events.push(...groups[idx].events);
groups.splice(idx, 1);
}
// opdatér startTime til min start
groups[base].startTime = new Date(
Math.min(...groups[base].events.map(e => e.start.getTime()))
);
}
Nu undgår du “brobygning” der splitter reelt sammenhængende grupper.
EventStackManager
B) Minimal stack level med min-heap (EventStackManager)
Udskift level-tildeling med klassisk interval partitioning:
public createOptimizedStackLinks(events: CalendarEvent[]): Map<string, StackLink> {
const res = new Map<string, StackLink>();
if (!events.length) return res;
const sorted = [...events].sort((a,b)=> a.start.getTime() - b.start.getTime());
type Col = { level: number; end: number };
const cols: Col[] = []; // min-heap på end
const push = (c: Col) => { cols.push(c); cols.sort((x,y)=> x.end - y.end); };
for (const ev of sorted) {
const t = ev.start.getTime();
// find første kolonne der er fri
let placed = false;
for (let i = 0; i < cols.length; i++) {
if (cols[i].end <= t) { cols[i].end = ev.end.getTime(); res.set(ev.id, { stackLevel: cols[i].level }); placed = true; break; }
}
if (!placed) { const level = cols.length; push({ level, end: ev.end.getTime() }); res.set(ev.id, { stackLevel: level }); }
}
// evt. byg prev/next separat hvis nødvendigt
return res;
}
Dette giver laveste ledige niveau og undgår “trappetårne”.
EventStackManager
C) Konsolidér margin/zIndex + brug DateService i drag (EventRenderer)
Lad StackManager styre marginLeft konsekvent (og undgå magic numbers):
// renderGridGroup
groupElement.style.top = `${gridGroup.position.top}px`;
this.stackManager.applyVisualStyling(groupElement, gridGroup.stackLevel); // i stedet for *15
this.stackManager.applyStackLinkToElement(groupElement, { stackLevel: gridGroup.stackLevel });
EventRenderer
Brug DateService i drag:
public handleDragMove(payload: DragMoveEventPayload): void {
if (!this.draggedClone || !payload.columnBounds) return;
const swp = this.draggedClone as SwpEventElement;
const colDate = this.dateService.parseISODate?.(payload.columnBounds.date) ?? new Date(payload.columnBounds.date);
swp.updatePosition(colDate, payload.snappedY);
}
public handleColumnChange(e: DragColumnChangeEventPayload): void {
if (!this.draggedClone) return;
const layer = e.newColumn.element.querySelector('swp-events-layer');
if (layer && this.draggedClone.parentElement !== layer) {
layer.appendChild(this.draggedClone);
const currentTop = parseFloat(this.draggedClone.style.top) || 0;
const swp = this.draggedClone as SwpEventElement;
const colDate = this.dateService.parseISODate?.(e.newColumn.date) ?? new Date(e.newColumn.date);
swp.updatePosition(colDate, currentTop);
}
}
EventRenderer
D) Reflow efter drop (EventRenderer)
Genberegn layout for den berørte kolonne:
public handleDragEnd(id: string, original: HTMLElement, clone: HTMLElement, finalColumn: ColumnBounds): void {
if (!clone || !original) { console.warn('Missing clone/original'); return; }
this.fadeOutAndRemove(original);
const cid = clone.dataset.eventId;
if (cid && cid.startsWith('clone-')) clone.dataset.eventId = cid.replace('clone-','');
clone.classList.remove('dragging');
const layer = finalColumn.element.querySelector('swp-events-layer') as HTMLElement | null;
if (layer) {
// 1) Hent kolonnens events fra din model/state (inkl. opdateret event)
const columnEvents: CalendarEvent[] = /* ... */;
// 2) Ryd
layer.querySelectorAll('swp-event, swp-event-group').forEach(el => el.remove());
// 3) Render igen via layout
this.renderColumnEvents(columnEvents, layer);
}
this.draggedClone = null;
this.originalEvent = null;
}
EventRenderer
E) Døgn-overlap i kolonnefilter (EventRenderer)
Hvis ønsket (ellers behold din nuværende):
protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] {
const d = column.dataset.date; if (!d) return [];
const start = this.dateService.parseISODate(`${d}T00:00:00`);
const end = this.dateService.parseISODate(`${d}T23:59:59.999`);
return events.filter(ev => ev.start < end && ev.end > start);
}
EventRenderer
F) Eksplicit “earliest” i GRID (Coordinator)
Gør det robust i tilfælde af usorteret input:
const earliestEvent = [...gridCandidates].sort((a,b)=> a.start.getTime()-b.start.getTime())[0];
const pos = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
EventLayoutCoordinator
Mini-noter
allocateColumns er O(n²); det er fint for typiske dagvisninger. Hvis I ser >100 events/kolonne, kan I optimere med sweep-line + min-heap.
EventLayoutCoordinator
Overvej at lade koordinatoren returnere rene layout-maps (id → {level, z, margin}) og holde DOM-påføring 100% i renderer — det gør DnD-”reflow” enklere at teste.
EventLayoutCoordinator
EventRenderer

237
CLAUDE.md
View file

@ -1,237 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build and Development Commands
### TypeScript Build
- **Build:** `npm run build` - Uses esbuild with NovaDI plugin to bundle to `wwwroot/js/calendar.js`
- **Watch:** `npm run watch` - Auto-rebuild on file changes
- **Clean:** `npm run clean` - Remove compiled output
### Testing
- **Run tests:** `npm test` or `vitest` - Interactive watch mode
- **Run once:** `npm run test:run` or `vitest run`
- **Test UI:** `npm run test:ui` - Visual test interface
- Tests use Vitest with jsdom environment (see `vitest.config.ts`)
### CSS Build
- **Build CSS:** `npm run css:build` - PostCSS with nesting support
- **Watch CSS:** `npm run css:watch` - Auto-rebuild CSS on changes
- **Production CSS:** `npm run css:build:prod` - Minified with PurgeCSS
- **Analyze CSS:** `npm run css:analyze` - CSS statistics and analysis
### Server
- **Start:** `dotnet run` - ASP.NET Core Kestrel server on `http://localhost:8000`
## Architecture Overview
### Core Architectural Pattern
This is a **manager-based, event-driven calendar application** using pure TypeScript with no UI frameworks. Communication happens exclusively through DOM CustomEvents via a central EventBus.
**Key Principles:**
- **No global state** - State lives in managers
- **Event-driven** - All inter-component communication via CustomEvents (see `CoreEvents` constants)
- **Dependency Injection** - Uses `@novadi/core` DI container
- **Pure DOM** - No React/Vue/Angular, just vanilla TypeScript + DOM manipulation
### Dependency Injection Flow
The application initializes in `src/index.ts` following this sequence:
1. **CalendarConfig.initialize()** - Static config from DOM attributes (`<swp-calendar>`)
2. **Container setup** - Register all services, managers, renderers, utilities
3. **Manager initialization** - CalendarManager coordinates all other managers
4. **Deep linking** - Handle URL-based event navigation
All dependencies are auto-wired using NovaDI's `@inject` decorators (configured in `build.js`).
### Event System
**EventBus** (`src/core/EventBus.ts`) wraps DOM CustomEvents with debugging/logging:
```typescript
// Emit
eventBus.emit('view:changed', { view: 'week', date: new Date() });
// Listen
eventBus.on('view:changed', (event: CustomEvent) => {
const { view, date } = event.detail;
});
```
**Core events** are defined in `src/constants/CoreEvents.ts` (~20 essential events organized by category: lifecycle, view, navigation, data, grid, event management, system, filter, rendering).
### Manager Architecture
Managers are the core organizational units. Each has a specific responsibility:
**Primary Managers:**
- `CalendarManager` - Main coordinator, initializes all managers
- `ViewManager` - Handles view switching (day/week/month)
- `NavigationManager` - Prev/next/today navigation, date changes
- `EventManager` - Event CRUD operations, selection, lifecycle
- `GridManager` - Calendar grid structure and layout
- `HeaderManager` - Date headers and column rendering
- `AllDayManager` - All-day event section management
**Interaction Managers:**
- `DragDropManager` - Event drag-and-drop functionality
- `ResizeHandleManager` - Event resize handles
- `DragHoverManager` - Visual feedback during drag operations
- `EdgeScrollManager` - Auto-scroll when dragging near edges
- `ScrollManager` - Grid scroll behavior
**Support Managers:**
- `ConfigManager` - Event-driven config updates (wraps CalendarConfig) and manages CSS custom properties
- `EventLayoutCoordinator` - Coordinates event positioning
- `EventStackManager` - Handles overlapping events
- `EventFilterManager` - Filter events by criteria
- `WorkHoursManager` - Work hours highlighting
### Renderer Architecture
Renderers handle DOM creation and updates (separation of concerns from managers):
- `EventRenderingService` - Main event rendering coordinator
- `DateEventRenderer` / `AllDayEventRenderer` - Event DOM generation
- `DateHeaderRenderer` - Date header rendering
- `DateColumnRenderer` - Column structure
- `GridRenderer` - Grid structure and time slots
- `NavigationRenderer` - Navigation controls
### Core Services
**CalendarConfig** (`src/core/CalendarConfig.ts`):
- Static configuration class
- Loads settings from DOM data attributes on `<swp-calendar>` element
- Provides computed values (hourHeight, snapInterval, totalSlots, etc.)
- ConfigManager wraps it for event-driven updates and automatically syncs CSS custom properties to the DOM
**DateService** (`src/utils/DateService.ts`):
- Uses `date-fns` and `date-fns-tz` for date calculations
- Default timezone: `Europe/Copenhagen`, locale: `da-DK`
**TimeFormatter** (`src/utils/TimeFormatter.ts`):
- Consistent time/date formatting across the app
- Configured via CalendarConfig
**PositionUtils** (`src/utils/PositionUtils.ts`):
- Convert between pixels and times
- Snap-to-grid calculations
**URLManager** (`src/utils/URLManager.ts`):
- Deep linking to events
- Parses `eventId` from URL
### Repository Pattern
Event data is accessed through `IEventRepository` interface:
- `MockEventRepository` - Current implementation using mock data from `wwwroot/data/mock-events.json`
- Ready for API implementation swap
## Code Organization
```
src/
├── constants/ # CoreEvents and other constants
├── core/ # EventBus, CalendarConfig (core infrastructure)
├── data/ # Data models and utilities
├── elements/ # Custom HTML elements (if any)
├── managers/ # Manager classes (business logic)
├── renderers/ # DOM rendering logic
├── repositories/ # Data access layer (IEventRepository, MockEventRepository)
├── types/ # TypeScript interfaces and types
├── utils/ # Utility functions (DateService, PositionUtils, etc.)
└── index.ts # Application entry point and DI setup
```
## Important Patterns
### Adding a New Manager
1. Create in `src/managers/YourManager.ts`
2. Use `@inject` for dependencies
3. Implement optional `initialize()` method if needed
4. Register in `src/index.ts` DI container
5. Listen to events via `eventBus.on()` (injected as `IEventBus`)
6. Emit events via `eventBus.emit()`
### Event Naming Convention
Events follow `category:action` pattern:
- `view:changed`, `view:rendered`
- `nav:date-changed`, `nav:navigation-completed`
- `data:loaded`, `data:error`
- `event:created`, `event:updated`, `event:deleted`
- `grid:rendered`, `grid:clicked`
### Grid Positioning
Events are positioned using CSS Grid and absolute positioning:
- Time slots are calculated via `CalendarConfig.slotHeight` and `minuteHeight`
- `PositionUtils` handles pixel ↔ time conversions
- Snap-to-grid uses `CalendarConfig.getGridSettings().snapInterval`
### Work Week Configuration
CalendarConfig supports work week presets:
- `standard` - Mon-Fri (default)
- `compressed` - Mon-Thu
- `midweek` - Wed-Fri
- `weekend` - Sat-Sun
- `fullweek` - Mon-Sun
Change via `CalendarConfig.setWorkWeek('preset-id')`
## Testing
Tests are written using Vitest with jsdom. Setup file: `test/setup.ts`
Run individual test file:
```bash
vitest run path/to/test-file.test.ts
```
## CSS Architecture
CSS is modular and built with PostCSS:
- **Source:** `wwwroot/css/src/` (uses PostCSS nesting)
- **Output:** `wwwroot/css/`
- **Main file:** `calendar.css` (currently used)
Planned modular CSS files:
- `calendar-base-css.css` - Variables and base styles
- `calendar-components-css.css` - UI components
- `calendar-events-css.css` - Event styling
- `calendar-layout-css.css` - Grid layout
- `calendar-popup-css.css` - Modals and popups
## Debugging
Enable EventBus debug mode (already enabled in `src/index.ts`):
```typescript
eventBus.setDebug(true);
```
Access debug interface in browser console:
```javascript
window.calendarDebug.eventBus.getEventLog(); // All events
window.calendarDebug.eventManager; // Access EventManager
window.calendarDebug.calendarManager; // Access CalendarManager
```
## Configuration via HTML
Set calendar options via data attributes on `<swp-calendar>`:
```html
<swp-calendar
data-view="week"
data-week-days="7"
data-snap-interval="15"
data-day-start-hour="6"
data-day-end-hour="22"
data-hour-height="60"
data-fit-to-width="false">
</swp-calendar>
```

View file

@ -1,578 +0,0 @@
# Cyclomatic Complexity Analysis Report
**Calendar Plantempus Project**
Generated: 2025-10-04
---
## Executive Summary
This report analyzes the cyclomatic complexity of the Calendar Plantempus TypeScript codebase, focusing on identifying methods that exceed recommended complexity thresholds and require refactoring.
### Key Metrics
| Metric | Value |
|--------|-------|
| **Total Files Analyzed** | 6 |
| **Total Methods Analyzed** | 74 |
| **Methods with Complexity >10** | 4 (5.4%) |
| **Methods with Complexity 6-10** | 5 (6.8%) |
| **Methods with Complexity 1-5** | 65 (87.8%) |
### Complexity Distribution
```
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ Low (1-5): 87.8%
■■■ Medium (6-10): 6.8%
■ High (>10): 5.4%
```
### Overall Assessment
✅ **Strengths:**
- 87.8% of methods have acceptable complexity
- Web Components (SwpEventElement) demonstrate excellent design
- Rendering services show clean separation of concerns
🔴 **Critical Issues:**
- 4 methods exceed complexity threshold of 10
- Stack management logic is overly complex (complexity 18!)
- Drag & drop handlers need refactoring
---
## Detailed File Analysis
### 1. DragDropManager.ts
**File:** `src/managers/DragDropManager.ts`
**Overall Complexity:** HIGH ⚠️
| Method | Lines | Complexity | Status | Notes |
|--------|-------|------------|--------|-------|
| `init()` | 88-133 | 7 | 🟡 Medium | Event listener setup could be extracted |
| `handleMouseDown()` | 135-168 | 5 | ✅ OK | Acceptable complexity |
| `handleMouseMove()` | 173-260 | **15** | 🔴 **Critical** | **NEEDS IMMEDIATE REFACTORING** |
| `handleMouseUp()` | 265-310 | 4 | ✅ OK | Clean implementation |
| `cleanupAllClones()` | 312-320 | 2 | ✅ OK | Simple utility method |
| `cancelDrag()` | 325-350 | 3 | ✅ OK | Straightforward cleanup |
| `calculateDragPosition()` | 355-364 | 2 | ✅ OK | Simple calculation |
| `calculateSnapPosition()` | 369-377 | 1 | ✅ OK | Base complexity |
| `checkAutoScroll()` | 383-403 | 5 | ✅ OK | Could be simplified slightly |
| `startAutoScroll()` | 408-444 | 6 | 🟡 Medium | Autoscroll logic could be extracted |
| `stopAutoScroll()` | 449-454 | 2 | ✅ OK | Simple cleanup |
| `detectDropTarget()` | 468-483 | 4 | ✅ OK | Clear DOM traversal |
| `handleHeaderMouseEnter()` | 488-516 | 4 | ✅ OK | Clean event handling |
| `handleHeaderMouseLeave()` | 521-544 | 4 | ✅ OK | Clean event handling |
**Decision Points in handleMouseMove():**
1. `if (event.buttons === 1)` - Check if mouse button is pressed
2. `if (!this.isDragStarted && this.draggedElement)` - Check for drag initialization
3. `if (totalMovement >= this.dragThreshold)` - Movement threshold check
4. `if (this.isDragStarted && this.draggedElement && this.draggedClone)` - Drag state validation
5. `if (!this.draggedElement.hasAttribute("data-allday"))` - Event type check
6. `if (deltaY >= this.snapDistancePx)` - Snap interval check
7. Multiple autoscroll conditionals
8. `if (newColumn == null)` - Column validation
9. `if (newColumn?.index !== this.currentColumnBounds?.index)` - Column change detection
**Recommendation for handleMouseMove():**
```typescript
// Current: 88 lines, complexity 15
// Suggested refactoring:
private handleMouseMove(event: MouseEvent): void {
this.updateMousePosition(event);
if (!this.isMouseButtonPressed(event)) return;
if (this.shouldStartDrag()) {
this.initializeDrag();
}
if (this.isDragActive()) {
this.updateDragPosition();
this.handleColumnChange();
}
}
// Extract methods with complexity 2-4 each:
// - initializeDrag()
// - updateDragPosition()
// - handleColumnChange()
```
---
### 2. SwpEventElement.ts
**File:** `src/elements/SwpEventElement.ts`
**Overall Complexity:** LOW ✅
| Method | Lines | Complexity | Status | Notes |
|--------|-------|------------|--------|-------|
| `connectedCallback()` | 84-89 | 2 | ✅ OK | Simple initialization |
| `attributeChangedCallback()` | 94-98 | 2 | ✅ OK | Clean attribute handling |
| `updatePosition()` | 109-128 | 2 | ✅ OK | Straightforward update logic |
| `createClone()` | 133-152 | 2 | ✅ OK | Simple cloning |
| `render()` | 161-171 | 1 | ✅ OK | Base complexity |
| `updateDisplay()` | 176-194 | 3 | ✅ OK | Clean DOM updates |
| `applyPositioning()` | 199-205 | 1 | ✅ OK | Delegates to PositionUtils |
| `calculateTimesFromPosition()` | 210-230 | 1 | ✅ OK | Simple calculation |
| `fromCalendarEvent()` (static) | 239-252 | 1 | ✅ OK | Factory method |
| `extractCalendarEventFromElement()` (static) | 257-270 | 1 | ✅ OK | Clean extraction |
| `fromAllDayElement()` (static) | 275-311 | 4 | ✅ OK | Acceptable conversion logic |
| `SwpAllDayEventElement.connectedCallback()` | 319-323 | 2 | ✅ OK | Simple setup |
| `SwpAllDayEventElement.createClone()` | 328-335 | 1 | ✅ OK | Base complexity |
| `SwpAllDayEventElement.applyGridPositioning()` | 340-343 | 1 | ✅ OK | Simple positioning |
| `SwpAllDayEventElement.fromCalendarEvent()` (static) | 348-362 | 1 | ✅ OK | Factory method |
**Best Practices Demonstrated:**
- ✅ Clear separation of concerns
- ✅ Factory methods for object creation
- ✅ Delegation to utility classes (PositionUtils, DateService)
- ✅ BaseSwpEventElement abstraction reduces duplication
- ✅ All methods stay within complexity threshold
**This file serves as a model for good design in the codebase.**
---
### 3. SimpleEventOverlapManager.ts
**File:** `src/managers/SimpleEventOverlapManager.ts`
**Overall Complexity:** HIGH ⚠️
| Method | Lines | Complexity | Status | Notes |
|--------|-------|------------|--------|-------|
| `resolveOverlapType()` | 33-58 | 4 | ✅ OK | Clear overlap detection |
| `groupOverlappingElements()` | 64-84 | 4 | ✅ OK | Acceptable grouping logic |
| `createEventGroup()` | 89-92 | 1 | ✅ OK | Simple factory |
| `addToEventGroup()` | 97-113 | 2 | ✅ OK | Straightforward addition |
| `createStackedEvent()` | 118-165 | 7 | 🟡 Medium | Chain traversal could be extracted |
| `removeStackedStyling()` | 170-284 | **18** | 🔴 **Critical** | **MOST COMPLEX METHOD IN CODEBASE** |
| `updateSubsequentStackLevels()` | 289-313 | 5 | ✅ OK | Could be simplified |
| `isStackedEvent()` | 318-324 | 3 | ✅ OK | Simple boolean check |
| `removeFromEventGroup()` | 329-364 | 6 | 🟡 Medium | Remaining event handling complex |
| `restackEventsInContainer()` | 369-432 | **11** | 🔴 **High** | **NEEDS REFACTORING** |
| `getEventGroup()` | 438-440 | 1 | ✅ OK | Simple utility |
| `isInEventGroup()` | 442-444 | 1 | ✅ OK | Simple utility |
| `getStackLink()` | 449-459 | 3 | ✅ OK | JSON parsing with error handling |
| `setStackLink()` | 461-467 | 2 | ✅ OK | Simple setter |
| `findElementById()` | 469-471 | 1 | ✅ OK | Base complexity |
**Critical Issue: removeStackedStyling() - Complexity 18**
**Decision Points Breakdown:**
1. `if (link)` - Check if element has stack link
2. `if (link.prev && link.next)` - Middle element in chain
3. `if (prevElement && nextElement)` - Both neighbors exist
4. `if (!actuallyOverlap)` - Chain breaking decision (CRITICAL BRANCH)
5. `if (nextLink?.next)` - Subsequent elements exist
6. `while (subsequentId)` - Loop through chain
7. `if (!subsequentElement)` - Element validation
8. `else` - Normal stacking (chain maintenance)
9. `else if (link.prev)` - Last element case
10. `if (prevElement)` - Previous element exists
11. `else if (link.next)` - First element case
12. `if (nextElement)` - Next element exists
13. `if (link.prev && link.next)` - Middle element check (duplicate)
14. `if (nextLink && nextLink.next)` - Chain continuation
15. `else` - Chain was broken
16-18. Additional nested conditions
**Recommendation for removeStackedStyling():**
```typescript
// Current: 115 lines, complexity 18
// Suggested refactoring:
public removeStackedStyling(eventElement: HTMLElement): void {
this.clearVisualStyling(eventElement);
const link = this.getStackLink(eventElement);
if (!link) return;
// Delegate to specialized methods based on position in chain
if (link.prev && link.next) {
this.removeMiddleElementFromChain(eventElement, link);
} else if (link.prev) {
this.removeLastElementFromChain(eventElement, link);
} else if (link.next) {
this.removeFirstElementFromChain(eventElement, link);
}
this.setStackLink(eventElement, null);
}
// Extract to separate methods:
// - clearVisualStyling() - complexity 1
// - removeMiddleElementFromChain() - complexity 5-6
// - removeLastElementFromChain() - complexity 3
// - removeFirstElementFromChain() - complexity 3
// - breakStackChain() - complexity 4
// - maintainStackChain() - complexity 4
```
**Critical Issue: restackEventsInContainer() - Complexity 11**
**Decision Points:**
1. `if (stackedEvents.length === 0)` - Early return
2. `for (const element of stackedEvents)` - Iterate events
3. `if (!eventId || processedEventIds.has(eventId))` - Validation
4. `while (rootLink?.prev)` - Find root of chain
5. `if (!prevElement)` - Break condition
6. `while (currentElement)` - Traverse chain
7. `if (!currentLink?.next)` - End of chain
8. `if (!nextElement)` - Break condition
9. `if (chain.length > 1)` - Only add multi-element chains
10. `forEach` - Restack each chain
11. `if (link)` - Update link data
**Recommendation for restackEventsInContainer():**
```typescript
// Current: 64 lines, complexity 11
// Suggested refactoring:
public restackEventsInContainer(container: HTMLElement): void {
const stackedEvents = this.getStackedEvents(container);
if (stackedEvents.length === 0) return;
const stackChains = this.collectStackChains(stackedEvents);
stackChains.forEach(chain => this.reapplyStackStyling(chain));
}
// Extract to separate methods:
// - getStackedEvents() - complexity 2
// - collectStackChains() - complexity 6
// - findStackRoot() - complexity 3
// - traverseChain() - complexity 3
// - reapplyStackStyling() - complexity 2
```
---
### 4. EventRendererManager.ts
**File:** `src/renderers/EventRendererManager.ts`
**Overall Complexity:** MEDIUM 🟡
| Method | Lines | Complexity | Status | Notes |
|--------|-------|------------|--------|-------|
| `renderEvents()` | 35-68 | 3 | ✅ OK | Clean rendering logic |
| `setupEventListeners()` | 70-95 | 1 | ✅ OK | Simple delegation |
| `handleGridRendered()` | 101-127 | 5 | ✅ OK | Could reduce conditionals |
| `handleViewChanged()` | 133-138 | 1 | ✅ OK | Simple cleanup |
| `setupDragEventListeners()` | 144-238 | **10** | 🔴 **High** | **NEEDS REFACTORING** |
| `handleConvertToTimeEvent()` | 243-292 | 4 | ✅ OK | Acceptable conversion logic |
| `clearEvents()` | 294-296 | 1 | ✅ OK | Delegates to strategy |
| `refresh()` | 298-300 | 1 | ✅ OK | Simple refresh |
**Issue: setupDragEventListeners() - Complexity 10**
**Decision Points:**
1. `if (hasAttribute('data-allday'))` - Filter all-day events
2. `if (draggedElement && strategy.handleDragStart && columnBounds)` - Validation
3. `if (hasAttribute('data-allday'))` - Filter check
4. `if (strategy.handleDragMove)` - Strategy check
5. `if (strategy.handleDragAutoScroll)` - Strategy check
6. `if (target === 'swp-day-column' && finalColumn)` - Drop target validation
7. `if (draggedElement && draggedClone && strategy.handleDragEnd)` - Validation
8. `if (dayEventClone)` - Cleanup check
9. `if (hasAttribute('data-allday'))` - Filter check
10. `if (strategy.handleColumnChange)` - Strategy check
**Recommendation:**
```typescript
// Current: 95 lines, complexity 10
// Suggested refactoring:
private setupDragEventListeners(): void {
this.setupDragStartListener();
this.setupDragMoveListener();
this.setupDragEndListener();
this.setupDragAutoScrollListener();
this.setupColumnChangeListener();
this.setupConversionListener();
this.setupNavigationListener();
}
// Each listener method: complexity 2-3
```
---
### 5. EventRenderer.ts
**File:** `src/renderers/EventRenderer.ts`
**Overall Complexity:** LOW ✅
| Method | Lines | Complexity | Status | Notes |
|--------|-------|------------|--------|-------|
| `handleDragStart()` | 50-72 | 2 | ✅ OK | Clean drag initialization |
| `handleDragMove()` | 77-84 | 2 | ✅ OK | Simple position update |
| `handleDragAutoScroll()` | 89-97 | 2 | ✅ OK | Simple scroll handling |
| `handleColumnChange()` | 102-115 | 3 | ✅ OK | Clean column switching |
| `handleDragEnd()` | 120-141 | 3 | ✅ OK | Proper cleanup |
| `handleNavigationCompleted()` | 146-148 | 1 | ✅ OK | Placeholder method |
| `fadeOutAndRemove()` | 153-160 | 1 | ✅ OK | Simple animation |
| `renderEvents()` | 163-182 | 2 | ✅ OK | Straightforward rendering |
| `renderEvent()` | 184-186 | 1 | ✅ OK | Factory delegation |
| `calculateEventPosition()` | 188-191 | 1 | ✅ OK | Delegates to utility |
| `clearEvents()` | 193-200 | 2 | ✅ OK | Simple cleanup |
| `getColumns()` | 202-205 | 1 | ✅ OK | DOM query |
| `getEventsForColumn()` | 207-221 | 2 | ✅ OK | Filter logic |
**Best Practices:**
- ✅ All methods under complexity 4
- ✅ Clear method names
- ✅ Delegation to utilities
- ✅ Single responsibility per method
---
### 6. AllDayEventRenderer.ts
**File:** `src/renderers/AllDayEventRenderer.ts`
**Overall Complexity:** LOW ✅
| Method | Lines | Complexity | Status | Notes |
|--------|-------|------------|--------|-------|
| `getContainer()` | 20-32 | 3 | ✅ OK | Container initialization |
| `getAllDayContainer()` | 35-37 | 1 | ✅ OK | Simple query |
| `handleDragStart()` | 41-65 | 3 | ✅ OK | Clean drag setup |
| `renderAllDayEventWithLayout()` | 72-83 | 2 | ✅ OK | Simple rendering |
| `removeAllDayEvent()` | 89-97 | 3 | ✅ OK | Clean removal |
| `clearCache()` | 102-104 | 1 | ✅ OK | Simple reset |
| `renderAllDayEventsForPeriod()` | 109-116 | 1 | ✅ OK | Delegates to helper |
| `clearAllDayEvents()` | 118-123 | 2 | ✅ OK | Simple cleanup |
| `handleViewChanged()` | 125-127 | 1 | ✅ OK | Simple handler |
**Best Practices:**
- ✅ Consistent low complexity across all methods
- ✅ Clear separation of concerns
- ✅ Focused functionality
---
## Recommendations
### Immediate Action Required (Complexity >10)
#### 1. SimpleEventOverlapManager.removeStackedStyling() - Priority: CRITICAL
**Current Complexity:** 18
**Target Complexity:** 4-6 per method
**Refactoring Steps:**
1. Extract `clearVisualStyling()` - Remove inline styles
2. Extract `removeMiddleElementFromChain()` - Handle middle element removal
3. Extract `removeLastElementFromChain()` - Handle last element removal
4. Extract `removeFirstElementFromChain()` - Handle first element removal
5. Extract `breakStackChain()` - Handle non-overlapping chain breaking
6. Extract `maintainStackChain()` - Handle overlapping chain maintenance
**Expected Impact:**
- Main method: complexity 4
- Helper methods: complexity 3-6 each
- Improved testability
- Easier maintenance
---
#### 2. DragDropManager.handleMouseMove() - Priority: HIGH
**Current Complexity:** 15
**Target Complexity:** 4-5 per method
**Refactoring Steps:**
1. Extract `updateMousePosition()` - Update tracking variables
2. Extract `shouldStartDrag()` - Check movement threshold
3. Extract `initializeDrag()` - Create clone and emit start event
4. Extract `updateDragPosition()` - Handle position and autoscroll
5. Extract `handleColumnChange()` - Detect and handle column transitions
**Expected Impact:**
- Main method: complexity 4
- Helper methods: complexity 3-4 each
- Better separation of drag lifecycle stages
---
#### 3. SimpleEventOverlapManager.restackEventsInContainer() - Priority: HIGH
**Current Complexity:** 11
**Target Complexity:** 3-4 per method
**Refactoring Steps:**
1. Extract `getStackedEvents()` - Filter stacked events
2. Extract `collectStackChains()` - Build stack chains
3. Extract `findStackRoot()` - Find root of chain
4. Extract `traverseChain()` - Collect chain elements
5. Extract `reapplyStackStyling()` - Apply visual styling
**Expected Impact:**
- Main method: complexity 3
- Helper methods: complexity 2-4 each
---
#### 4. EventRendererManager.setupDragEventListeners() - Priority: MEDIUM
**Current Complexity:** 10
**Target Complexity:** 2-3 per method
**Refactoring Steps:**
1. Extract `setupDragStartListener()`
2. Extract `setupDragMoveListener()`
3. Extract `setupDragEndListener()`
4. Extract `setupDragAutoScrollListener()`
5. Extract `setupColumnChangeListener()`
6. Extract `setupConversionListener()`
7. Extract `setupNavigationListener()`
**Expected Impact:**
- Main method: complexity 1 (just calls helpers)
- Helper methods: complexity 2-3 each
- Improved readability
---
### Medium Priority (Complexity 6-10)
#### 5. SimpleEventOverlapManager.createStackedEvent() - Complexity 7
Consider extracting chain traversal logic into `findEndOfChain()`
#### 6. DragDropManager.startAutoScroll() - Complexity 6
Extract scroll calculation into `calculateScrollAmount()`
#### 7. SimpleEventOverlapManager.removeFromEventGroup() - Complexity 6
Extract remaining event handling into `handleRemainingEvents()`
---
## Code Quality Metrics
### Complexity by File
```
DragDropManager.ts: ████████░░ 8/10 (1 critical, 2 medium)
SwpEventElement.ts: ██░░░░░░░░ 2/10 (excellent!)
SimpleEventOverlapManager.ts: ██████████ 10/10 (2 critical, 2 medium)
EventRendererManager.ts: ██████░░░░ 6/10 (1 critical)
EventRenderer.ts: ██░░░░░░░░ 2/10 (excellent!)
AllDayEventRenderer.ts: ██░░░░░░░░ 2/10 (excellent!)
```
### Methods Requiring Attention
| Priority | File | Method | Complexity | Effort |
|----------|------|--------|------------|--------|
| 🔴 Critical | SimpleEventOverlapManager | removeStackedStyling | 18 | High |
| 🔴 Critical | DragDropManager | handleMouseMove | 15 | High |
| 🔴 High | SimpleEventOverlapManager | restackEventsInContainer | 11 | Medium |
| 🔴 High | EventRendererManager | setupDragEventListeners | 10 | Low |
| 🟡 Medium | SimpleEventOverlapManager | createStackedEvent | 7 | Low |
| 🟡 Medium | DragDropManager | startAutoScroll | 6 | Low |
| 🟡 Medium | SimpleEventOverlapManager | removeFromEventGroup | 6 | Low |
---
## Positive Examples
### SwpEventElement.ts - Excellent Design Pattern
This file demonstrates best practices:
```typescript
// ✅ Clear, focused methods with single responsibility
public updatePosition(columnDate: Date, snappedY: number): void {
this.style.top = `${snappedY + 1}px`;
const { startMinutes, endMinutes } = this.calculateTimesFromPosition(snappedY);
const startDate = this.dateService.createDateAtTime(columnDate, startMinutes);
let endDate = this.dateService.createDateAtTime(columnDate, endMinutes);
if (endMinutes >= 1440) {
const extraDays = Math.floor(endMinutes / 1440);
endDate = this.dateService.addDays(endDate, extraDays);
}
this.start = startDate;
this.end = endDate;
}
// Complexity: 2 (one if statement)
```
**Why this works:**
- Single responsibility (update position)
- Delegates complex calculations to helper methods
- Clear variable names
- Minimal branching
---
## Action Plan
### Phase 1: Critical Refactoring (Week 1-2)
1. ✅ Refactor `SimpleEventOverlapManager.removeStackedStyling()` (18 → 4-6)
2. ✅ Refactor `DragDropManager.handleMouseMove()` (15 → 4-5)
**Expected Impact:**
- Reduce highest complexity from 18 to 4-6
- Improve maintainability significantly
- Enable easier testing
### Phase 2: High Priority (Week 3)
3. ✅ Refactor `SimpleEventOverlapManager.restackEventsInContainer()` (11 → 3-4)
4. ✅ Refactor `EventRendererManager.setupDragEventListeners()` (10 → 2-3)
**Expected Impact:**
- Eliminate all methods with complexity >10
- Improve overall code quality score
### Phase 3: Medium Priority (Week 4)
5. ✅ Review and simplify medium complexity methods (complexity 6-7)
6. ✅ Add unit tests for extracted methods
**Expected Impact:**
- All methods under complexity threshold of 10
- Comprehensive test coverage
### Phase 4: Continuous Improvement
7. ✅ Establish cyclomatic complexity checks in CI/CD
8. ✅ Set max complexity threshold to 10
9. ✅ Regular code reviews focusing on complexity
---
## Tools & Resources
### Recommended Tools for Ongoing Monitoring:
- **TypeScript ESLint** with `complexity` rule
- **SonarQube** for continuous code quality monitoring
- **CodeClimate** for maintainability scoring
### Suggested ESLint Configuration:
```json
{
"rules": {
"complexity": ["error", 10],
"max-lines-per-function": ["warn", 50],
"max-depth": ["error", 4]
}
}
```
---
## Conclusion
The Calendar Plantempus codebase shows **mixed code quality**:
**Strengths:**
- 87.8% of methods have acceptable complexity
- Web Components demonstrate excellent design patterns
- Clear separation of concerns in rendering services
**Areas for Improvement:**
- Stack management logic is overly complex
- Some drag & drop handlers need refactoring
- File naming could better reflect complexity (e.g., "Simple"EventOverlapManager has complexity 18!)
**Overall Grade: B-**
With the recommended refactoring, the codebase can easily achieve an **A grade** by reducing the 4 critical methods to acceptable complexity levels.
---
**Generated by:** Claude Code Cyclomatic Complexity Analyzer
**Date:** 2025-10-04
**Analyzer Version:** 1.0

177
README.md
View file

@ -1,177 +0,0 @@
# Calendar Plantempus
En moderne, event-drevet kalenderapplikation bygget med TypeScript og ASP.NET Core.
## Projekt Information
- **Projekt ID:** 8ecf2aa3-a2e4-4cc3-aa18-1c4352f00ff1
- **Repository:** Calendar (afb8a8ec-cdbc-4c55-8631-fd0285974485)
- **Status:** Under aktiv udvikling
## Teknisk Arkitektur
- **Frontend:** TypeScript med esbuild som bundler
- **Arkitektur:** Event-drevet med CustomEvents (`document.dispatchEvent`/`addEventListener`)
- **Backend:** ASP.NET Core Kestrel server
- **Styling:** Modulær CSS struktur uden eksterne frameworks
- **Bundling:** esbuild for TypeScript transpilering og bundling
## Arkitekturelle Principper
- **Ingen global state** - Alt state håndteres i de relevante managers
- **Event-drevet kommunikation** - Alle komponenter kommunikerer via DOM CustomEvents
- **Modulær opbygning** - Hver manager har et specifikt ansvarsområde
- **Ren DOM manipulation** - Ingen eksterne JavaScript frameworks (React, Vue, etc.)
- **Custom HTML tags** - Semantisk markup med custom elements
## Implementerede Komponenter
Projektet følger en manager-baseret arkitektur, hvor hver manager er ansvarlig for et specifikt aspekt af kalenderen:
### 1. CalendarManager
Hovedkoordinator for alle managers
- Initialiserer og koordinerer alle andre managers
- Håndterer global konfiguration
- Administrerer kalender lifecycle
### 2. ViewManager
Håndterer kalendervisninger
- Skifter mellem dag/uge/måned visninger
- Opdaterer UI baseret på den valgte visning
- Renderer kalender grid struktur
### 3. NavigationManager
Håndterer navigation
- Implementerer prev/next/today funktionalitet
- Håndterer dato navigation
- Opdaterer week info (uge nummer, dato range)
### 4. EventManager
Administrerer events
- Håndterer event lifecycle og CRUD operationer
- Loader og synkroniserer event data
- Administrerer event selection og state
### 5. EventRenderer
Renderer events i DOM
- Positionerer events korrekt i kalender grid
- Håndterer event styling baseret på type
- Implementerer visual feedback for event interactions
### 6. DataManager
Håndterer data operationer
- Mock data loading for udvikling
- Event data transformation
- Data persistence interface
### 7. GridManager
Administrerer kalender grid
- Opretter og vedligeholder grid struktur
- Håndterer time slots og positioning
- Responsive grid layout
## CSS Struktur
Projektet har en modulær CSS struktur for bedre organisering:
- **`calendar-base-css.css`** - Grundlæggende styling og CSS custom properties
- **`calendar-components-css.css`** - UI komponenter og controls
- **`calendar-events-css.css`** - Event styling og farver
- **`calendar-layout-css.css`** - Layout struktur og grid
- **`calendar-popup-css.css`** - Popup og modal styling
- **`calendar.css`** - Samlet styling fra POC (bruges i øjeblikket)
## Kommende Funktionalitet
Baseret på projektstrukturen planlægges følgende komponenter:
### Utilities
- **PositionUtils** - Konvertering mellem pixels og tidspunkter
- **SnapUtils** - Snap-to-interval funktionalitet
- **DOMUtils** - DOM manipulation utilities
### Interaction Managers
- **DragManager** - Drag & drop funktionalitet for events
- **ResizeManager** - Resize funktionalitet for events
- **PopupManager** - Håndtering af event detaljer og popups
### Feature Managers
- **SearchManager** - Søgefunktionalitet i events
- **TimeManager** - Current time indicator
- **LoadingManager** - Loading states og error handling
### Avancerede Features
- Collision detection system for overlappende events
- Animation system for smooth transitions
- Event creation funktionalitet (double-click, drag-to-create)
- Multi-day event support
- Touch support for mobile enheder
- Keyboard navigation
## Projekt Struktur
```
Calendar Plantempus/
├── src/ # TypeScript source files
│ ├── constants/ # Konstanter og enums
│ ├── core/ # Core funktionalitet
│ ├── managers/ # Manager klasser
│ ├── types/ # TypeScript type definitioner
│ └── utils/ # Utility funktioner
├── wwwroot/ # Static web assets
│ ├── css/ # Stylesheets
│ ├── js/ # Compiled JavaScript
│ └── index.html # Main HTML file
├── build.js # esbuild configuration
├── tsconfig.json # TypeScript configuration
├── package.json # Node.js dependencies
└── Program.cs # ASP.NET Core server
```
## Kom i Gang
### Forudsætninger
- .NET 8.0 SDK
- Node.js (for esbuild)
### Installation
1. Klon repository
2. Installer dependencies: `npm install`
3. Build TypeScript: `npm run build`
4. Start server: `dotnet run`
5. Åbn browser på `http://localhost:8000`
### Development
- **Build TypeScript:** `npm run build`
- **Watch mode:** `npm run watch` (hvis konfigureret)
- **Start server:** `dotnet run`
## Event System
Projektet bruger et event-drevet system hvor alle komponenter kommunikerer via DOM CustomEvents:
```typescript
// Dispatch event
document.dispatchEvent(new CustomEvent('calendar:view-changed', {
detail: { view: 'week', date: new Date() }
}));
// Listen for event
document.addEventListener('calendar:view-changed', (event) => {
// Handle view change
});
```
## Bidrag
Dette projekt følger clean code principper og modulær arkitektur. Når du bidrager:
1. Følg den eksisterende manager-baserede struktur
2. Brug event-drevet kommunikation mellem komponenter
3. Undgå global state - hold state i relevante managers
4. Skriv semantisk HTML med custom tags
5. Brug modulær CSS struktur
## Licens
[Specificer licens her]

View file

@ -1,772 +0,0 @@
# 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

View file