Improves event layout and stacking logic
Refactors the event layout and stacking logic based on review feedback. This includes: - Merging conflicting event groups to prevent inconsistencies. - Implementing minimal stack level assignment using a min-heap. - Consolidating styling and using DateService for drag operations. - Adding reflow after drag and drop. - Improving the column event filtering to include events overlapping midnight. - Ensuring explicit sorting of events for grid layout.
This commit is contained in:
parent
b590467f60
commit
faa59f6a3c
19 changed files with 1502 additions and 55 deletions
217
.workbench/review.txt
Normal file
217
.workbench/review.txt
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
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:00–23: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
|
||||
0
complexity-output.json
Normal file
0
complexity-output.json
Normal file
50
docs/cyclomatic-complexity-report.md
Normal file
50
docs/cyclomatic-complexity-report.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Cyclomatic Complexity Analysis Report
|
||||
## Calendar Plantempus TypeScript Codebase
|
||||
|
||||
**Generated:** 2025-10-06
|
||||
**Analyzer:** Roo AI Assistant
|
||||
**Total Files Analyzed:** 40+
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
### Complexity Distribution
|
||||
|
||||
| Complexity Range | Risk Level | Count | Percentage |
|
||||
|-----------------|------------|-------|------------|
|
||||
| 1-10 | ✅ Low | TBD | TBD% |
|
||||
| 11-20 | ⚠️ Medium | TBD | TBD% |
|
||||
| 21-50 | 🔴 High | TBD | TBD% |
|
||||
| 50+ | 💀 Critical | TBD | TBD% |
|
||||
|
||||
### Key Metrics
|
||||
- **Total Functions:** TBD
|
||||
- **Average Complexity:** TBD
|
||||
- **Highest Complexity:** TBD
|
||||
- **Files Needing Refactoring:** TBD
|
||||
|
||||
---
|
||||
|
||||
## Methodology
|
||||
|
||||
### Cyclomatic Complexity Calculation
|
||||
For each function, we count:
|
||||
- **+1** for the function itself
|
||||
- **+1** for each `if`, `else if`, `while`, `for`, `case`
|
||||
- **+1** for each `&&`, `||` in conditions
|
||||
- **+1** for each `catch` block
|
||||
- **+1** for each ternary operator `? :`
|
||||
- **+1** for each `return` statement (except the last one)
|
||||
|
||||
### Risk Assessment
|
||||
- **1-10:** Simple, easy to test and maintain
|
||||
- **11-20:** Moderate complexity, acceptable
|
||||
- **21-50:** Complex, should be refactored
|
||||
- **50+:** Very complex, high maintenance risk
|
||||
|
||||
---
|
||||
|
||||
## Analysis in Progress...
|
||||
|
||||
This report is being generated. Please wait for the complete analysis.
|
||||
62
scenarios/scenario-1.html
Normal file
62
scenarios/scenario-1.html
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 1: No Overlap</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 1: No Overlap</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Three sequential events with no time overlap. All events should have stack level 0 since they don't conflict.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=0 (stacked)<br>
|
||||
Event C: stackLevel=0 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S1A" data-title="Scenario 1: Event A" data-start="2025-10-06T08:00:00.000Z" data-end="2025-10-06T09:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 1px; height: 77px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 1: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S1B" data-title="Scenario 1: Event B" data-start="2025-10-06T09:00:00.000Z" data-end="2025-10-06T10:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 81px; height: 77px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="60">11:00 - 12:00</swp-event-time>
|
||||
<swp-event-title>Scenario 1: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S1C" data-title="Scenario 1: Event C" data-start="2025-10-06T10:00:00.000Z" data-end="2025-10-06T11:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 161px; height: 77px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="60">12:00 - 13:00</swp-event-time>
|
||||
<swp-event-title>Scenario 1: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-1',
|
||||
expected: [
|
||||
{ eventId: 'S1A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S1B', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S1C', stackLevel: 0, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
77
scenarios/scenario-10.html
Normal file
77
scenarios/scenario-10.html
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 10: Four Column Grid</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 10: Four Column Grid</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Four events all starting at exactly the same time (14:00). Tests maximum column sharing with a 4-column grid layout.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Grid group with 4 columns at stackLevel=0<br>
|
||||
Event A: in grid<br>
|
||||
Event B: in grid<br>
|
||||
Event C: in grid<br>
|
||||
Event D: in grid
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event-group class="cols-4 stack-level-0" data-stack-link="{"stackLevel":0}" style="top: 561px; margin-left: 0px; z-index: 100;">
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S10A" data-title="Scenario 10: Event A" data-start="2025-10-10T12:00:00.000Z" data-end="2025-10-10T13:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">14:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 10: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S10B" data-title="Scenario 10: Event B" data-start="2025-10-10T12:00:00.000Z" data-end="2025-10-10T13:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">14:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 10: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S10C" data-title="Scenario 10: Event C" data-start="2025-10-10T12:00:00.000Z" data-end="2025-10-10T13:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">14:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 10: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S10D" data-title="Scenario 10: Event D" data-start="2025-10-10T12:00:00.000Z" data-end="2025-10-10T13:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">14:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 10: Event D</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</swp-event-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-10',
|
||||
expected: [
|
||||
{ eventId: 'S10A', stackLevel: 0, cols: 4, type: 'grid' },
|
||||
{ eventId: 'S10B', stackLevel: 0, cols: 4, type: 'grid' },
|
||||
{ eventId: 'S10C', stackLevel: 0, cols: 4, type: 'grid' },
|
||||
{ eventId: 'S10D', stackLevel: 0, cols: 4, type: 'grid' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
61
scenarios/scenario-2.html
Normal file
61
scenarios/scenario-2.html
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 2: Column Sharing (Grid)</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 2: Column Sharing (Grid)</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Two events starting at exactly the same time (10:00). These should be placed in a grid container with 2 columns, allowing them to share horizontal space.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Grid group with 2 columns at stackLevel=0<br>
|
||||
Event A: in grid<br>
|
||||
Event B: in grid
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event-group class="cols-2 stack-level-0" data-stack-link="{"stackLevel":0}" style="top: 1px; margin-left: 0px; z-index: 100;">
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S2A" data-title="Scenario 2: Event A" data-start="2025-10-06T08:00:00.000Z" data-end="2025-10-06T09:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 2: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S2B" data-title="Scenario 2: Event B" data-start="2025-10-06T08:00:00.000Z" data-end="2025-10-06T09:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 2: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</swp-event-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-2',
|
||||
expected: [
|
||||
{ eventId: 'S2A', stackLevel: 0, cols: 2, type: 'grid' },
|
||||
{ eventId: 'S2B', stackLevel: 0, cols: 2, type: 'grid' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
69
scenarios/scenario-3.html
Normal file
69
scenarios/scenario-3.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 3: Nested Stacking</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 3: Nested Stacking</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Progressive nesting pattern: Event A (09:00-15:00) contains B (10:00-13:00), B contains C (11:00-12:00), and C overlaps with D (12:30-13:30). Tests correct stack level calculation for nested events.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=1 (stacked)<br>
|
||||
Event C: stackLevel=2 (stacked)<br>
|
||||
Event D: stackLevel=2 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S3A" data-title="Scenario 3: Event A" data-start="2025-10-07T07:00:00.000Z" data-end="2025-10-07T13:00:00.000Z" data-type="work" data-duration="360" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 1px; height: 357px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="360">09:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S3B" data-title="Scenario 3: Event B" data-start="2025-10-07T08:00:00.000Z" data-end="2025-10-07T11:00:00.000Z" data-type="work" data-duration="180" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 81px; height: 217px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="180">10:00 - 13:00</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S3C" data-title="Scenario 3: Event C" data-start="2025-10-07T09:00:00.000Z" data-end="2025-10-07T10:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":2}" style="position: absolute; top: 161px; height: 77px; left: 2px; right: 2px; margin-left: 30px; z-index: 102;">
|
||||
<swp-event-time data-duration="60">11:00 - 12:00</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S3D" data-title="Scenario 3: Event D" data-start="2025-10-07T10:30:00.000Z" data-end="2025-10-07T11:30:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":2}" style="position: absolute; top: 241px; height: 77px; left: 2px; right: 2px; margin-left: 30px; z-index: 102;">
|
||||
<swp-event-time data-duration="60">12:30 - 13:30</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event D</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-3',
|
||||
expected: [
|
||||
{ eventId: 'S3A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S3B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S3C', stackLevel: 2, type: 'stacked' },
|
||||
{ eventId: 'S3D', stackLevel: 2, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
69
scenarios/scenario-4.html
Normal file
69
scenarios/scenario-4.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 4: Complex Stacking</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 4: Complex Stacking</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Long event A (14:00-20:00) with multiple shorter events (B, C, D) nested inside at different times. Tests multiple stack levels with varying overlap patterns.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=1 (stacked)<br>
|
||||
Event C: stackLevel=2 (stacked)<br>
|
||||
Event D: stackLevel=1 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S4A" data-title="Scenario 4: Event A" data-start="2025-10-07T12:00:00.000Z" data-end="2025-10-07T18:00:00.000Z" data-type="work" data-duration="360" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 481px; height: 357px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="360">14:00 - 20:00</swp-event-time>
|
||||
<swp-event-title>Scenario 4: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S4B" data-title="Scenario 4: Event B" data-start="2025-10-07T13:00:00.000Z" data-end="2025-10-07T15:00:00.000Z" data-type="work" data-duration="120" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 561px; height: 157px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="120">15:00 - 17:00</swp-event-time>
|
||||
<swp-event-title>Scenario 4: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S4C" data-title="Scenario 4: Event C" data-start="2025-10-07T13:30:00.000Z" data-end="2025-10-07T14:30:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":2}" style="position: absolute; top: 601px; height: 77px; left: 2px; right: 2px; margin-left: 30px; z-index: 102;">
|
||||
<swp-event-time data-duration="60">15:30 - 16:30</swp-event-time>
|
||||
<swp-event-title>Scenario 4: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S4D" data-title="Scenario 4: Event D" data-start="2025-10-07T16:00:00.000Z" data-end="2025-10-07T17:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 721px; height: 77px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="60">18:00 - 19:00</swp-event-time>
|
||||
<swp-event-title>Scenario 4: Event D</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-4',
|
||||
expected: [
|
||||
{ eventId: 'S4A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S4B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S4C', stackLevel: 2, type: 'stacked' },
|
||||
{ eventId: 'S4D', stackLevel: 1, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
69
scenarios/scenario-5.html
Normal file
69
scenarios/scenario-5.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 5: Three Column Share</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 5: Three Column Share</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Three events all starting at exactly the same time (10:00). Should create a grid layout with 3 columns.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Grid group with 3 columns at stackLevel=0<br>
|
||||
Event A: in grid<br>
|
||||
Event B: in grid<br>
|
||||
Event C: in grid
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event-group class="cols-3 stack-level-0" data-stack-link="{"stackLevel":0}" style="top: 1px; margin-left: 0px; z-index: 100;">
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S5A" data-title="Scenario 5: Event A" data-start="2025-10-08T08:00:00.000Z" data-end="2025-10-08T09:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 5: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S5B" data-title="Scenario 5: Event B" data-start="2025-10-08T08:00:00.000Z" data-end="2025-10-08T09:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 5: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S5C" data-title="Scenario 5: Event C" data-start="2025-10-08T08:00:00.000Z" data-end="2025-10-08T09:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 5: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</swp-event-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-5',
|
||||
expected: [
|
||||
{ eventId: 'S5A', stackLevel: 0, cols: 3, type: 'grid' },
|
||||
{ eventId: 'S5B', stackLevel: 0, cols: 3, type: 'grid' },
|
||||
{ eventId: 'S5C', stackLevel: 0, cols: 3, type: 'grid' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
69
scenarios/scenario-6.html
Normal file
69
scenarios/scenario-6.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 6: Overlapping Pairs</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 6: Overlapping Pairs</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Two separate pairs of overlapping events: (A, B) and (C, D). Each pair should be independent with their own stack levels.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=1 (stacked)<br>
|
||||
Event C: stackLevel=0 (stacked)<br>
|
||||
Event D: stackLevel=1 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S6A" data-title="Scenario 6: Event A" data-start="2025-10-08T08:00:00.000Z" data-end="2025-10-08T10:00:00.000Z" data-type="work" data-duration="120" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 161px; height: 157px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="120">10:00 - 12:00</swp-event-time>
|
||||
<swp-event-title>Scenario 6: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S6B" data-title="Scenario 6: Event B" data-start="2025-10-08T09:00:00.000Z" data-end="2025-10-08T10:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 241px; height: 77px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="60">11:00 - 12:00</swp-event-time>
|
||||
<swp-event-title>Scenario 6: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S6C" data-title="Scenario 6: Event C" data-start="2025-10-08T11:00:00.000Z" data-end="2025-10-08T13:00:00.000Z" data-type="work" data-duration="120" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 401px; height: 157px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="120">13:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 6: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S6D" data-title="Scenario 6: Event D" data-start="2025-10-08T12:00:00.000Z" data-end="2025-10-08T13:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 481px; height: 77px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="60">14:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 6: Event D</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-6',
|
||||
expected: [
|
||||
{ eventId: 'S6A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S6B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S6C', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S6D', stackLevel: 1, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
62
scenarios/scenario-7.html
Normal file
62
scenarios/scenario-7.html
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 7: Long Event Container</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 7: Long Event Container</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>One long event (A: 09:00-15:00) containing two shorter events (B: 10:00-11:00, C: 12:00-13:00) that don't overlap with each other. B and C should both have the same stack level.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=1 (stacked)<br>
|
||||
Event C: stackLevel=1 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S7A" data-title="Scenario 7: Event A" data-start="2025-10-09T07:00:00.000Z" data-end="2025-10-09T13:00:00.000Z" data-type="work" data-duration="360" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 1px; height: 357px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="360">09:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 7: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S7B" data-title="Scenario 7: Event B" data-start="2025-10-09T08:00:00.000Z" data-end="2025-10-09T09:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 81px; height: 77px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 7: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S7C" data-title="Scenario 7: Event C" data-start="2025-10-09T10:00:00.000Z" data-end="2025-10-09T11:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":1}" style="position: absolute; top: 241px; height: 77px; left: 2px; right: 2px; margin-left: 15px; z-index: 101;">
|
||||
<swp-event-time data-duration="60">12:00 - 13:00</swp-event-time>
|
||||
<swp-event-title>Scenario 7: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-7',
|
||||
expected: [
|
||||
{ eventId: 'S7A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S7B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S7C', stackLevel: 1, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
55
scenarios/scenario-8.html
Normal file
55
scenarios/scenario-8.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 8: Edge-Adjacent Events</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 8: Edge-Adjacent Events</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Events that touch but don't overlap: Event A (10:00-11:00) and Event B (11:00-12:00). A ends exactly when B starts, so they should NOT stack.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Event A: stackLevel=0 (stacked)<br>
|
||||
Event B: stackLevel=0 (stacked)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event data-event-id="S8A" data-title="Scenario 8: Event A" data-start="2025-10-09T08:00:00.000Z" data-end="2025-10-09T09:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 241px; height: 77px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="60">10:00 - 11:00</swp-event-time>
|
||||
<swp-event-title>Scenario 8: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
|
||||
<swp-event data-event-id="S8B" data-title="Scenario 8: Event B" data-start="2025-10-09T09:00:00.000Z" data-end="2025-10-09T10:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":0}" style="position: absolute; top: 321px; height: 77px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="60">11:00 - 12:00</swp-event-time>
|
||||
<swp-event-title>Scenario 8: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-8',
|
||||
expected: [
|
||||
{ eventId: 'S8A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S8B', stackLevel: 0, type: 'stacked' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
67
scenarios/scenario-9.html
Normal file
67
scenarios/scenario-9.html
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenario 9: End-to-Start Chain</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<a href="../stacking-visualization-new.html" class="back-link">← Back to All Scenarios</a>
|
||||
|
||||
<div class="scenario-header">
|
||||
<h1 class="scenario-title">Scenario 9: End-to-Start Chain</h1>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario-description">
|
||||
<h3>Description</h3>
|
||||
<p>Events linked by end-to-start conflicts within the threshold: Event A (12:00-13:00), Event B (12:30-13:30), and Event C (13:15-15:00). Even though C doesn't start close to A, it starts within threshold before B ends, creating a conflict chain A→B→C.</p>
|
||||
|
||||
<div class="expected-result">
|
||||
<strong>Expected Result:</strong><br>
|
||||
Grid group with 2 columns at stackLevel=0<br>
|
||||
Event A: in grid (column 1)<br>
|
||||
Event B: in grid (column 2)<br>
|
||||
Event C: in grid (column 1)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-column">
|
||||
<swp-event-group class="cols-2 stack-level-0" data-stack-link="{"stackLevel":0}" style="top: 481px; margin-left: 0px; z-index: 100;">
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S9A" data-title="Scenario 9: Event A" data-start="2025-10-09T10:00:00.000Z" data-end="2025-10-09T11:00:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 0px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">12:00 - 13:00</swp-event-time>
|
||||
<swp-event-title>Scenario 9: Event A</swp-event-title>
|
||||
</swp-event>
|
||||
<swp-event data-event-id="S9C" data-title="Scenario 9: Event C" data-start="2025-10-09T11:15:00.000Z" data-end="2025-10-09T13:00:00.000Z" data-type="work" data-duration="105" style="position: absolute; top: 100px; height: 137px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="105">13:15 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 9: Event C</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<swp-event data-event-id="S9B" data-title="Scenario 9: Event B" data-start="2025-10-09T10:30:00.000Z" data-end="2025-10-09T11:30:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 40px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">12:30 - 13:30</swp-event-time>
|
||||
<swp-event-title>Scenario 9: Event B</swp-event-title>
|
||||
</swp-event>
|
||||
</div>
|
||||
</swp-event-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
window.scenarioTests = {
|
||||
id: 'scenario-9',
|
||||
expected: [
|
||||
{ eventId: 'S9A', stackLevel: 0, cols: 2, type: 'grid' },
|
||||
{ eventId: 'S9B', stackLevel: 0, cols: 2, type: 'grid' },
|
||||
{ eventId: 'S9C', stackLevel: 0, cols: 2, type: 'grid' }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<script type="module" src="scenario-test-runner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
162
scenarios/scenario-styles.css
Normal file
162
scenarios/scenario-styles.css
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/* Shared styles for all scenario visualization files */
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.scenario-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.scenario-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.scenario-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.test-badge {
|
||||
display: inline-block;
|
||||
margin-left: 15px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.test-passed {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-failed {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-pending {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.scenario-description {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-left: 4px solid #b53f7a;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.expected-result {
|
||||
background: #e8f5e9;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.calendar-column {
|
||||
position: relative;
|
||||
width: 350px;
|
||||
height: 800px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* Event styling */
|
||||
swp-event-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
swp-event-group > div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
swp-event {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
border-left: 4px solid #b53f7a;
|
||||
background: #fff3e0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
swp-event-time {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
swp-event-title {
|
||||
display: block;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
color: #b53f7a;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.test-details {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.test-details h4 {
|
||||
margin-top: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.test-result-line {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.test-result-line.passed {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.test-result-line.failed {
|
||||
color: #f44336;
|
||||
}
|
||||
152
scenarios/scenario-test-runner.js
Normal file
152
scenarios/scenario-test-runner.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* Scenario Test Runner
|
||||
* Validates that rendered events match expected layout
|
||||
*/
|
||||
|
||||
export class ScenarioTestRunner {
|
||||
/**
|
||||
* Validate a scenario's rendered output
|
||||
* @param {string} scenarioId - e.g., "scenario-1"
|
||||
* @param {Array} expectedResults - Array of {eventId, stackLevel, cols?, type: 'grid'|'stacked'}
|
||||
* @returns {object} - {passed: boolean, results: Array, message: string}
|
||||
*/
|
||||
static validateScenario(scenarioId, expectedResults) {
|
||||
const results = [];
|
||||
let allPassed = true;
|
||||
|
||||
for (const expected of expectedResults) {
|
||||
const result = this.validateEvent(expected.eventId, expected);
|
||||
results.push(result);
|
||||
if (!result.passed) {
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
passed: allPassed,
|
||||
results,
|
||||
message: allPassed ? 'All tests passed ✅' : 'Some tests failed ❌'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single event
|
||||
*/
|
||||
static validateEvent(eventId, expected) {
|
||||
const eventEl = document.querySelector(`swp-event[data-event-id="${eventId}"]`);
|
||||
|
||||
if (!eventEl) {
|
||||
return {
|
||||
passed: false,
|
||||
eventId,
|
||||
message: `Event ${eventId} not found in DOM`
|
||||
};
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
|
||||
// Check if in grid group
|
||||
const gridGroup = eventEl.closest('swp-event-group');
|
||||
|
||||
if (expected.type === 'grid') {
|
||||
if (!gridGroup) {
|
||||
errors.push(`Expected to be in grid group, but found as stacked event`);
|
||||
} else {
|
||||
// Validate grid group properties
|
||||
const groupStackLevel = this.getStackLevel(gridGroup);
|
||||
if (groupStackLevel !== expected.stackLevel) {
|
||||
errors.push(`Grid group stack level: expected ${expected.stackLevel}, got ${groupStackLevel}`);
|
||||
}
|
||||
|
||||
if (expected.cols) {
|
||||
const cols = this.getColumnCount(gridGroup);
|
||||
if (cols !== expected.cols) {
|
||||
errors.push(`Grid columns: expected ${expected.cols}, got ${cols}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (expected.type === 'stacked') {
|
||||
if (gridGroup) {
|
||||
errors.push(`Expected to be stacked, but found in grid group`);
|
||||
} else {
|
||||
// Validate stacked event properties
|
||||
const stackLevel = this.getStackLevel(eventEl);
|
||||
if (stackLevel !== expected.stackLevel) {
|
||||
errors.push(`Stack level: expected ${expected.stackLevel}, got ${stackLevel}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
passed: errors.length === 0,
|
||||
eventId,
|
||||
message: errors.length === 0 ? '✅' : errors.join('; ')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stack level from element
|
||||
*/
|
||||
static getStackLevel(element) {
|
||||
const stackLink = element.getAttribute('data-stack-link');
|
||||
if (stackLink) {
|
||||
try {
|
||||
const parsed = JSON.parse(stackLink);
|
||||
return parsed.stackLevel;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Try class name fallback
|
||||
const classMatch = element.className.match(/stack-level-(\d+)/);
|
||||
return classMatch ? parseInt(classMatch[1]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column count from grid group
|
||||
*/
|
||||
static getColumnCount(gridGroup) {
|
||||
const classMatch = gridGroup.className.match(/cols-(\d+)/);
|
||||
return classMatch ? parseInt(classMatch[1]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display test results in the DOM
|
||||
*/
|
||||
static displayResults(containerId, results) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const badge = document.createElement('span');
|
||||
badge.className = `test-badge ${results.passed ? 'test-passed' : 'test-failed'}`;
|
||||
badge.textContent = results.message;
|
||||
container.appendChild(badge);
|
||||
|
||||
// Add detailed results
|
||||
const details = document.createElement('div');
|
||||
details.className = 'test-details';
|
||||
details.innerHTML = '<h4>Test Results:</h4>';
|
||||
|
||||
results.results.forEach(r => {
|
||||
const line = document.createElement('div');
|
||||
line.className = `test-result-line ${r.passed ? 'passed' : 'failed'}`;
|
||||
line.textContent = `${r.eventId}: ${r.message}`;
|
||||
details.appendChild(line);
|
||||
});
|
||||
|
||||
container.appendChild(details);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-run tests if window.scenarioTests is defined
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
if (window.scenarioTests) {
|
||||
const results = ScenarioTestRunner.validateScenario(
|
||||
window.scenarioTests.id,
|
||||
window.scenarioTests.expected
|
||||
);
|
||||
|
||||
ScenarioTestRunner.displayResults('test-results', results);
|
||||
}
|
||||
});
|
||||
|
|
@ -58,49 +58,8 @@ export class EventLayoutCoordinator {
|
|||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const thresholdMinutes = gridSettings.gridStartThresholdMinutes;
|
||||
|
||||
const gridCandidates = [firstEvent];
|
||||
let candidatesChanged = true;
|
||||
|
||||
// Keep expanding until no new candidates can be added
|
||||
while (candidatesChanged) {
|
||||
candidatesChanged = false;
|
||||
|
||||
for (let i = 1; i < remaining.length; i++) {
|
||||
const candidate = remaining[i];
|
||||
|
||||
// Skip if already in candidates
|
||||
if (gridCandidates.includes(candidate)) continue;
|
||||
|
||||
// Check if candidate conflicts with ANY event in gridCandidates
|
||||
for (const existingCandidate of gridCandidates) {
|
||||
let hasConflict = false;
|
||||
|
||||
// Check 1: Start-to-start conflict (starts within threshold)
|
||||
const startToStartDiff = Math.abs(candidate.start.getTime() - existingCandidate.start.getTime()) / (1000 * 60);
|
||||
if (startToStartDiff <= thresholdMinutes && this.stackManager.doEventsOverlap(candidate, existingCandidate)) {
|
||||
hasConflict = true;
|
||||
}
|
||||
|
||||
// Check 2: End-to-start conflict (candidate starts within threshold before existingCandidate ends)
|
||||
const endToStartMinutes = (existingCandidate.end.getTime() - candidate.start.getTime()) / (1000 * 60);
|
||||
if (endToStartMinutes > 0 && endToStartMinutes <= thresholdMinutes) {
|
||||
hasConflict = true;
|
||||
}
|
||||
|
||||
// Check 3: Reverse end-to-start (existingCandidate starts within threshold before candidate ends)
|
||||
const reverseEndToStart = (candidate.end.getTime() - existingCandidate.start.getTime()) / (1000 * 60);
|
||||
if (reverseEndToStart > 0 && reverseEndToStart <= thresholdMinutes) {
|
||||
hasConflict = true;
|
||||
}
|
||||
|
||||
if (hasConflict) {
|
||||
gridCandidates.push(candidate);
|
||||
candidatesChanged = true;
|
||||
break; // Found conflict, move to next candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use refactored method for expanding grid candidates
|
||||
const gridCandidates = this.expandGridCandidates(firstEvent, remaining, thresholdMinutes);
|
||||
|
||||
// Decide: should this group be GRID or STACK?
|
||||
const group: EventGroup = {
|
||||
|
|
@ -117,7 +76,8 @@ export class EventLayoutCoordinator {
|
|||
renderedEventsWithLevels
|
||||
);
|
||||
|
||||
const earliestEvent = gridCandidates[0];
|
||||
// Ensure we get the earliest event (explicit sort for robustness)
|
||||
const earliestEvent = [...gridCandidates].sort((a, b) => a.start.getTime() - b.start.getTime())[0];
|
||||
const position = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
|
||||
const columns = this.allocateColumns(gridCandidates);
|
||||
|
||||
|
|
@ -201,6 +161,78 @@ export class EventLayoutCoordinator {
|
|||
return maxOverlappingLevel + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if two events have a conflict based on threshold
|
||||
*
|
||||
* @param event1 - First event
|
||||
* @param event2 - Second event
|
||||
* @param thresholdMinutes - Threshold in minutes
|
||||
* @returns true if events conflict
|
||||
*/
|
||||
private detectConflict(event1: CalendarEvent, event2: CalendarEvent, thresholdMinutes: number): boolean {
|
||||
// Check 1: Start-to-start conflict (starts within threshold)
|
||||
const startToStartDiff = Math.abs(event1.start.getTime() - event2.start.getTime()) / (1000 * 60);
|
||||
if (startToStartDiff <= thresholdMinutes && this.stackManager.doEventsOverlap(event1, event2)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check 2: End-to-start conflict (event1 starts within threshold before event2 ends)
|
||||
const endToStartMinutes = (event2.end.getTime() - event1.start.getTime()) / (1000 * 60);
|
||||
if (endToStartMinutes > 0 && endToStartMinutes <= thresholdMinutes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check 3: Reverse end-to-start (event2 starts within threshold before event1 ends)
|
||||
const reverseEndToStart = (event1.end.getTime() - event2.start.getTime()) / (1000 * 60);
|
||||
if (reverseEndToStart > 0 && reverseEndToStart <= thresholdMinutes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand grid candidates to find all events connected by conflict chains
|
||||
*
|
||||
* Uses expanding search to find chains (A→B→C where each conflicts with next)
|
||||
*
|
||||
* @param firstEvent - The first event to start with
|
||||
* @param remaining - Remaining events to check
|
||||
* @param thresholdMinutes - Threshold in minutes
|
||||
* @returns Array of all events in the conflict chain
|
||||
*/
|
||||
private expandGridCandidates(
|
||||
firstEvent: CalendarEvent,
|
||||
remaining: CalendarEvent[],
|
||||
thresholdMinutes: number
|
||||
): CalendarEvent[] {
|
||||
const gridCandidates = [firstEvent];
|
||||
let candidatesChanged = true;
|
||||
|
||||
// Keep expanding until no new candidates can be added
|
||||
while (candidatesChanged) {
|
||||
candidatesChanged = false;
|
||||
|
||||
for (let i = 1; i < remaining.length; i++) {
|
||||
const candidate = remaining[i];
|
||||
|
||||
// Skip if already in candidates
|
||||
if (gridCandidates.includes(candidate)) continue;
|
||||
|
||||
// Check if candidate conflicts with ANY event in gridCandidates
|
||||
for (const existingCandidate of gridCandidates) {
|
||||
if (this.detectConflict(candidate, existingCandidate, thresholdMinutes)) {
|
||||
gridCandidates.push(candidate);
|
||||
candidatesChanged = true;
|
||||
break; // Found conflict, move to next candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gridCandidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate events to columns within a grid group
|
||||
*
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
|
||||
// Delegate to SwpEventElement to update position and timestamps
|
||||
const swpEvent = this.draggedClone as SwpEventElement;
|
||||
const columnDate = new Date(payload.columnBounds.date);
|
||||
const columnDate = this.dateService.parseISO(payload.columnBounds.date);
|
||||
swpEvent.updatePosition(columnDate, payload.snappedY);
|
||||
}
|
||||
|
||||
|
|
@ -111,11 +111,11 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
const eventsLayer = dragColumnChangeEvent.newColumn.element.querySelector('swp-events-layer');
|
||||
if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) {
|
||||
eventsLayer.appendChild(this.draggedClone);
|
||||
|
||||
|
||||
// Recalculate timestamps with new column date
|
||||
const currentTop = parseFloat(this.draggedClone.style.top) || 0;
|
||||
const swpEvent = this.draggedClone as SwpEventElement;
|
||||
const columnDate = new Date(dragColumnChangeEvent.newColumn.date);
|
||||
const columnDate = this.dateService.parseISO(dragColumnChangeEvent.newColumn.date);
|
||||
swpEvent.updatePosition(columnDate, currentTop);
|
||||
}
|
||||
}
|
||||
|
|
@ -221,16 +221,15 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
// Position from layout
|
||||
groupElement.style.top = `${gridGroup.position.top}px`;
|
||||
|
||||
// Add inline styles for margin-left and z-index (guaranteed to work)
|
||||
groupElement.style.marginLeft = `${gridGroup.stackLevel * 15}px`;
|
||||
groupElement.style.zIndex = `${this.stackManager.calculateZIndex(gridGroup.stackLevel)}`;
|
||||
|
||||
// Add stack-link attribute for drag-drop (group acts as a stacked item)
|
||||
const stackLink = {
|
||||
stackLevel: gridGroup.stackLevel
|
||||
};
|
||||
this.stackManager.applyStackLinkToElement(groupElement, stackLink);
|
||||
|
||||
// Apply visual styling (margin-left and z-index) using StackManager
|
||||
this.stackManager.applyVisualStyling(groupElement, gridGroup.stackLevel);
|
||||
|
||||
// Render each column
|
||||
const earliestEvent = gridGroup.events[0];
|
||||
gridGroup.columns.forEach(columnEvents => {
|
||||
|
|
@ -330,11 +329,14 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
return [];
|
||||
}
|
||||
|
||||
const columnEvents = events.filter(event => {
|
||||
const eventDateStr = this.dateService.formatISODate(event.start);
|
||||
const matches = eventDateStr === columnDate;
|
||||
// Create start and end of day for interval overlap check
|
||||
const columnStart = this.dateService.parseISO(`${columnDate}T00:00:00`);
|
||||
const columnEnd = this.dateService.parseISO(`${columnDate}T23:59:59.999`);
|
||||
|
||||
return matches;
|
||||
const columnEvents = events.filter(event => {
|
||||
// Interval overlap: event overlaps with column day if event.start < columnEnd AND event.end > columnStart
|
||||
const overlaps = event.start < columnEnd && event.end > columnStart;
|
||||
return overlaps;
|
||||
});
|
||||
|
||||
return columnEvents;
|
||||
|
|
|
|||
150
stacking-visualization-new.html
Normal file
150
stacking-visualization-new.html
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Event Stacking Scenarios - Test Suite</title>
|
||||
<link rel="stylesheet" href="scenarios/scenario-styles.css">
|
||||
<style>
|
||||
.scenario-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.scenario-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.scenario-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.scenario-card h3 {
|
||||
margin-top: 0;
|
||||
color: #b53f7a;
|
||||
}
|
||||
|
||||
.scenario-card p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.scenario-link {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #b53f7a;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.scenario-link:hover {
|
||||
background: #8e3260;
|
||||
}
|
||||
|
||||
.intro {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-left: 4px solid #b53f7a;
|
||||
margin-bottom: 30px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.intro h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<h1>Event Stacking & Grid Layout - Test Scenarios</h1>
|
||||
|
||||
<div class="intro">
|
||||
<h2>About This Test Suite</h2>
|
||||
<p>
|
||||
This test suite validates the event layout algorithm for the Calendar Plantempus application.
|
||||
The algorithm determines how overlapping events should be rendered using two strategies:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>GRID Layout:</strong> Events that start within a threshold (±30 minutes) are placed in a grid container where they can share columns</li>
|
||||
<li><strong>STACKED Layout:</strong> Events are stacked with horizontal offsets (15px per level)</li>
|
||||
</ul>
|
||||
<p>
|
||||
Each scenario tests a specific edge case or layout pattern. Click on a scenario below to view the visual representation and test results.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="scenario-grid">
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 1: No Overlap</h3>
|
||||
<p>Three sequential events with no time overlap. All should have stack level 0.</p>
|
||||
<a href="scenarios/scenario-1.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 2: Column Sharing (Grid)</h3>
|
||||
<p>Two events starting at same time (10:00) - should share columns in a grid with 2 columns.</p>
|
||||
<a href="scenarios/scenario-2.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 3: Nested Stacking</h3>
|
||||
<p>Events with progressive nesting: A contains B, B contains C, C and D overlap. Tests stack level calculation.</p>
|
||||
<a href="scenarios/scenario-3.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 4: Complex Stacking</h3>
|
||||
<p>Long event (A) with multiple shorter events (B, C, D) nested inside at different times.</p>
|
||||
<a href="scenarios/scenario-4.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 5: Three Column Share</h3>
|
||||
<p>Three events all starting at 10:00 - should create a 3-column grid layout.</p>
|
||||
<a href="scenarios/scenario-5.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 6: Overlapping Pairs</h3>
|
||||
<p>Two separate pairs of overlapping events. Each pair should be independent.</p>
|
||||
<a href="scenarios/scenario-6.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 7: Long Event Container</h3>
|
||||
<p>One long event (A) containing two shorter events (B, C) that don't overlap each other.</p>
|
||||
<a href="scenarios/scenario-7.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 8: Edge-Adjacent Events</h3>
|
||||
<p>Events that touch but don't overlap (A ends when B starts). Should not stack.</p>
|
||||
<a href="scenarios/scenario-8.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 9: End-to-Start Chain</h3>
|
||||
<p>Events linked by end-to-start conflicts within threshold. Tests conflict chain detection.</p>
|
||||
<a href="scenarios/scenario-9.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 10: Four Column Grid</h3>
|
||||
<p>Four events all starting at same time - maximum column sharing test.</p>
|
||||
<a href="scenarios/scenario-10.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -209,6 +209,28 @@
|
|||
.single-column {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.test-badge {
|
||||
display: inline-block;
|
||||
margin-left: 15px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.test-passed {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
.test-failed {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
.test-pending {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue