Enhances event layout engine with advanced rendering logic

Introduces sophisticated event layout algorithm for handling complex scheduling scenarios

Adds support for:
- Grid and stacked event rendering
- Automatic column allocation
- Nested event stacking
- Threshold-based event grouping

Improves visual representation of overlapping and concurrent events
This commit is contained in:
Janus C. H. Knudsen 2025-12-11 18:11:11 +01:00
parent 4e22fbc948
commit 70172e8f10
26 changed files with 2108 additions and 44 deletions

View file

@ -0,0 +1,307 @@
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>V2 Event Layout Engine - All Scenarios</title>
<link rel="stylesheet" href="scenario-styles.css">
<style>
.scenarios-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-top: 20px;
}
.scenario-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
}
.scenario-card h3 {
margin-top: 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.calendar-column {
height: 600px;
margin-top: 10px;
}
.summary {
background: #f0f0f0;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.summary h2 {
margin-top: 0;
}
.pass-count { color: #4caf50; font-weight: bold; }
.fail-count { color: #f44336; font-weight: bold; }
.expected-info {
font-size: 12px;
color: #666;
background: #f8f9fa;
padding: 8px;
border-radius: 4px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="scenario-container" style="max-width: 1600px;">
<h1>V2 Event Layout Engine - Visual Tests</h1>
<div class="summary">
<h2>Test Summary</h2>
<p>
<span class="pass-count" id="pass-count">0</span> passed,
<span class="fail-count" id="fail-count">0</span> failed
</p>
<p>Using V2 EventLayoutEngine with gridStartThresholdMinutes: 30</p>
</div>
<div class="scenarios-grid" id="scenarios-container">
<!-- Scenarios will be rendered here -->
</div>
</div>
<script type="module">
import { renderScenario, calculateColumnLayout, gridConfig } from './v2-scenario-renderer.js';
import { ScenarioTestRunner } from './scenario-test-runner.js';
// ============================================
// Scenario Definitions
// ============================================
const scenarios = [
{
id: 'scenario-1',
title: 'Scenario 1: No Overlap',
description: 'Three sequential events with no time overlap',
events: [
{ id: 'S1A', title: 'Event A', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
{ id: 'S1B', title: 'Event B', start: new Date('2025-10-06T11:00'), end: new Date('2025-10-06T12:00') },
{ id: 'S1C', title: 'Event C', start: new Date('2025-10-06T12:00'), end: new Date('2025-10-06T13:00') }
],
expected: [
{ eventId: 'S1A', stackLevel: 0, type: 'stacked' },
{ eventId: 'S1B', stackLevel: 0, type: 'stacked' },
{ eventId: 'S1C', stackLevel: 0, type: 'stacked' }
]
},
{
id: 'scenario-2',
title: 'Scenario 2: Column Sharing (Grid)',
description: 'Two events starting at exactly the same time',
events: [
{ id: 'S2A', title: 'Event A', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
{ id: 'S2B', title: 'Event B', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') }
],
expected: [
{ eventId: 'S2A', stackLevel: 0, cols: 2, type: 'grid' },
{ eventId: 'S2B', stackLevel: 0, cols: 2, type: 'grid' }
]
},
{
id: 'scenario-3',
title: 'Scenario 3: Nested Stacking',
description: 'Progressive nesting: A contains B, B contains C, C overlaps D',
events: [
{ id: 'S3A', title: 'Event A', start: new Date('2025-10-06T09:00'), end: new Date('2025-10-06T15:00') },
{ id: 'S3B', title: 'Event B', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T13:00') },
{ id: 'S3C', title: 'Event C', start: new Date('2025-10-06T11:00'), end: new Date('2025-10-06T12:00') },
{ id: 'S3D', title: 'Event D', start: new Date('2025-10-06T12:30'), end: new Date('2025-10-06T13:30') }
],
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' }
]
},
{
id: 'scenario-4',
title: 'Scenario 4: Complex Stacking',
description: 'Long event A with nested B, C, D at different times',
events: [
{ id: 'S4A', title: 'Event A', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T20:00') },
{ id: 'S4B', title: 'Event B', start: new Date('2025-10-06T15:00'), end: new Date('2025-10-06T17:00') },
{ id: 'S4C', title: 'Event C', start: new Date('2025-10-06T15:30'), end: new Date('2025-10-06T16:30') },
{ id: 'S4D', title: 'Event D', start: new Date('2025-10-06T18:00'), end: new Date('2025-10-06T19:00') }
],
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' }
]
},
{
id: 'scenario-5',
title: 'Scenario 5: Three Column Share',
description: 'Three events all starting at the same time',
events: [
{ id: 'S5A', title: 'Event A', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
{ id: 'S5B', title: 'Event B', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
{ id: 'S5C', title: 'Event C', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') }
],
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' }
]
},
{
id: 'scenario-6',
title: 'Scenario 6: Overlapping Pairs',
description: 'Two independent pairs of overlapping events',
events: [
{ id: 'S6A', title: 'Event A', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T12:00') },
{ id: 'S6B', title: 'Event B', start: new Date('2025-10-06T11:00'), end: new Date('2025-10-06T12:00') },
{ id: 'S6C', title: 'Event C', start: new Date('2025-10-06T13:00'), end: new Date('2025-10-06T15:00') },
{ id: 'S6D', title: 'Event D', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T15:00') }
],
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' }
]
},
{
id: 'scenario-7',
title: 'Scenario 7: Long Event Container',
description: 'Long event containing two non-overlapping events',
events: [
{ id: 'S7A', title: 'Event A', start: new Date('2025-10-06T09:00'), end: new Date('2025-10-06T15:00') },
{ id: 'S7B', title: 'Event B', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
{ id: 'S7C', title: 'Event C', start: new Date('2025-10-06T12:00'), end: new Date('2025-10-06T13:00') }
],
expected: [
{ eventId: 'S7A', stackLevel: 0, type: 'stacked' },
{ eventId: 'S7B', stackLevel: 1, type: 'stacked' },
{ eventId: 'S7C', stackLevel: 1, type: 'stacked' }
]
},
{
id: 'scenario-8',
title: 'Scenario 8: Edge-Adjacent Events',
description: 'Events that touch but do not overlap (end === start)',
events: [
{ id: 'S8A', title: 'Event A', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
{ id: 'S8B', title: 'Event B', start: new Date('2025-10-06T11:00'), end: new Date('2025-10-06T12:00') }
],
expected: [
{ eventId: 'S8A', stackLevel: 0, type: 'stacked' },
{ eventId: 'S8B', stackLevel: 0, type: 'stacked' }
]
},
{
id: 'scenario-9',
title: 'Scenario 9: End-to-Start Chain',
description: 'Events linked by conflict chain: A->B->C',
events: [
{ id: 'S9A', title: 'Event A', start: new Date('2025-10-06T12:00'), end: new Date('2025-10-06T13:00') },
{ id: 'S9B', title: 'Event B', start: new Date('2025-10-06T12:30'), end: new Date('2025-10-06T13:30') },
{ id: 'S9C', title: 'Event C', start: new Date('2025-10-06T13:15'), end: new Date('2025-10-06T15:00') }
],
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' }
]
},
{
id: 'scenario-10',
title: 'Scenario 10: Four Column Grid',
description: 'Four events all starting at the same time',
events: [
{ id: 'S10A', title: 'Event A', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T15:00') },
{ id: 'S10B', title: 'Event B', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T15:00') },
{ id: 'S10C', title: 'Event C', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T15:00') },
{ id: 'S10D', title: 'Event D', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T15:00') }
],
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' }
]
}
];
// ============================================
// Render All Scenarios
// ============================================
const container = document.getElementById('scenarios-container');
let passCount = 0;
let failCount = 0;
scenarios.forEach(scenario => {
// Create card
const card = document.createElement('div');
card.className = 'scenario-card';
card.innerHTML = `
<h3>
${scenario.title}
<span class="test-badge test-pending" id="badge-${scenario.id}">Testing...</span>
</h3>
<p>${scenario.description}</p>
<div class="expected-info">
Expected: ${scenario.expected.map(e =>
e.type === 'grid'
? `${e.eventId} (grid cols-${e.cols})`
: `${e.eventId} (level ${e.stackLevel})`
).join(', ')}
</div>
<div class="calendar-column" id="column-${scenario.id}"></div>
<div id="results-${scenario.id}"></div>
`;
container.appendChild(card);
// Render events
const columnEl = document.getElementById(`column-${scenario.id}`);
renderScenario(columnEl, scenario.events);
// Run validation
const results = ScenarioTestRunner.validateScenario(scenario.id, scenario.expected);
// Update badge
const badge = document.getElementById(`badge-${scenario.id}`);
badge.className = `test-badge ${results.passed ? 'test-passed' : 'test-failed'}`;
badge.textContent = results.passed ? 'PASSED' : 'FAILED';
if (results.passed) {
passCount++;
} else {
failCount++;
}
// Show detailed results if failed
if (!results.passed) {
const resultsEl = document.getElementById(`results-${scenario.id}`);
resultsEl.innerHTML = '<div class="test-details"><h4>Failed checks:</h4>' +
results.results
.filter(r => !r.passed)
.map(r => `<div class="test-result-line failed">${r.eventId}: ${r.message}</div>`)
.join('') +
'</div>';
}
});
// Update summary
document.getElementById('pass-count').textContent = passCount;
document.getElementById('fail-count').textContent = failCount;
</script>
</body>
</html>