Introduces a polymorphic `createClone` method on base event elements to customize clone generation. Adds a `replaceClone` delegate to drag event payloads, enabling subscribers to dynamically swap the active dragged clone. This supports scenarios like converting a standard event clone to an all-day event clone when dragging to the all-day header.
21 KiB
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():
if (event.buttons === 1)- Check if mouse button is pressedif (!this.isDragStarted && this.draggedElement)- Check for drag initializationif (totalMovement >= this.dragThreshold)- Movement threshold checkif (this.isDragStarted && this.draggedElement && this.draggedClone)- Drag state validationif (!this.draggedElement.hasAttribute("data-allday"))- Event type checkif (deltaY >= this.snapDistancePx)- Snap interval check- Multiple autoscroll conditionals
if (newColumn == null)- Column validationif (newColumn?.index !== this.currentColumnBounds?.index)- Column change detection
Recommendation for handleMouseMove():
// 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:
if (link)- Check if element has stack linkif (link.prev && link.next)- Middle element in chainif (prevElement && nextElement)- Both neighbors existif (!actuallyOverlap)- Chain breaking decision (CRITICAL BRANCH)if (nextLink?.next)- Subsequent elements existwhile (subsequentId)- Loop through chainif (!subsequentElement)- Element validationelse- Normal stacking (chain maintenance)else if (link.prev)- Last element caseif (prevElement)- Previous element existselse if (link.next)- First element caseif (nextElement)- Next element existsif (link.prev && link.next)- Middle element check (duplicate)if (nextLink && nextLink.next)- Chain continuationelse- Chain was broken 16-18. Additional nested conditions
Recommendation for removeStackedStyling():
// 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:
if (stackedEvents.length === 0)- Early returnfor (const element of stackedEvents)- Iterate eventsif (!eventId || processedEventIds.has(eventId))- Validationwhile (rootLink?.prev)- Find root of chainif (!prevElement)- Break conditionwhile (currentElement)- Traverse chainif (!currentLink?.next)- End of chainif (!nextElement)- Break conditionif (chain.length > 1)- Only add multi-element chainsforEach- Restack each chainif (link)- Update link data
Recommendation for restackEventsInContainer():
// 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:
if (hasAttribute('data-allday'))- Filter all-day eventsif (draggedElement && strategy.handleDragStart && columnBounds)- Validationif (hasAttribute('data-allday'))- Filter checkif (strategy.handleDragMove)- Strategy checkif (strategy.handleDragAutoScroll)- Strategy checkif (target === 'swp-day-column' && finalColumn)- Drop target validationif (draggedElement && draggedClone && strategy.handleDragEnd)- Validationif (dayEventClone)- Cleanup checkif (hasAttribute('data-allday'))- Filter checkif (strategy.handleColumnChange)- Strategy check
Recommendation:
// 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:
- Extract
clearVisualStyling()- Remove inline styles - Extract
removeMiddleElementFromChain()- Handle middle element removal - Extract
removeLastElementFromChain()- Handle last element removal - Extract
removeFirstElementFromChain()- Handle first element removal - Extract
breakStackChain()- Handle non-overlapping chain breaking - 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:
- Extract
updateMousePosition()- Update tracking variables - Extract
shouldStartDrag()- Check movement threshold - Extract
initializeDrag()- Create clone and emit start event - Extract
updateDragPosition()- Handle position and autoscroll - 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:
- Extract
getStackedEvents()- Filter stacked events - Extract
collectStackChains()- Build stack chains - Extract
findStackRoot()- Find root of chain - Extract
traverseChain()- Collect chain elements - 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:
- Extract
setupDragStartListener() - Extract
setupDragMoveListener() - Extract
setupDragEndListener() - Extract
setupDragAutoScrollListener() - Extract
setupColumnChangeListener() - Extract
setupConversionListener() - 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:
// ✅ 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)
- ✅ Refactor
SimpleEventOverlapManager.removeStackedStyling()(18 → 4-6) - ✅ 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)
- ✅ Refactor
SimpleEventOverlapManager.restackEventsInContainer()(11 → 3-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)
- ✅ Review and simplify medium complexity methods (complexity 6-7)
- ✅ Add unit tests for extracted methods
Expected Impact:
- All methods under complexity threshold of 10
- Comprehensive test coverage
Phase 4: Continuous Improvement
- ✅ Establish cyclomatic complexity checks in CI/CD
- ✅ Set max complexity threshold to 10
- ✅ Regular code reviews focusing on complexity
Tools & Resources
Recommended Tools for Ongoing Monitoring:
- TypeScript ESLint with
complexityrule - SonarQube for continuous code quality monitoring
- CodeClimate for maintainability scoring
Suggested ESLint Configuration:
{
"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