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 gridSettings = calendarConfig.getGridSettings();
|
||||||
const thresholdMinutes = gridSettings.gridStartThresholdMinutes;
|
const thresholdMinutes = gridSettings.gridStartThresholdMinutes;
|
||||||
|
|
||||||
const gridCandidates = [firstEvent];
|
// Use refactored method for expanding grid candidates
|
||||||
let candidatesChanged = true;
|
const gridCandidates = this.expandGridCandidates(firstEvent, remaining, thresholdMinutes);
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decide: should this group be GRID or STACK?
|
// Decide: should this group be GRID or STACK?
|
||||||
const group: EventGroup = {
|
const group: EventGroup = {
|
||||||
|
|
@ -117,7 +76,8 @@ export class EventLayoutCoordinator {
|
||||||
renderedEventsWithLevels
|
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 position = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
|
||||||
const columns = this.allocateColumns(gridCandidates);
|
const columns = this.allocateColumns(gridCandidates);
|
||||||
|
|
||||||
|
|
@ -201,6 +161,78 @@ export class EventLayoutCoordinator {
|
||||||
return maxOverlappingLevel + 1;
|
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
|
* 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
|
// Delegate to SwpEventElement to update position and timestamps
|
||||||
const swpEvent = this.draggedClone as SwpEventElement;
|
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);
|
swpEvent.updatePosition(columnDate, payload.snappedY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,11 +111,11 @@ export class DateEventRenderer implements EventRendererStrategy {
|
||||||
const eventsLayer = dragColumnChangeEvent.newColumn.element.querySelector('swp-events-layer');
|
const eventsLayer = dragColumnChangeEvent.newColumn.element.querySelector('swp-events-layer');
|
||||||
if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) {
|
if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) {
|
||||||
eventsLayer.appendChild(this.draggedClone);
|
eventsLayer.appendChild(this.draggedClone);
|
||||||
|
|
||||||
// Recalculate timestamps with new column date
|
// Recalculate timestamps with new column date
|
||||||
const currentTop = parseFloat(this.draggedClone.style.top) || 0;
|
const currentTop = parseFloat(this.draggedClone.style.top) || 0;
|
||||||
const swpEvent = this.draggedClone as SwpEventElement;
|
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);
|
swpEvent.updatePosition(columnDate, currentTop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -221,16 +221,15 @@ export class DateEventRenderer implements EventRendererStrategy {
|
||||||
// Position from layout
|
// Position from layout
|
||||||
groupElement.style.top = `${gridGroup.position.top}px`;
|
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)
|
// Add stack-link attribute for drag-drop (group acts as a stacked item)
|
||||||
const stackLink = {
|
const stackLink = {
|
||||||
stackLevel: gridGroup.stackLevel
|
stackLevel: gridGroup.stackLevel
|
||||||
};
|
};
|
||||||
this.stackManager.applyStackLinkToElement(groupElement, stackLink);
|
this.stackManager.applyStackLinkToElement(groupElement, stackLink);
|
||||||
|
|
||||||
|
// Apply visual styling (margin-left and z-index) using StackManager
|
||||||
|
this.stackManager.applyVisualStyling(groupElement, gridGroup.stackLevel);
|
||||||
|
|
||||||
// Render each column
|
// Render each column
|
||||||
const earliestEvent = gridGroup.events[0];
|
const earliestEvent = gridGroup.events[0];
|
||||||
gridGroup.columns.forEach(columnEvents => {
|
gridGroup.columns.forEach(columnEvents => {
|
||||||
|
|
@ -330,11 +329,14 @@ export class DateEventRenderer implements EventRendererStrategy {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnEvents = events.filter(event => {
|
// Create start and end of day for interval overlap check
|
||||||
const eventDateStr = this.dateService.formatISODate(event.start);
|
const columnStart = this.dateService.parseISO(`${columnDate}T00:00:00`);
|
||||||
const matches = eventDateStr === columnDate;
|
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;
|
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 {
|
.single-column {
|
||||||
max-width: 600px;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue